{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"input-otp","type":"registry:ui","title":"Input OTP","description":"One-time-code field: slots that pop each digit in, a blinking caret, paste and SMS autofill, a wave on completion and a shake on error.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/input-otp.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { Fragment, useRef, useState, type InputHTMLAttributes, type Ref } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst css = `\n@keyframes ui-otp-pop{0%{opacity:0;scale:.4;translate:0 6px;filter:blur(4px)}60%{opacity:1;scale:1.15;filter:blur(0)}100%{scale:1;translate:0 0}}\n@keyframes ui-otp-blink{0%,45%{opacity:1}55%,100%{opacity:0}}\n@keyframes ui-otp-wave{0%,100%{translate:0 0}40%{translate:0 -6px}}\n@keyframes ui-otp-shake{0%,100%{translate:0}20%{translate:-6px}40%{translate:5px}60%{translate:-3px}80%{translate:2px}}\n.ui-otp-char{animation:ui-otp-pop .32s cubic-bezier(.2,.9,.3,1.2) both}\n.ui-otp-caret{animation:ui-otp-blink 1s steps(1) infinite}\n.ui-otp[data-complete] .ui-otp-slot{animation:ui-otp-wave .45s cubic-bezier(.3,1.4,.5,1) calc(var(--i)*45ms) both}\n.ui-otp[data-invalid]{animation:ui-otp-shake .4s cubic-bezier(.36,.07,.19,.97)}\n@media (prefers-reduced-motion:reduce){.ui-otp-char,.ui-otp[data-complete] .ui-otp-slot,.ui-otp[data-invalid]{animation:none}}\n`;\n\nconst ALLOW = { numeric: /[^0-9]/g, alphanumeric: /[^a-zA-Z0-9]/g };\n\nexport interface InputOTPProps\n  extends Omit<InputHTMLAttributes<HTMLInputElement>, \"value\" | \"defaultValue\" | \"onChange\" | \"maxLength\" | \"size\" | \"pattern\"> {\n  /** Number of slots. */\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Called once every slot is filled. */\n  onComplete?: (value: string) => void;\n  /** Characters accepted; everything else is dropped (also from pasted text). */\n  allow?: \"numeric\" | \"alphanumeric\";\n  /** Draw a separator after every N slots (0 = none). */\n  groupSize?: number;\n  /** Error look + shake. */\n  invalid?: boolean;\n  ref?: Ref<HTMLInputElement>;\n}\n\nexport function InputOTP({\n  length = 6,\n  value: valueProp,\n  defaultValue = \"\",\n  onChange,\n  onComplete,\n  allow = \"numeric\",\n  groupSize = 3,\n  invalid = false,\n  disabled,\n  className,\n  onFocus,\n  onBlur,\n  ref,\n  ...props\n}: InputOTPProps) {\n  const [inner, setInner] = useState(defaultValue);\n  const [focused, setFocused] = useState(false);\n  const input = useRef<HTMLInputElement | null>(null);\n  const value = (valueProp ?? inner).slice(0, length);\n  const complete = value.length === length;\n  const active = focused ? Math.min(value.length, length - 1) : -1;\n\n  const setRefs = (el: HTMLInputElement | null) => {\n    input.current = el;\n    if (typeof ref === \"function\") ref(el);\n    else if (ref) ref.current = el;\n  };\n\n  const change = (raw: string) => {\n    const next = raw.replace(ALLOW[allow], \"\").slice(0, length);\n    if (next === value) return;\n    if (valueProp === undefined) setInner(next);\n    onChange?.(next);\n    if (next.length === length) onComplete?.(next);\n  };\n\n  // One real <input> sits on top of the slots: native typing, paste, autofill\n  // (autocomplete=\"one-time-code\") and backspace for free. The caret is pinned\n  // to the end so slots always fill left to right.\n  const pinCaret = () => {\n    const el = input.current;\n    if (el && document.activeElement === el) el.setSelectionRange(el.value.length, el.value.length);\n  };\n\n  const slots = Array.from({ length }, (_, i) => i);\n\n  return (\n    <div\n      data-complete={complete || undefined}\n      data-invalid={invalid || undefined}\n      className={cn(\"ui-otp relative inline-flex items-center gap-2\", disabled && \"opacity-50\", className)}\n    >\n      <style href=\"ui-input-otp\" precedence=\"default\">\n        {css}\n      </style>\n      {slots.map((i) => {\n        const char = value[i];\n        const isActive = i === active;\n        return (\n          <Fragment key={i}>\n            {groupSize > 0 && i > 0 && i % groupSize === 0 && (\n              <span aria-hidden className=\"h-0.5 w-3 rounded-full bg-muted-foreground/40\" />\n            )}\n            <div\n              aria-hidden\n              style={{ [\"--i\" as string]: i }}\n              className={cn(\n                \"ui-otp-slot relative grid h-12 w-10 place-items-center rounded-lg border border-input bg-background text-lg font-semibold text-foreground shadow-xs sm:h-14 sm:w-12 sm:text-xl\",\n                \"transition-[border-color,box-shadow,scale] duration-200 ease-[cubic-bezier(.3,1.5,.5,1)] motion-reduce:transition-none\",\n                char && \"border-foreground/25\",\n                isActive && \"z-1 scale-105 border-ring shadow-[0_0_0_3px_color-mix(in_oklab,var(--ring)_28%,transparent)]\",\n                complete && !invalid && \"border-primary/70\",\n                invalid && \"border-destructive/80 text-destructive\",\n              )}\n            >\n              {char ? (\n                <span key={char} className=\"ui-otp-char\">\n                  {char}\n                </span>\n              ) : (\n                isActive && <span className=\"ui-otp-caret h-6 w-0.5 rounded-full bg-foreground\" />\n              )}\n            </div>\n          </Fragment>\n        );\n      })}\n      <input\n        aria-label=\"One-time code\"\n        {...props}\n        ref={setRefs}\n        value={value}\n        disabled={disabled}\n        maxLength={length}\n        inputMode={allow === \"numeric\" ? \"numeric\" : \"text\"}\n        autoComplete=\"one-time-code\"\n        autoCapitalize=\"off\"\n        autoCorrect=\"off\"\n        spellCheck={false}\n        aria-invalid={invalid || undefined}\n        onChange={(e) => change(e.target.value)}\n        onFocus={(e) => {\n          setFocused(true);\n          onFocus?.(e);\n          requestAnimationFrame(pinCaret);\n        }}\n        onBlur={(e) => {\n          setFocused(false);\n          onBlur?.(e);\n        }}\n        onSelect={pinCaret}\n        className=\"absolute inset-0 z-10 w-full cursor-text bg-transparent text-base text-transparent caret-transparent outline-none! selection:bg-transparent disabled:cursor-not-allowed\"\n      />\n    </div>\n  );\n}\n"}]}