{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "type": "registry:ui",
  "files": [
    {
      "path": "ui/input-otp.tsx",
      "content": "import { type Component, type ComponentProps, createSignal, For, splitProps } from \"solid-js\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype InputOTPProps = Omit<ComponentProps<\"div\">, \"onChange\"> & {\n  maxLength: number\n  value?: string\n  onChange?: (value: string) => void\n  disabled?: boolean\n}\n\nconst InputOTP: Component<InputOTPProps> = (props) => {\n  const [local, others] = splitProps(props, [\n    \"class\",\n    \"maxLength\",\n    \"value\",\n    \"onChange\",\n    \"disabled\",\n    \"children\"\n  ])\n\n  const initial = (local.value ?? \"\").split(\"\")\n  const [values, setValues] = createSignal<string[]>(\n    Array.from({ length: local.maxLength }, (_, i) => initial[i] ?? \"\")\n  )\n\n  const inputRefs: HTMLInputElement[] = []\n\n  const focusInput = (index: number) => {\n    const ref = inputRefs[index]\n    if (ref) {\n      ref.focus()\n      ref.select()\n    }\n  }\n\n  const updateValue = (index: number, char: string) => {\n    const next = [...values()]\n    next[index] = char\n    setValues(next)\n    local.onChange?.(next.join(\"\"))\n  }\n\n  const handleInput = (index: number, e: Event) => {\n    const target = e.target as HTMLInputElement\n    const char = target.value.slice(-1)\n\n    if (char && !/^\\d$/.test(char)) {\n      target.value = values()[index]\n      return\n    }\n\n    updateValue(index, char)\n\n    if (char && index < local.maxLength - 1) {\n      focusInput(index + 1)\n    }\n  }\n\n  const handleKeyDown = (index: number, e: KeyboardEvent) => {\n    switch (e.key) {\n      case \"Backspace\": {\n        e.preventDefault()\n        if (values()[index]) {\n          updateValue(index, \"\")\n        } else if (index > 0) {\n          updateValue(index - 1, \"\")\n          focusInput(index - 1)\n        }\n        break\n      }\n      case \"ArrowLeft\": {\n        e.preventDefault()\n        if (index > 0) {\n          focusInput(index - 1)\n        }\n        break\n      }\n      case \"ArrowRight\": {\n        e.preventDefault()\n        if (index < local.maxLength - 1) {\n          focusInput(index + 1)\n        }\n        break\n      }\n      case \"Delete\": {\n        e.preventDefault()\n        updateValue(index, \"\")\n        break\n      }\n    }\n  }\n\n  const handlePaste = (index: number, e: ClipboardEvent) => {\n    e.preventDefault()\n    const pasted = e.clipboardData?.getData(\"text/plain\") ?? \"\"\n    const digits = pasted.replace(/\\D/g, \"\")\n\n    if (!digits) return\n\n    const chars = digits.split(\"\")\n    let currentIndex = index\n\n    for (const char of chars) {\n      if (currentIndex >= local.maxLength) break\n      updateValue(currentIndex, char)\n      currentIndex++\n    }\n\n    focusInput(Math.min(currentIndex, local.maxLength - 1))\n  }\n\n  const handleFocus = (e: FocusEvent) => {\n    const target = e.target as HTMLInputElement\n    target.select()\n  }\n\n  return (\n    <div class={cn(\"flex items-center gap-2\", local.class)} data-slot=\"input-otp\" {...others}>\n      <For each={Array.from({ length: local.maxLength }, (_, i) => i)}>\n        {(index) => (\n          <input\n            aria-label={`Digit ${index + 1} of ${local.maxLength}`}\n            autocomplete=\"one-time-code\"\n            class={cn(\n              \"h-9 w-9 rounded-md border border-input bg-transparent text-center text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n              \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n              \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\"\n            )}\n            data-slot=\"input-otp-slot\"\n            disabled={local.disabled}\n            inputmode=\"numeric\"\n            maxLength={1}\n            onFocus={handleFocus}\n            onInput={[handleInput, index]}\n            onKeyDown={[handleKeyDown, index]}\n            onPaste={[handlePaste, index]}\n            pattern=\"[0-9]\"\n            ref={(el) => {\n              inputRefs[index] = el\n            }}\n            type=\"text\"\n            value={values()[index]}\n          />\n        )}\n      </For>\n    </div>\n  )\n}\n\ntype InputOTPGroupProps = ComponentProps<\"div\">\n\nconst InputOTPGroup: Component<InputOTPGroupProps> = (props) => {\n  const [local, others] = splitProps(props, [\"class\"])\n  return (\n    <div\n      class={cn(\"flex items-center gap-2\", local.class)}\n      data-slot=\"input-otp-group\"\n      {...others}\n    />\n  )\n}\n\ntype InputOTPSeparatorProps = ComponentProps<\"div\">\n\nconst InputOTPSeparator: Component<InputOTPSeparatorProps> = (props) => {\n  const [local, others] = splitProps(props, [\"class\"])\n  return (\n    <div\n      aria-hidden=\"true\"\n      class={cn(\"flex items-center text-muted-foreground\", local.class)}\n      data-slot=\"input-otp-separator\"\n      role=\"separator\"\n      {...others}\n    >\n      <svg\n        class=\"size-4\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n        stroke-width=\"2\"\n        viewBox=\"0 0 24 24\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"1\" />\n      </svg>\n    </div>\n  )\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSeparator }\n",
      "type": "registry:ui"
    }
  ]
}