{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"input-otp","type":"registry:ui","title":"Input OTP","description":"A one-time code in boxes, one character each: typed, pasted or filled from a text message.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/field.json"],"files":[{"path":"registry/dashral/input-otp.tsx","type":"registry:file","target":"components/dashral/input-otp.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"./lib/utils\"\nimport { FieldWrapper, splitFieldProps, useFieldControl, type FieldClassNames, type FieldOwnProps } from \"./field\"\n\ntype InputOTPProps = Omit<React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" | \"maxLength\" | \"type\" | \"size\" | \"pattern\"> &\n  FieldOwnProps & {\n    /** How many characters the code has. */\n    length?: number\n    value?: string\n    defaultValue?: string\n    onValueChange?: (value: string) => void\n    /** Called once every slot is filled, e.g. to verify the code right away. */\n    onComplete?: (value: string) => void\n    /** What can be typed: digits (the default, with the number keypad on phones) or letters and digits. */\n    pattern?: \"digits\" | \"alphanumeric\"\n    /** Splits the slots into groups with a dash between, e.g. [3, 3]. */\n    groups?: number[]\n    /** Dots in place of the characters, for a PIN. */\n    mask?: boolean\n    size?: \"default\" | \"lg\"\n    classNames?: FieldClassNames & { slots?: string; slot?: string }\n  }\n\nconst allowed = { digits: /[^0-9]/g, alphanumeric: /[^0-9a-zA-Z]/g }\n\n/**\n * A one-time code in boxes, one character each: type, paste or let the phone fill it from a text message (it's one\n * real input underneath, with autocomplete=\"one-time-code\"). Give it a label, description or error and it becomes a\n * whole field. onComplete fires once every box is filled.\n */\nfunction InputOTP(props: InputOTPProps) {\n  const [field, control] = splitFieldProps(props)\n  return (\n    <FieldWrapper {...field}>\n      <InputOTPControl {...control} />\n    </FieldWrapper>\n  )\n}\n\nfunction InputOTPControl({\n  length = 6,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onComplete,\n  pattern = \"digits\",\n  groups,\n  mask = false,\n  size = \"default\",\n  className,\n  classNames,\n  onFocus,\n  onBlur,\n  ...rest\n}: Omit<InputOTPProps, keyof FieldOwnProps>) {\n  const props = useFieldControl(rest)\n  const [own, setOwn] = React.useState(defaultValue)\n  const code = (value ?? own).slice(0, length)\n  const [focused, setFocused] = React.useState(false)\n  const input = React.useRef<HTMLInputElement>(null)\n\n  function change(raw: string) {\n    const next = raw.replace(allowed[pattern], \"\").slice(0, length).toUpperCase()\n    if (next === code) return\n    if (value === undefined) setOwn(next)\n    onValueChange?.(next)\n    if (next.length === length) onComplete?.(next)\n  }\n\n  // The box the next character goes in: after the last one typed, or the last box once they're all filled.\n  const active = Math.min(code.length, length - 1)\n  const runs = groups && groups.reduce((sum, n) => sum + n, 0) === length ? groups : [length]\n  let start = 0\n\n  return (\n    <div\n      data-slot=\"input-otp\"\n      data-disabled={props.disabled || undefined}\n      className={cn(\"group/otp relative flex w-fit items-center gap-2 has-disabled:opacity-50\", className)}\n    >\n      <input\n        ref={input}\n        {...props}\n        type=\"text\"\n        value={code}\n        onChange={(event) => change(event.target.value)}\n        onFocus={(event) => {\n          setFocused(true)\n          // Typing always goes on at the end, so a stray tap in the middle doesn't type over a box.\n          const end = event.currentTarget.value.length\n          event.currentTarget.setSelectionRange(end, end)\n          onFocus?.(event)\n        }}\n        onBlur={(event) => {\n          setFocused(false)\n          onBlur?.(event)\n        }}\n        onSelect={(event) => {\n          const end = event.currentTarget.value.length\n          if (event.currentTarget.selectionStart !== end) event.currentTarget.setSelectionRange(end, end)\n        }}\n        maxLength={length}\n        inputMode={pattern === \"digits\" ? \"numeric\" : \"text\"}\n        autoComplete=\"one-time-code\"\n        autoCapitalize=\"characters\"\n        spellCheck={false}\n        // Over the boxes, invisible: it takes the taps, the keys and what's pasted or filled in.\n        className=\"absolute inset-0 z-10 size-full cursor-text appearance-none bg-transparent text-transparent caret-transparent opacity-0 outline-none disabled:cursor-not-allowed\"\n      />\n      {runs.map((run, i) => {\n        const from = start\n        start += run\n        return (\n          <React.Fragment key={i}>\n            {i > 0 && <span aria-hidden className=\"h-0.5 w-2 shrink-0 rounded-full bg-border\" />}\n            <div aria-hidden className={cn(\"flex gap-2\", classNames?.slots)}>\n              {Array.from({ length: run }, (_, j) => {\n                const index = from + j\n                const char = code[index]\n                const current = focused && index === active\n                return (\n                  <div\n                    key={index}\n                    data-slot=\"input-otp-slot\"\n                    data-active={current || undefined}\n                    data-filled={char ? \"\" : undefined}\n                    className={cn(\n                      \"relative grid place-items-center rounded-md border border-input bg-transparent font-medium shadow-xs transition-[color,box-shadow,border-color] tabular-nums\",\n                      size === \"lg\" ? \"size-12 text-xl\" : \"size-10 text-lg\",\n                      \"data-active:border-ring data-active:ring-2 data-active:ring-ring/15\",\n                      \"group-has-[[aria-invalid=true]]/otp:border-destructive group-has-[[aria-invalid=true]]/otp:data-active:ring-destructive/15\",\n                      classNames?.slot\n                    )}\n                  >\n                    {char && (mask ? <span className=\"size-2 rounded-full bg-current\" /> : char)}\n                    {/* The caret, blinking in the box that's next, while it's empty. */}\n                    {current && !char && <span className=\"h-5 w-px animate-pulse bg-foreground [animation-duration:1s] motion-reduce:animate-none\" />}\n                  </div>\n                )\n              })}\n            </div>\n          </React.Fragment>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { InputOTP, type InputOTPProps }\n"}]}