{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "otp-input",
  "title": "OTP Input",
  "description": "A one time code, one box per character, where pasting the whole code works.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/otp-input/otp-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type OtpInputProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  length?: number\n  value?: string\n  defaultValue?: string\n  onChange?: (value: string) => void\n  /** Called once the last box is filled. */\n  onComplete?: (value: string) => void\n  /** Which characters are allowed. Digits only by default. */\n  pattern?: RegExp\n  label?: string\n  disabled?: boolean\n  autoFocus?: boolean\n}\n\nconst DIGITS = /^[0-9]$/\n\n/**\n * A one time code, one box per character. Pasting the whole code into any box\n * fills the rest, which is what people do with the code their phone just\n * showed them.\n */\nexport function OtpInput({\n  length = 6,\n  value,\n  defaultValue = \"\",\n  onChange,\n  onComplete,\n  pattern = DIGITS,\n  label = \"One time code\",\n  disabled,\n  autoFocus,\n  className,\n  ...rootProps\n}: OtpInputProps) {\n  const boxes = React.useRef<(HTMLInputElement | null)[]>([])\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n  const current = (value ?? uncontrolled).slice(0, length)\n  const completed = React.useRef(false)\n\n  const commit = (next: string) => {\n    if (value === undefined) setUncontrolled(next)\n    onChange?.(next)\n\n    if (next.length === length && !completed.current) {\n      completed.current = true\n      onComplete?.(next)\n    }\n\n    if (next.length < length) completed.current = false\n  }\n\n  const focusBox = (index: number) => {\n    const box = boxes.current[Math.min(Math.max(index, 0), length - 1)]\n    box?.focus()\n    box?.select()\n  }\n\n  const write = (index: number, characters: string) => {\n    const allowed = Array.from(characters).filter((character) =>\n      pattern.test(character)\n    )\n\n    if (allowed.length === 0) return\n\n    const filled = current.padEnd(length, \" \").split(\"\")\n    allowed.forEach((character, offset) => {\n      if (index + offset < length) filled[index + offset] = character\n    })\n\n    commit(filled.join(\"\").trimEnd())\n    focusBox(index + allowed.length)\n  }\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={label}\n      data-slot=\"otp-input\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...rootProps}\n    >\n      {Array.from({ length }, (_, index) => {\n        const character = current[index] ?? \"\"\n\n        return (\n          <input\n            key={index}\n            ref={(node) => {\n              boxes.current[index] = node\n            }}\n            data-slot=\"otp-input-box\"\n            data-filled={character ? \"\" : undefined}\n            inputMode={pattern === DIGITS ? \"numeric\" : \"text\"}\n            autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n            autoFocus={autoFocus && index === 0}\n            disabled={disabled}\n            aria-label={`${label}, character ${index + 1} of ${length}`}\n            value={character}\n            className={cn(\n              \"border-border bg-background focus-visible:border-ring focus-visible:ring-ring size-12 rounded-[calc(var(--radius)-0.25rem)] border text-center text-lg font-semibold\",\n              \"transition-[transform,border-color] duration-150 focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\",\n              \"data-filled:border-foreground disabled:opacity-50 data-filled:scale-105\"\n            )}\n            onChange={(event) => {\n              const typed = event.target.value.replace(character, \"\")\n              write(index, typed || event.target.value)\n            }}\n            onPaste={(event) => {\n              event.preventDefault()\n              write(index, event.clipboardData.getData(\"text\"))\n            }}\n            onKeyDown={(event) => {\n              if (event.key === \"Backspace\") {\n                event.preventDefault()\n                const filled = current.split(\"\")\n\n                if (filled[index]) {\n                  filled[index] = \"\"\n                  commit(filled.join(\"\").trimEnd())\n                } else {\n                  filled[index - 1] = \"\"\n                  commit(filled.join(\"\").trimEnd())\n                  focusBox(index - 1)\n                }\n              }\n\n              if (event.key === \"ArrowLeft\") {\n                event.preventDefault()\n                focusBox(index - 1)\n              }\n\n              if (event.key === \"ArrowRight\") {\n                event.preventDefault()\n                focusBox(index + 1)\n              }\n            }}\n          />\n        )\n      })}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "form",
    "input",
    "auth",
    "otp"
  ],
  "type": "registry:ui"
}