{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp-state",
  "title": "Otp-state input",
  "description": "Styled input: the Otp-state set. 5 decorative input variations (ValidOtp, InvalidOtp, LoadingOtp, LockedOtp, CompleteOtp) on the ai2 token system, driven by CSS token transitions and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/input-otp-state.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Loader2, Lock } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* State OTP family: 5 state presentations (valid, invalid, loading, locked,\n   complete). The difference is ONLY in the state feedback: a success border, the\n   aria-invalid danger appearance, a fixed-duration verification spinner,\n   locked/disabled cells, and a checkmark that appears once the code is complete.\n   Every cell is a real <input maxLength=1 inputMode=numeric>; typing a digit moves\n   focus forward, Backspace clears and moves back, and pasting fills every cell.\n   Color comes ONLY from tokens; the invalid appearance comes from the ai2 danger\n   tone. Self-sufficient: NO input-otp package and NO radix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype OtpProps = {\n  className?: string\n  size?: StyledSize\n  length?: number\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst cellSize: Record<StyledSize, string> = {\n  sm: \"size-8 text-sm\",\n  md: \"size-10 text-base\",\n  lg: \"size-12 text-lg\",\n  xl: \"size-14 text-xl\",\n}\n\nconst cellBase =\n  \"bg-transparent text-center font-medium tabular-nums text-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50\"\n\nconst rootBase = \"inline-flex items-center gap-2\"\n\nconst boxLook = \"rounded-md border focus-visible:border-ring\"\n\nconst invalidLook =\n  \"aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40\"\n\nconst iconBase = \"shrink-0 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\n/* Kontrollu/kontrolsuz OTP durumu + ileri/geri odak, yapistirma. */\nfunction useOtpCode(\n  length: number,\n  { value, defaultValue, onValueChange }: Pick<OtpProps, \"value\" | \"defaultValue\" | \"onValueChange\">\n) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string>(() => (defaultValue ?? \"\").slice(0, length))\n  const current = (isControlled ? value : internal) ?? \"\"\n\n  const chars = React.useMemo(() => {\n    const arr: string[] = Array(length).fill(\"\")\n    for (let i = 0; i < length; i++) arr[i] = current[i] ?? \"\"\n    return arr\n  }, [current, length])\n\n  const refs = React.useRef<Array<HTMLInputElement | null>>([])\n\n  const commit = (next: string) => {\n    if (!isControlled) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  const setAt = (i: number, raw: string) => {\n    const ch = raw.slice(-1)\n    const arr = [...chars]\n    arr[i] = ch\n    commit(arr.join(\"\"))\n    if (ch && i < length - 1) refs.current[i + 1]?.focus()\n  }\n\n  const onKey = (i: number, e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Backspace\") {\n      const arr = [...chars]\n      if (chars[i]) {\n        arr[i] = \"\"\n        commit(arr.join(\"\"))\n      } else if (i > 0) {\n        arr[i - 1] = \"\"\n        commit(arr.join(\"\"))\n        refs.current[i - 1]?.focus()\n      }\n      e.preventDefault()\n    } else if (e.key === \"ArrowLeft\" && i > 0) {\n      refs.current[i - 1]?.focus()\n    } else if (e.key === \"ArrowRight\" && i < length - 1) {\n      refs.current[i + 1]?.focus()\n    }\n  }\n\n  const onPaste = (i: number, e: React.ClipboardEvent<HTMLInputElement>) => {\n    e.preventDefault()\n    const text = e.clipboardData.getData(\"text\").replace(/\\s+/g, \"\")\n    if (!text) return\n    const arr = [...chars]\n    for (let k = 0; k < text.length && i + k < length; k++) arr[i + k] = text[k]\n    commit(arr.join(\"\"))\n    const last = Math.min(i + text.length, length - 1)\n    refs.current[last]?.focus()\n  }\n\n  const isComplete = chars.every((c) => c !== \"\")\n\n  return { chars, refs, setAt, onKey, onPaste, isComplete }\n}\n\ntype Controller = ReturnType<typeof useOtpCode>\n\nfunction cellHandlers(ctl: Controller, i: number) {\n  return {\n    ref: (el: HTMLInputElement | null) => {\n      ctl.refs.current[i] = el\n    },\n    value: ctl.chars[i],\n    onChange: (e: React.ChangeEvent<HTMLInputElement>) => ctl.setAt(i, e.target.value),\n    onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => ctl.onKey(i, e),\n    onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => ctl.onPaste(i, e),\n    inputMode: \"numeric\" as const,\n    autoComplete: i === 0 ? (\"one-time-code\" as const) : (\"off\" as const),\n    maxLength: 1,\n    \"aria-label\": `Digit ${i + 1}`,\n  }\n}\n\n/* Valid: dolan her hucre success kenarligi ve yumusak success dolgusu alir. */\nexport function ValidOtp({ className, size = \"md\", length = 6, value, defaultValue, onValueChange }: OtpProps) {\n  const ctl = useOtpCode(length, { value, defaultValue, onValueChange })\n  return (\n    <div\n      data-slot=\"styled-input-otp\"\n      data-tone=\"success\"\n      role=\"group\"\n      aria-label=\"Verification code\"\n      className={cn(rootBase, className)}\n    >\n      {ctl.chars.map((ch, i) => (\n        <input\n          key={i}\n          {...cellHandlers(ctl, i)}\n          className={cn(\n            cellBase,\n            cellSize[size],\n            boxLook,\n            \"focus-visible:border-success focus-visible:ring-success/40\",\n            ch\n              ? \"border-success bg-[color-mix(in_oklab,var(--color-success)_12%,transparent)]\"\n              : \"border-field-border\"\n          )}\n        />\n      ))}\n    </div>\n  )\n}\n\n/* Invalid: the rejected-code appearance; the cells carry aria-invalid and take the standard danger border and ring styling. The helper text is wired through aria-describedby. */\nexport function InvalidOtp({ className, size = \"md\", length = 6, value, defaultValue, onValueChange }: OtpProps) {\n  const ctl = useOtpCode(length, { value, defaultValue, onValueChange })\n  const msgId = React.useId()\n  return (\n    <div\n      data-slot=\"styled-input-otp\"\n      data-tone=\"danger\"\n      role=\"group\"\n      aria-label=\"Verification code\"\n      className={cn(\"inline-flex flex-col items-start gap-2\", className)}\n    >\n      <div className={rootBase}>\n        {ctl.chars.map((ch, i) => (\n          <input\n            key={i}\n            {...cellHandlers(ctl, i)}\n            aria-invalid=\"true\"\n            aria-describedby={msgId}\n            className={cn(\n              cellBase,\n              cellSize[size],\n              boxLook,\n              invalidLook,\n              ch ? \"bg-[color-mix(in_oklab,var(--color-danger)_10%,transparent)]\" : null\n            )}\n          />\n        ))}\n      </div>\n      <p id={msgId} className=\"text-sm text-danger\">\n        That code is not correct. Please try again.\n      </p>\n    </div>\n  )\n}\n\n/* Loading: once the code is complete a fixed-duration verification simulation runs; the cells lock and a spinner appears. The timeout is cleared on cleanup. */\nexport function LoadingOtp({ className, size = \"md\", length = 6, value, defaultValue, onValueChange }: OtpProps) {\n  const ctl = useOtpCode(length, { value, defaultValue, onValueChange })\n  const [verifying, setVerifying] = React.useState(false)\n  const statusId = React.useId()\n  const complete = ctl.isComplete\n\n  React.useEffect(() => {\n    if (!complete) {\n      setVerifying(false)\n      return\n    }\n    setVerifying(true)\n    const id = window.setTimeout(() => setVerifying(false), 1600)\n    return () => window.clearTimeout(id)\n  }, [complete])\n\n  return (\n    <div\n      data-slot=\"styled-input-otp\"\n      role=\"group\"\n      aria-label=\"Verification code\"\n      aria-describedby={statusId}\n      aria-busy={verifying || undefined}\n      className={cn(rootBase, className)}\n    >\n      {ctl.chars.map((ch, i) => (\n        <input\n          key={i}\n          {...cellHandlers(ctl, i)}\n          disabled={verifying}\n          className={cn(cellBase, cellSize[size], boxLook, ch ? \"border-primary\" : \"border-field-border\")}\n        />\n      ))}\n      <span\n        id={statusId}\n        role=\"status\"\n        aria-live=\"polite\"\n        className={cn(iconBase, \"ml-1 inline-flex items-center gap-2 text-sm text-muted-foreground\")}\n      >\n        {verifying ? (\n          <>\n            <Loader2 className=\"animate-spin\" />\n            Verifying\n          </>\n        ) : null}\n      </span>\n    </div>\n  )\n}\n\n/* Locked: kilitli/disabled kod girisi; hucreler devre disi, yaninda kilit ikonu. */\nexport function LockedOtp({ className, size = \"md\", length = 6, value, defaultValue, onValueChange }: OtpProps) {\n  const ctl = useOtpCode(length, { value, defaultValue, onValueChange })\n  const msgId = React.useId()\n  return (\n    <div\n      data-slot=\"styled-input-otp\"\n      role=\"group\"\n      aria-label=\"Verification code\"\n      aria-describedby={msgId}\n      className={cn(rootBase, className)}\n    >\n      {ctl.chars.map((ch, i) => (\n        <input\n          key={i}\n          {...cellHandlers(ctl, i)}\n          disabled\n          className={cn(cellBase, cellSize[size], boxLook, \"bg-muted\", ch ? \"border-primary\" : \"border-field-border\")}\n        />\n      ))}\n      <span\n        id={msgId}\n        className={cn(iconBase, \"ml-1 inline-flex items-center gap-2 text-sm text-muted-foreground\")}\n      >\n        <Lock />\n        Locked\n      </span>\n    </div>\n  )\n}\n\n/* Complete: tum haneler dolunca hucreler success'e doner ve onay isareti cikar. */\nexport function CompleteOtp({ className, size = \"md\", length = 6, value, defaultValue, onValueChange }: OtpProps) {\n  const ctl = useOtpCode(length, { value, defaultValue, onValueChange })\n  const statusId = React.useId()\n  const done = ctl.isComplete\n  return (\n    <div\n      data-slot=\"styled-input-otp\"\n      data-tone={done ? \"success\" : \"neutral\"}\n      role=\"group\"\n      aria-label=\"Verification code\"\n      aria-describedby={statusId}\n      className={cn(rootBase, className)}\n    >\n      {ctl.chars.map((ch, i) => (\n        <input\n          key={i}\n          {...cellHandlers(ctl, i)}\n          className={cn(\n            cellBase,\n            cellSize[size],\n            boxLook,\n            done\n              ? \"border-success bg-[color-mix(in_oklab,var(--color-success)_12%,transparent)] focus-visible:border-success focus-visible:ring-success/40\"\n              : ch\n                ? \"border-primary\"\n                : \"border-field-border\"\n          )}\n        />\n      ))}\n      <span\n        id={statusId}\n        role=\"status\"\n        aria-live=\"polite\"\n        className={cn(iconBase, \"ml-1 inline-flex items-center gap-2 text-sm text-success\")}\n      >\n        {done ? (\n          <>\n            <Check />\n            Code complete\n          </>\n        ) : null}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/input-otp-state.tsx"
    }
  ],
  "categories": [
    "styled",
    "input"
  ],
  "type": "registry:component"
}