{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "otp-input",
  "title": "OTP input",
  "description": "N-digit code, paste-aware",
  "dependencies": [
    "cn@^0.4.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/kit/otp-input.tsx",
      "content": "'use client';\nimport { cn } from 'cn';\nimport * as React from 'react';\n\ntype Props = {\n  length?: number;\n  value: string;\n  onChange: (next: string) => void;\n  onComplete?: (code: string) => void;\n  error?: string;\n  autoFocus?: boolean;\n  className?: string;\n};\n\n/** N-digit one-time-code entry: auto-advances, handles paste, backspaces into the previous cell. */\nexport function OtpInput({ length = 6, value, onChange, onComplete, error, autoFocus, className }: Props) {\n  const refs = React.useRef<(HTMLInputElement | null)[]>([]);\n  const digits = Array.from({ length }, (_, i) => (value ?? '').replace(/\\D/g, '').slice(0, length)[i] ?? '');\n\n  const commit = (next: string[]) => {\n    const joined = next.join('');\n    onChange(joined);\n    if (joined.length === length && !next.includes('')) onComplete?.(joined);\n  };\n  const setAt = (idx: number, raw: string) => {\n    const clean = raw.replace(/\\D/g, '');\n    const next = [...digits];\n    if (clean.length > 1) {\n      // A paste, or an autofilled code.\n      for (let i = 0; i < clean.length && idx + i < length; i++) next[idx + i] = clean[i];\n      commit(next);\n      refs.current[Math.min(idx + clean.length, length - 1)]?.focus();\n      return;\n    }\n    next[idx] = clean;\n    commit(next);\n    if (clean && idx < length - 1) refs.current[idx + 1]?.focus();\n  };\n\n  return (\n    <div className={className}>\n      <div role=\"group\" aria-label={`${length}-digit code`} className=\"flex gap-2\">\n        {digits.map((d, idx) => (\n          <input\n            key={idx}\n            ref={(r) => {\n              refs.current[idx] = r;\n            }}\n            value={d}\n            autoFocus={autoFocus && idx === 0}\n            onChange={(e) => setAt(idx, e.target.value)}\n            onKeyDown={(e) => {\n              if (e.key === 'Backspace' && !digits[idx] && idx > 0) {\n                e.preventDefault();\n                const next = [...digits];\n                next[idx - 1] = '';\n                onChange(next.join(''));\n                refs.current[idx - 1]?.focus();\n              }\n            }}\n            onFocus={(e) => e.target.select()}\n            inputMode=\"numeric\"\n            autoComplete={idx === 0 ? 'one-time-code' : 'off'}\n            maxLength={idx === 0 ? length : 1}\n            aria-label={`Digit ${idx + 1} of ${length}`}\n            aria-invalid={!!error}\n            className={cn(\n              'bg-card text-foreground focus-visible:ring-ring/50 h-14 w-full min-w-0 flex-1 rounded-lg border text-center text-2xl font-bold tabular-nums outline-none focus-visible:ring-[3px]',\n              error ? 'border-destructive' : 'border-border focus-visible:border-ring'\n            )}\n          />\n        ))}\n      </div>\n      {error ? (\n        <p role=\"alert\" className=\"text-destructive mt-2 text-center text-sm\">\n          {error}\n        </p>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/otp-input.tsx"
    }
  ],
  "meta": {
    "exports": [
      "OtpInput"
    ],
    "api": "<OtpInput length value onChange onComplete error? />"
  },
  "docs": "Needs the kit theme (`@kit/theme`) and a next-themes ThemeProvider (attribute=\"class\") at the root.",
  "categories": [
    "inputs"
  ],
  "type": "registry:component"
}