{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"otp-input","type":"registry:ui","title":"OtpInput","description":"OtpInput from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":["inputs"],"files":[{"path":"components\/fancy\/otp-input\/OtpInput.tsx","content":"import { forwardRef, useCallback, useRef } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useControllableState } from \"..\/..\/hooks\/use-controllable-state\";\r\nimport { useFieldMode } from \"..\/inputs\/mode\/FieldMode.context\";\r\nimport { useInlineEdit } from \"..\/inputs\/mode\/useInlineEdit\";\r\nimport type { OtpInputProps } from \".\/OtpInput.types\";\r\n\r\nexport const OtpInput = forwardRef<HTMLDivElement, OtpInputProps>(\r\n  function OtpInput(\r\n    {\r\n      length = 6,\r\n      value: controlledValue,\r\n      onChange,\r\n      disabled = false,\r\n      autoFocus = false,\r\n      className,\r\n      mode,\r\n    },\r\n    ref,\r\n  ) {\r\n    const [value, setValue] = useControllableState(\r\n      controlledValue,\r\n      \"\",\r\n      onChange,\r\n    );\r\n    const resolvedMode = useFieldMode(mode);\r\n    const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\r\n    const inputsRef = useRef<(HTMLInputElement | null)[]>([]);\r\n\r\n    const focusInput = useCallback(\r\n      (index: number) => {\r\n        const clamped = Math.max(0, Math.min(index, length - 1));\r\n        inputsRef.current[clamped]?.focus();\r\n      },\r\n      [length],\r\n    );\r\n\r\n    const handleChange = useCallback(\r\n      (index: number, char: string) => {\r\n        const digit = char.replace(\/[^0-9]\/g, \"\");\r\n        if (!digit) return;\r\n\r\n        const chars = value.padEnd(length, \" \").split(\"\");\r\n        chars[index] = digit[0];\r\n        const newValue = chars.join(\"\").trimEnd();\r\n        setValue(newValue);\r\n\r\n        if (index < length - 1) {\r\n          focusInput(index + 1);\r\n        }\r\n      },\r\n      [value, length, setValue, focusInput],\r\n    );\r\n\r\n    const handleKeyDown = useCallback(\r\n      (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {\r\n        if (e.key === \"Backspace\") {\r\n          e.preventDefault();\r\n          const chars = value.padEnd(length, \" \").split(\"\");\r\n          if (chars[index]?.trim()) {\r\n            chars[index] = \" \";\r\n            setValue(chars.join(\"\").trimEnd());\r\n          } else if (index > 0) {\r\n            chars[index - 1] = \" \";\r\n            setValue(chars.join(\"\").trimEnd());\r\n            focusInput(index - 1);\r\n          }\r\n        } else if (e.key === \"ArrowLeft\") {\r\n          focusInput(index - 1);\r\n        } else if (e.key === \"ArrowRight\") {\r\n          focusInput(index + 1);\r\n        }\r\n      },\r\n      [value, length, setValue, focusInput],\r\n    );\r\n\r\n    const handlePaste = useCallback(\r\n      (e: React.ClipboardEvent) => {\r\n        e.preventDefault();\r\n        const pasted = e.clipboardData.getData(\"text\").replace(\/[^0-9]\/g, \"\");\r\n        const newValue = pasted.slice(0, length);\r\n        setValue(newValue);\r\n        focusInput(Math.min(newValue.length, length - 1));\r\n      },\r\n      [length, setValue, focusInput],\r\n    );\r\n\r\n    if (!showControl) {\r\n      return (\r\n        <div\r\n          data-react-fancy-otp-input=\"\"\r\n          data-mode=\"view\"\r\n          ref={ref}\r\n          role={interactive ? \"button\" : undefined}\r\n          tabIndex={interactive ? 0 : undefined}\r\n          title={interactive ? \"Click to edit\" : undefined}\r\n          onClick={interactive ? enterEdit : undefined}\r\n          onKeyDown={\r\n            interactive\r\n              ? (e) => {\r\n                  if (e.key === \"Enter\" || e.key === \" \") {\r\n                    e.preventDefault();\r\n                    enterEdit();\r\n                  }\r\n                }\r\n              : undefined\r\n          }\r\n          className={cn(\r\n            \"font-mono text-lg tracking-[0.4em] text-zinc-900 dark:text-zinc-100\",\r\n            interactive &&\r\n              \"cursor-pointer rounded-md outline-none transition-opacity hover:opacity-80 focus-visible:ring-2 focus-visible:ring-blue-500\/40\",\r\n            className,\r\n          )}\r\n        >\r\n          {value || \"\u2014\"}\r\n        <\/div>\r\n      );\r\n    }\r\n\r\n    return (\r\n      <div\r\n        data-react-fancy-otp-input=\"\"\r\n        ref={ref}\r\n        className={cn(\"flex gap-2\", className)}\r\n        onBlur={(e) => {\r\n          if (interactive && !e.currentTarget.contains(e.relatedTarget as Node)) exitEdit();\r\n        }}\r\n      >\r\n        {Array.from({ length }, (_, i) => (\r\n          <input\r\n            key={i}\r\n            ref={(el) => {\r\n              inputsRef.current[i] = el;\r\n            }}\r\n            type=\"text\"\r\n            inputMode=\"numeric\"\r\n            maxLength={1}\r\n            value={value[i] ?? \"\"}\r\n            onChange={(e) => handleChange(i, e.target.value)}\r\n            onKeyDown={(e) => handleKeyDown(i, e)}\r\n            onPaste={handlePaste}\r\n            onFocus={(e) => e.target.select()}\r\n            disabled={disabled}\r\n            autoFocus={(autoFocus || interactive) && i === 0}\r\n            className=\"h-12 w-10 rounded-lg border border-zinc-200 bg-white text-center text-lg font-medium text-zinc-900 outline-none transition-[border-color,box-shadow] duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500\/40 dark:border-zinc-700 dark:bg-[#1e1e24] dark:text-zinc-100 dark:focus:border-blue-400 dark:focus:ring-blue-400\/20\"\r\n            aria-label={`Digit ${i + 1}`}\r\n          \/>\r\n        ))}\r\n      <\/div>\r\n    );\r\n  },\r\n);\r\n\r\nOtpInput.displayName = \"OtpInput\";\r\n","type":"registry:ui","target":"components\/fancy\/otp-input\/OtpInput.tsx"},{"path":"components\/fancy\/otp-input\/OtpInput.types.ts","content":"import type { FieldMode } from \"..\/inputs\/inputs.types\";\r\n\r\nexport interface OtpInputProps {\r\n  length?: number;\r\n  value?: string;\r\n  onChange?: (value: string) => void;\r\n  disabled?: boolean;\r\n  autoFocus?: boolean;\r\n  className?: string;\r\n  \/** `\"edit\"` (default) renders the inputs; `\"view\"` renders the digits as text. *\/\r\n  mode?: FieldMode;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/otp-input\/OtpInput.types.ts"},{"path":"components\/fancy\/otp-input\/index.ts","content":"export { OtpInput } from \".\/OtpInput\";\r\nexport type { OtpInputProps } from \".\/OtpInput.types\";\r\n","type":"registry:ui","target":"components\/fancy\/otp-input\/index.ts"}]}