{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "title": "Input OTP",
  "description": "A one-time code on short baselines, three and three. Digits drop in as you type; whole, the lines ink in turn. Or the code closes up into one figure, or is read back in words under its digits.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/field.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/input-otp.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useFieldControl } from \"@/components/ui/field\"\n\ntype InputOTPProps = Omit<React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" | \"maxLength\" | \"type\"> & {\n  /** How many digits. */\n  length?: number\n  /** A separator stands after every this many slots (0 for none). Three and three for six. */\n  group?: number\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  /**\n   * How the code waits and ends. line: short baselines, three and three; whole, they ink in turn. close-up: a dot\n   * waits for each digit; whole, the spaces close and the proofreader's close-up mark ties the halves into one figure.\n   * lyric: each digit is read back in words under it, as lyrics sit under their notes, so you can check it aloud.\n   */\n  variant?: \"line\" | \"close-up\" | \"lyric\"\n  /** Called once, when the last slot fills. */\n  onComplete?: (value: string) => void\n  /** Pins a state for documentation (\"focus\"); set on the root. */\n  \"data-force\"?: string\n}\n\nconst WORDS = [\"zero\", \"one\", \"two\", \"three\", \"four\", \"five\", \"six\", \"seven\", \"eight\", \"nine\"]\n\n/**\n * One real input holds the code, so paste, autofill and the keyboard all work natively.\n * The slots only draw it. Whole, the lines ink in turn; wrong (aria-invalid), they turn crimson together.\n */\nfunction InputOTP({\n  length = 6,\n  variant = \"line\",\n  group = length % 2 === 0 ? length / 2 : length % 3 === 0 ? 3 : 0,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onComplete,\n  className,\n  \"data-force\": force,\n  onFocus,\n  onBlur,\n  onPointerUp,\n  ...props\n}: InputOTPProps) {\n  const field = useFieldControl()\n  const [own, setOwn] = React.useState(defaultValue)\n  const [focused, setFocused] = React.useState(false)\n  const code = (value ?? own).slice(0, length)\n  const wrong = props[\"aria-invalid\"] ?? field[\"aria-invalid\"]\n  const invalid = wrong === true || wrong === \"true\"\n  const full = code.length === length\n  const here = (focused || force?.includes(\"focus\")) && !full ? code.length : -1\n\n  const set = (raw: string) => {\n    const next = raw.replace(/\\D/g, \"\").slice(0, length)\n    if (value === undefined) setOwn(next)\n    onValueChange?.(next)\n    if (next.length === length && next !== code) onComplete?.(next)\n  }\n\n  const slots: React.ReactNode[] = []\n  const lyric = variant === \"lyric\"\n  for (let i = 0; i < length; i++) {\n    if (i > 0 && group > 0 && i % group === 0) slots.push(<span key={`sep${i}`} className=\"ot-code-sep\" aria-hidden=\"true\" />)\n    slots.push(\n      <span key={i} data-slot=\"input-otp-slot\" className=\"ot-code-slot\" style={{ \"--i\": i } as React.CSSProperties} data-here={i === here ? \"\" : undefined} aria-hidden=\"true\">\n        {code[i] ? <span key={code[i]}>{code[i]}</span> : null}\n        {lyric && code[i] ? (\n          <span key={`w${code[i]}`} className=\"ot-code-word\">\n            {WORDS[+code[i]]}\n            {i === length - 1 ? \".\" : group > 0 && (i + 1) % group === 0 ? \",\" : null}\n          </span>\n        ) : null}\n      </span>\n    )\n  }\n\n  return (\n    <div data-slot=\"input-otp\" data-variant={variant} data-force={force} data-state={full ? (invalid ? \"wrong\" : \"done\") : undefined} className={cn(\"ot-code\", className)}>\n      <input\n        inputMode=\"numeric\"\n        autoComplete=\"one-time-code\"\n        pattern={`[0-9]{${length}}`}\n        {...props}\n        id={props.id ?? field.id}\n        aria-invalid={props[\"aria-invalid\"] ?? field[\"aria-invalid\"]}\n        aria-describedby={props[\"aria-describedby\"] ?? field[\"aria-describedby\"]}\n        value={code}\n        onChange={(e) => set(e.target.value)}\n        // A pasted code replaces what's there, so a full code fills every slot.\n        onPaste={(e) => {\n          e.preventDefault()\n          set(e.clipboardData.getData(\"text\"))\n        }}\n        onFocus={(e) => {\n          setFocused(true)\n          onFocus?.(e)\n        }}\n        onBlur={(e) => {\n          setFocused(false)\n          onBlur?.(e)\n        }}\n        // Always type at the end, so Backspace takes back the last digit.\n        onPointerUp={(e) => {\n          onPointerUp?.(e)\n          e.currentTarget.setSelectionRange(code.length, code.length)\n        }}\n      />\n      {slots}\n    </div>\n  )\n}\n\nexport { InputOTP, type InputOTPProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/input-otp.css",
      "content": "@layer components {\n  /* ── ot-code ── a one-time code: digits drop onto short baselines, three and three */\n  .ot-code { position: relative; display: inline-flex; align-items: flex-end; gap: 0.18em; font-size: min(calc(var(--ot-f) * var(--ot-expression-scale)), 12.5vw); }\n  .ot-field > .ot-code { grid-column: 1 / -1; justify-self: start; margin-top: var(--ot-space-2); }\n  .ot-code > input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; font-size: 16px; cursor: text; }\n  .ot-code[data-force~=\"focus\"], .ot-code > input:focus-visible { outline: 0; } /* the waiting line is the focus mark */\n  .ot-code-slot {\n    --c: var(--ot-accent);\n    display: grid; place-items: end center; width: 0.78em; height: 1.2em; padding-bottom: 0.1em;\n    font-family: var(--ot-expression); font-style: italic; line-height: 1; color: var(--ot-ink); font-variant-numeric: lining-nums;\n    background:\n      linear-gradient(var(--c), var(--c)) no-repeat 50% 100% / 0% var(--ot-stroke),\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 0 100% / 100% var(--ot-hairline);\n    transition: background-size var(--ot-moderato) var(--ot-exhale);\n  }\n  .ot-code-slot > span { animation: ot-drop-in var(--ot-moderato) var(--ot-spiccato) backwards; }\n  @keyframes ot-drop-in { from { opacity: 0; translate: 0 -0.3em; } }\n  .ot-code-slot[data-here] { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-code-sep { align-self: center; width: var(--ot-hairline); height: 0.7em; margin-inline: 0.12em; background: var(--ot-rule-strong); rotate: 22deg; }\n  .ot-code-sep:is([dir=\"rtl\"], [dir=\"rtl\"] *) { rotate: -22deg; }\n  /* Whole: the lines ink in turn. Wrong: they turn crimson together. */\n  .ot-code[data-state] .ot-code-slot { background-size: 100% var(--ot-stroke), 100% var(--ot-hairline); transition-delay: calc(var(--i, 0) * 2 * var(--ot-arpeggio)); }\n  .ot-code[data-state=\"done\"] .ot-code-slot { --c: var(--ot-ink); }\n  .ot-code[data-state=\"wrong\"] .ot-code-slot { --c: var(--ot-signal); color: var(--ot-signal); transition-delay: 0s; }\n  /* ── close-up ── the proofreader's close-up mark: a dot waits for each digit (the next one in the accent); whole,\n     the spaces close, the halves meet in one tight figure, and the mark's two arcs land over and under the join. */\n  .ot-code[data-variant=\"close-up\"] { gap: 0.1em; transition: gap var(--ot-moderato) var(--ot-breath); }\n  .ot-code[data-variant=\"close-up\"] .ot-code-slot {\n    --c: var(--ot-rule-strong);\n    width: 0.62em; background: radial-gradient(circle closest-side, var(--c) 92%, transparent) no-repeat 50% calc(100% - 0.24em) / var(--ot-dot) var(--ot-dot);\n    transition: width var(--ot-moderato) var(--ot-breath);\n  }\n  .ot-code[data-variant=\"close-up\"] .ot-code-slot[data-here] { --c: var(--ot-accent); }\n  .ot-code[data-variant=\"close-up\"] .ot-code-slot:not(:empty) { background: none; }\n  .ot-code[data-variant=\"close-up\"] .ot-code-sep { position: relative; align-self: stretch; width: 0.3em; height: auto; margin: 0; background: none; rotate: none; transition: width var(--ot-moderato) var(--ot-breath); }\n  .ot-code[data-variant=\"close-up\"] .ot-code-sep::before, .ot-code[data-variant=\"close-up\"] .ot-code-sep::after {\n    content: \"\"; position: absolute; inset-inline-start: 50%; width: 0.36em; height: 0.1em; margin-inline-start: -0.18em;\n    border: var(--ot-stroke) solid var(--ot-ink); scale: 0; opacity: 0;\n    transition: scale var(--ot-moderato) var(--ot-spiccato) var(--ot-moderato), opacity var(--ot-allegro) var(--ot-moderato);\n  }\n  .ot-code[data-variant=\"close-up\"] .ot-code-sep::before { top: 0; translate: 0.1em 0; border-bottom: 0; border-radius: 50% 50% 0 0 / 100% 100% 0 0; } /* the join leans with the italic, so the upper arc sits a little ahead */\n  .ot-code[data-variant=\"close-up\"] .ot-code-sep::after { bottom: 0; border-top: 0; border-radius: 0 0 50% 50% / 0 0 100% 100%; }\n  .ot-code[data-variant=\"close-up\"][data-state=\"done\"] { gap: 0; }\n  .ot-code[data-variant=\"close-up\"][data-state=\"done\"] .ot-code-slot { width: auto; padding-inline: 0.01em 0.05em; } /* the italic leans right, so it keeps a hair on that side */\n  .ot-code[data-variant=\"close-up\"][data-state=\"done\"] .ot-code-sep { width: 0; }\n  .ot-code[data-variant=\"close-up\"][data-state=\"done\"] .ot-code-sep::before, .ot-code[data-variant=\"close-up\"][data-state=\"done\"] .ot-code-sep::after { scale: 1; opacity: 1; }\n\n  /* ── lyric ── each digit read back in words under its line, as lyrics sit under their notes; a comma at the\n     halfway breath and a full stop at the end. The words are ours, upright; the figures yours. */\n  .ot-code[data-variant=\"lyric\"] { margin-bottom: calc(var(--ot-pp) * var(--ot-pp-lh) + var(--ot-space-2)); }\n  .ot-code[data-variant=\"lyric\"] .ot-code-slot { position: relative; }\n  .ot-code[data-variant=\"lyric\"] .ot-code-slot > .ot-code-word {\n    position: absolute; top: calc(100% + var(--ot-space-2)); inset-inline: -1em; text-align: center;\n    font-family: var(--ot-voice); font-style: normal; font-size: var(--ot-pp); line-height: var(--ot-pp-lh); letter-spacing: var(--ot-pp-tr); color: var(--ot-graphite);\n    animation: ot-read-back var(--ot-moderato) var(--ot-exhale) backwards;\n  }\n  @keyframes ot-read-back { from { opacity: 0; clip-path: inset(0 50%); } }\n  .ot-code[data-variant=\"lyric\"][data-state=\"done\"] .ot-code-word { color: var(--ot-ink); }\n  @media (forced-colors: active) {\n    .ot-code { forced-color-adjust: none; color: CanvasText; --ot-ink: CanvasText; --ot-graphite: CanvasText; --ot-pencil: GrayText; --ot-rule-strong: GrayText; --ot-accent: Highlight; --ot-signal: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/input-otp.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/input-otp.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "hook",
    "contract": "ot-code"
  },
  "type": "registry:ui"
}
