{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gummy-input-otp",
  "type": "registry:ui",
  "title": "Gummy Input OTP",
  "description": "Labelled one-time-code group with numeric slots, paste distribution, forward and backward focus, hidden form value, and LTR digit order.",
  "registryDependencies": [
    "https://gummyui.dev/r/gummy-primitives-styles.json"
  ],
  "files": [
    {
      "path": "components/ui/GummyInputOTP.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nfunction joinClassNames(...values: Array<string | false | null | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\nexport type GummyInputOTPProps = Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> & {\n  label: string;\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  disabled?: boolean;\n  invalid?: boolean;\n  name?: string;\n};\n\nexport const GummyInputOTP = React.forwardRef<HTMLDivElement, GummyInputOTPProps>(\n  function GummyInputOTP(\n    {\n      label,\n      length = 6,\n      value,\n      defaultValue = \"\",\n      onValueChange,\n      disabled,\n      invalid,\n      name,\n      className,\n      ...props\n    },\n    ref,\n  ) {\n    const safeLength = Math.max(4, Math.min(8, Math.floor(length)));\n    const [internal, setInternal] = React.useState(defaultValue.replace(/\\D/g, \"\").slice(0, safeLength));\n    const current = (value ?? internal).padEnd(safeLength, \" \").slice(0, safeLength);\n    const id = React.useId().replace(/:/g, \"\");\n    function update(next: string) {\n      const clean = next.replace(/\\D/g, \"\").slice(0, safeLength);\n      if (value === undefined) setInternal(clean);\n      onValueChange?.(clean);\n    }\n    function focus(index: number) {\n      document.getElementById(`${id}-${Math.max(0, Math.min(safeLength - 1, index))}`)?.focus();\n    }\n    return (\n      <div {...props} ref={ref} className={joinClassNames(\"gummy-otp\", className)} role=\"group\" aria-labelledby={`${id}-label`} data-invalid={invalid || undefined}>\n        <span id={`${id}-label`} className=\"gummy-otp__label\">{label}</span>\n        <div className=\"gummy-otp__slots\" dir=\"ltr\" onPaste={(event) => {\n          const pasted = event.clipboardData.getData(\"text\").replace(/\\D/g, \"\");\n          if (pasted) {\n            event.preventDefault();\n            update(pasted);\n            requestAnimationFrame(() => focus(Math.min(pasted.length, safeLength) - 1));\n          }\n        }}>\n          {Array.from({ length: safeLength }, (_, index) => (\n            <input\n              key={index}\n              id={`${id}-${index}`}\n              aria-label={`Digit ${index + 1} of ${safeLength}`}\n              inputMode=\"numeric\"\n              autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n              pattern=\"[0-9]*\"\n              maxLength={1}\n              disabled={disabled}\n              aria-invalid={invalid || undefined}\n              value={current[index].trim()}\n              onChange={(event) => {\n                const digit = event.currentTarget.value.replace(/\\D/g, \"\").slice(-1);\n                const chars = current.trimEnd().split(\"\");\n                chars[index] = digit;\n                update(chars.join(\"\"));\n                if (digit) focus(index + 1);\n              }}\n              onKeyDown={(event) => {\n                if (event.key === \"Backspace\" && !current[index].trim() && index > 0) {\n                  event.preventDefault();\n                  const chars = current.trimEnd().split(\"\");\n                  chars[index - 1] = \"\";\n                  update(chars.join(\"\"));\n                  focus(index - 1);\n                } else if (event.key === \"ArrowLeft\") focus(index - 1);\n                else if (event.key === \"ArrowRight\") focus(index + 1);\n              }}\n            />\n          ))}\n        </div>\n        {name ? <input type=\"hidden\" name={name} value={current.trim()} /> : null}\n      </div>\n    );\n  },\n);\n\nGummyInputOTP.displayName = \"GummyInputOTP\";\n"
    }
  ]
}
