{
  "slug": "otp-input",
  "name": "OTP Input",
  "description": "Ultra-smooth physics-based one-time password input with rolling character tumblers, sliding focus halo, kinetic caret stretching, and celebration choreography.",
  "summary": "A high-performance OTP and pin verification input component powered by motion/react. Features spring-driven vertical rolling character glyphs with blur-to-focus transitions, a kinetic caret that physically stretches during horizontal transit, an animated active slot halo that glides between cells via layoutId, tactile keypress impacts, and orchestrated success and error animations.",
  "category": "inputs",
  "tags": [
    "otp",
    "input",
    "pin",
    "auth",
    "verification",
    "motion",
    "spring",
    "tumbler",
    "caret",
    "framer"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "motion"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-21",
  "updatedDate": "2026-09-21",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Spring vertical roll with entry scale and blur-to-sharp transitions",
    "Kinetic stretching caret with horizontal spring transit and stationary pulse blinking",
    "Sliding active slot focus halo powered by layoutId for buttery-smooth cell transitions",
    "Staggered wave celebration with SVG perimeter path-length draw and ambient bloom",
    "Horizontal 7-keyframe shake on error; the focused slot halo turns red",
    "Full keyboard navigation, backspace slot traversal, SMS autofill, and paste distribution",
    "Four visual variants (default, glass, neon, underlined) with four scale sizes (sm, md, lg, xl)"
  ],
  "anatomy": [
    "<div> (Root container for the OTP input with data-status and variant markers)",
    "<motion.div> (Row container managing layout spacing, error shake, and caret tracking)",
    "<motion.div> (Individual slot cell with hover micro-elevation and layoutId focus highlight)",
    "<input> (Per-slot transparent native input handling mobile autofill and keyboard events)",
    "<motion.svg> (Perimeter SVG border rectangle with animated stroke pathLength on success)",
    "<AnimatePresence> (Handles vertical rolling character entries and exits with blur)",
    "<motion.span> (Floating caret that stretches to 1.4x scaleX while moving between slots)"
  ],
  "physics": {
    "engine": "Motion Spring Physics (motion/react)",
    "description": "Choreographed spring dynamics combining high-stiffness glyph tumblers with kinetic stretching caret transitions and layoutId smooth focus interpolation.",
    "parameters": [
      {
        "label": "Roll Spring Stiffness",
        "value": "520"
      },
      {
        "label": "Roll Spring Damping",
        "value": "30"
      },
      {
        "label": "Caret Spring Stiffness",
        "value": "600"
      },
      {
        "label": "Caret Spring Damping",
        "value": "36"
      },
      {
        "label": "Caret Stretch ScaleX",
        "value": "1.4x during transit"
      },
      {
        "label": "Success Wave Stagger",
        "value": "50ms per cell"
      },
      {
        "label": "Error Shake Keyframes",
        "value": "7-step harmonic decay"
      }
    ]
  },
  "accessibility": {
    "aria": "Each slot contains an input with an aria-label giving its position and the total length. Supports one-time-code autocomplete. The root has no group role; pass role=\"group\" and aria-label through props (they are spread onto the root).",
    "reducedMotion": "Integrates useReducedMotion to disable tumblers, shakes, and stretching transforms while maintaining instantaneous state changes.",
    "keyboard": [
      {
        "key": "ArrowLeft / ArrowRight",
        "description": "Move to the previous or next slot."
      },
      {
        "key": "Backspace",
        "description": "Clear the slot; if it is empty, clear the previous slot and move back."
      },
      {
        "key": "Delete",
        "description": "Clear the current slot."
      },
      {
        "key": "Cmd/Ctrl + V",
        "description": "Paste a code and fill the slots from the focused one."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Two-factor authentication (2FA) verification screens",
      "SMS and email confirmation code entry flows",
      "Transaction confirmation security pin inputs"
    ],
    "bestPractices": [
      "Use type=\"numbers\" (default) for numeric codes; slots automatically use inputMode=\"numeric\"",
      "Pass onComplete to automatically trigger verification when all slots are populated",
      "Provide clear visual status indicators via status='success' or status='error'"
    ]
  },
  "props": [
    {
      "name": "length",
      "type": "number",
      "defaultValue": "6",
      "description": "Number of character slots in the code input."
    },
    {
      "name": "value",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Controlled code string value."
    },
    {
      "name": "defaultValue",
      "type": "string",
      "defaultValue": "''",
      "description": "Initial uncontrolled code string value."
    },
    {
      "name": "onChange",
      "type": "(value: string) => void",
      "defaultValue": "undefined",
      "description": "Callback invoked whenever any slot character changes."
    },
    {
      "name": "onComplete",
      "type": "(value: string) => void",
      "defaultValue": "undefined",
      "description": "Callback invoked when all slots have been filled."
    },
    {
      "name": "type",
      "type": "'numbers' | 'letters' | 'both'",
      "defaultValue": "'numbers'",
      "description": "Character pattern allowed in the input slots."
    },
    {
      "name": "size",
      "type": "'sm' | 'md' | 'lg' | 'xl'",
      "defaultValue": "'md'",
      "description": "Visual scale and box dimension preset."
    },
    {
      "name": "variant",
      "type": "'default' | 'glass' | 'neon' | 'underlined'",
      "defaultValue": "'default'",
      "description": "Visual appearance style variant."
    },
    {
      "name": "status",
      "type": "'idle' | 'success' | 'error' | 'loading'",
      "defaultValue": "'idle'",
      "description": "Current verification state triggering choreographed animations."
    },
    {
      "name": "mask",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Whether to obscure character digits with animated bullet dots."
    },
    {
      "name": "disabled",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Whether the input is disabled."
    },
    {
      "name": "autoFocus",
      "type": "boolean",
      "defaultValue": "false",
      "description": "Whether to automatically focus the first slot on mount."
    },
    {
      "name": "separator",
      "type": "React.ReactNode",
      "defaultValue": "undefined",
      "description": "Custom divider element rendered between slot groups."
    },
    {
      "name": "groupSize",
      "type": "number",
      "defaultValue": "undefined",
      "description": "Number of slots per group when separator is provided."
    },
    {
      "name": "className",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Additional CSS classes for the root container."
    },
    {
      "name": "slotClassName",
      "type": "string",
      "defaultValue": "undefined",
      "description": "Additional CSS classes for each slot cell."
    }
  ],
  "usage": "\"use client\";\n\nimport { OtpInput } from \"@/components/ui/otp-input\";\n\nexport function OtpInputDemo() {\n  return <OtpInput length={6} onComplete={(code) => console.log(code)} />;\n}",
  "files": [
    {
      "name": "otp-input.tsx",
      "path": "registry/ui/otp-input.tsx",
      "code": "\"use client\";\n\nimport React, {\n  useRef,\n  useState,\n  useId,\n  useEffect,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ClipboardEvent,\n  type PointerEvent,\n} from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst PATTERNS = {\n  numbers: /^[0-9]$/,\n  letters: /^[a-zA-Z]$/,\n  both: /^[a-zA-Z0-9]$/,\n} as const;\n\nexport type OtpStatus = \"idle\" | \"success\" | \"error\" | \"loading\";\nexport type OtpSize = \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type OtpVariant = \"default\" | \"glass\" | \"neon\" | \"underlined\";\n\nexport interface OtpInputProps extends Omit<\n  ComponentProps<\"div\">,\n  \"onChange\" | \"value\" | \"defaultValue\"\n> {\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  onComplete?: (value: string) => void;\n  type?: keyof typeof PATTERNS;\n  size?: OtpSize;\n  variant?: OtpVariant;\n  status?: OtpStatus;\n  mask?: boolean;\n  disabled?: boolean;\n  autoFocus?: boolean;\n  separator?: React.ReactNode;\n  groupSize?: number;\n  slotClassName?: string;\n}\n\nconst SIZES = {\n  sm: {\n    box: \"size-10 rounded-lg\",\n    text: \"text-base\",\n    caret: \"h-5\",\n    caretHeight: 20,\n    gap: \"gap-2\",\n    px: 40,\n    radius: 8,\n  },\n  md: {\n    box: \"size-12 rounded-xl\",\n    text: \"text-lg\",\n    caret: \"h-6\",\n    caretHeight: 24,\n    gap: \"gap-2.5\",\n    px: 48,\n    radius: 12,\n  },\n  lg: {\n    box: \"size-14 rounded-2xl\",\n    text: \"text-xl\",\n    caret: \"h-7\",\n    caretHeight: 28,\n    gap: \"gap-3\",\n    px: 56,\n    radius: 16,\n  },\n  xl: {\n    box: \"size-16 rounded-2xl\",\n    text: \"text-2xl font-semibold\",\n    caret: \"h-8\",\n    caretHeight: 32,\n    gap: \"gap-3.5\",\n    px: 64,\n    radius: 16,\n  },\n} as const;\n\nconst SIZE_SPRING = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 28,\n  mass: 0.7,\n} as const;\n\nconst ROLL_SPRING = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 30,\n  mass: 0.75,\n} as const;\n\nconst CARET_SPRING = {\n  type: \"spring\",\n  stiffness: 600,\n  damping: 36,\n  mass: 0.6,\n} as const;\n\nconst SHAKE_KEYFRAMES = [0, -12, 10, -8, 6, -3, 0];\n\nconst toSlots = (code: string, length: number) =>\n  Array.from({ length }, (_, i) => code[i] ?? \"\");\n\nexport function OtpInput({\n  length = 6,\n  value,\n  defaultValue = \"\",\n  onChange,\n  onComplete,\n  type = \"numbers\",\n  size = \"md\",\n  variant = \"default\",\n  status = \"idle\",\n  mask = false,\n  disabled = false,\n  autoFocus = false,\n  separator,\n  groupSize,\n  className,\n  slotClassName,\n  ...props\n}: OtpInputProps) {\n  const instanceId = useId();\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    toSlots(defaultValue, length),\n  );\n  const [clearedSlot, setClearedSlot] = useState<number | null>(null);\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n  const [caretX, setCaretX] = useState<number>(0);\n  const [isCaretMoving, setIsCaretMoving] = useState(false);\n  const [lastPunchedIndex, setLastPunchedIndex] = useState<number | null>(null);\n  const [prevStatus, setPrevStatus] = useState(status);\n  const [isCelebrating, setIsCelebrating] = useState(false);\n\n  if (prevStatus !== status) {\n    setPrevStatus(status);\n    if (status === \"success\") {\n      setIsCelebrating(true);\n    } else {\n      setIsCelebrating(false);\n    }\n  }\n\n  const inputs = useRef<(HTMLInputElement | null)[]>([]);\n  const cells = useRef<(HTMLDivElement | null)[]>([]);\n  const rowRef = useRef<HTMLDivElement | null>(null);\n  const editingAt = useRef<number | null>(null);\n  const caretTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const reduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (isCelebrating) {\n      const timer = setTimeout(\n        () => {\n          setIsCelebrating(false);\n        },\n        500 + length * 50,\n      );\n      return () => clearTimeout(timer);\n    }\n  }, [isCelebrating, length]);\n\n  const slots =\n    value === undefined\n      ? Array.from({ length }, (_, i) => uncontrolled[i] ?? \"\")\n      : toSlots(value, length);\n\n  const numeric = type === \"numbers\";\n  const scale = SIZES[size];\n  const caretVisible =\n    focusedIndex !== null && !slots[focusedIndex] && status !== \"error\";\n\n  const updateCaretPosition = (index: number) => {\n    const cell = cells.current[index];\n    const row = rowRef.current;\n    if (cell && row) {\n      const cellRect = cell.getBoundingClientRect();\n      const rowRect = row.getBoundingClientRect();\n      const targetX =\n        cell.offsetParent === row\n          ? cell.offsetLeft + cell.offsetWidth / 2\n          : cellRect.left - rowRect.left + cellRect.width / 2;\n\n      setIsCaretMoving(true);\n      setCaretX(targetX);\n\n      if (caretTimerRef.current) clearTimeout(caretTimerRef.current);\n      caretTimerRef.current = setTimeout(() => {\n        setIsCaretMoving(false);\n      }, 240);\n    }\n  };\n\n  useEffect(() => {\n    if (focusedIndex !== null) {\n      updateCaretPosition(focusedIndex);\n      const frame = requestAnimationFrame(() => {\n        updateCaretPosition(focusedIndex);\n      });\n      return () => cancelAnimationFrame(frame);\n    }\n  }, [focusedIndex, size, length]);\n\n  const commit = (next: string[]) => {\n    if (value === undefined) setUncontrolled(next);\n    const code = next.join(\"\");\n    onChange?.(code);\n    if (next.every(Boolean)) onComplete?.(code);\n  };\n\n  const focusAt = (index: number) => {\n    const target = Math.min(Math.max(index, 0), length - 1);\n    const input = inputs.current[target];\n    input?.focus();\n    input?.select();\n    setFocusedIndex(target);\n    updateCaretPosition(target);\n  };\n\n  const setCharAt = (index: number, char: string) => {\n    if (!char) {\n      setClearedSlot(index);\n    } else {\n      setClearedSlot(null);\n      setLastPunchedIndex(index);\n      setTimeout(() => setLastPunchedIndex(null), 180);\n    }\n\n    const next = slots.map((slot, i) => (i === index ? char : slot));\n    commit(next);\n  };\n\n  const fill = (startIndex: number, chars: string[]) => {\n    const availableSpace = Math.min(chars.length, length - startIndex);\n    const next = [...slots];\n    chars.slice(0, availableSpace).forEach((char, i) => {\n      next[startIndex + i] = char;\n    });\n    setClearedSlot(null);\n    commit(next);\n    editingAt.current = null;\n    focusAt(startIndex + availableSpace);\n  };\n\n  const handleChange = (index: number, raw: string) => {\n    const filtered = raw.split(\"\").filter((char) => PATTERNS[type].test(char));\n    if (!filtered.length) {\n      if (raw === \"\") {\n        setCharAt(index, \"\");\n      }\n      return;\n    }\n\n    const typedChar =\n      filtered.length === 1\n        ? filtered[0]\n        : filtered.length === 2 && filtered[0] === slots[index]\n          ? filtered[1]\n          : null;\n\n    if (typedChar === null) {\n      fill(index, filtered);\n      return;\n    }\n\n    if (slots.every(Boolean) && editingAt.current !== index) return;\n\n    setCharAt(index, typedChar);\n    editingAt.current = null;\n    focusAt(index + 1);\n  };\n\n  const handleKeyDown = (\n    index: number,\n    event: KeyboardEvent<HTMLInputElement>,\n  ) => {\n    if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      editingAt.current = Math.max(index - 1, 0);\n      focusAt(index - 1);\n      return;\n    }\n\n    if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      editingAt.current = Math.min(index + 1, length - 1);\n      focusAt(index + 1);\n      return;\n    }\n\n    if (event.key === \"Backspace\") {\n      event.preventDefault();\n      if (slots[index]) {\n        setCharAt(index, \"\");\n      } else if (index > 0) {\n        setCharAt(index - 1, \"\");\n        focusAt(index - 1);\n      }\n      return;\n    }\n\n    if (event.key === \"Delete\") {\n      event.preventDefault();\n      setCharAt(index, \"\");\n      return;\n    }\n  };\n\n  const handlePaste = (\n    index: number,\n    event: ClipboardEvent<HTMLInputElement>,\n  ) => {\n    event.preventDefault();\n    const pasted = event.clipboardData\n      .getData(\"text\")\n      .split(\"\")\n      .filter((char) => PATTERNS[type].test(char));\n    if (pasted.length) fill(index, pasted);\n  };\n\n  const handlePointerDown = (\n    index: number,\n    event: PointerEvent<HTMLInputElement>,\n  ) => {\n    const firstEmpty = slots.findIndex((slot) => !slot);\n    const target = firstEmpty === -1 ? index : Math.min(index, firstEmpty);\n    editingAt.current = target;\n    if (target === index) return;\n    event.preventDefault();\n    focusAt(target);\n  };\n\n  const getVariantStyles = (filled: boolean, isFocused: boolean) => {\n    switch (variant) {\n      case \"glass\":\n        return cn(\n          \"bg-black/[0.03] dark:bg-white/3 backdrop-blur-md border border-black/10 dark:border-white/10 shadow-[inset_0_1px_1px_rgba(0,0,0,0.05)] dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]\",\n          filled &&\n            \"border-black/25 bg-black/[0.06] dark:border-white/25 dark:bg-white/8\",\n          isFocused &&\n            \"border-black/40 shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:border-white/40 dark:shadow-[0_0_20px_rgba(255,255,255,0.15)]\",\n        );\n      case \"neon\":\n        return cn(\n          \"bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 shadow-[0_0_12px_rgba(0,0,0,0.05)] dark:shadow-[0_0_12px_rgba(0,0,0,0.5)]\",\n          filled &&\n            \"border-zinc-500 shadow-[0_0_16px_rgba(0,0,0,0.08)] dark:border-zinc-600 dark:shadow-[0_0_16px_rgba(255,255,255,0.06)]\",\n          isFocused &&\n            \"border-zinc-900 shadow-[0_0_24px_rgba(0,0,0,0.15)] dark:border-zinc-300 dark:shadow-[0_0_24px_rgba(255,255,255,0.2)]\",\n        );\n      case \"underlined\":\n        return cn(\n          \"bg-transparent border-b-2 rounded-none! border-zinc-300 dark:border-zinc-700 shadow-none\",\n          filled && \"border-zinc-600 dark:border-zinc-400\",\n          isFocused && \"border-zinc-950 dark:border-white\",\n        );\n      default:\n        return cn(\n          \"bg-zinc-100/90 dark:bg-[#161619] border border-black/10 dark:border-white/10 shadow-[0_2px_8px_rgba(0,0,0,0.04),inset_0_1px_0_rgba(255,255,255,0.8)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.04),inset_0_1px_0_rgba(255,255,255,0.08)]\",\n          filled &&\n            \"border-black/25 dark:border-white/20 bg-zinc-200/60 dark:bg-[#1a1a1e]\",\n          isFocused && \"border-black/40 dark:border-white/35\",\n        );\n    }\n  };\n\n  return (\n    <div\n      data-slot=\"otp-input\"\n      data-status={status}\n      data-variant={variant}\n      role=\"group\"\n      aria-label=\"One-time code\"\n      className={cn(\n        \"relative inline-flex flex-col items-center select-none\",\n        className,\n      )}\n      {...props}\n    >\n      <motion.div\n        ref={rowRef}\n        data-slot=\"otp-input-row\"\n        layout\n        transition={{\n          layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n          duration: 0.42,\n          ease: [0.36, 0.66, 0.04, 1],\n        }}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget as Node)) {\n            setFocusedIndex(null);\n          }\n        }}\n        animate={{\n          x: status === \"error\" && !reduceMotion ? SHAKE_KEYFRAMES : 0,\n        }}\n        className={cn(\"relative flex items-center\", scale.gap)}\n      >\n        {slots.map((slot, index) => {\n          const isFocused = focusedIndex === index;\n          const isFilled = Boolean(slot);\n          const isLastPunched = lastPunchedIndex === index;\n          const showSeparator =\n            separator && groupSize && index > 0 && index % groupSize === 0;\n\n          return (\n            <React.Fragment key={index}>\n              {showSeparator && (\n                <motion.div\n                  layout\n                  transition={{\n                    layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                  }}\n                  aria-hidden=\"true\"\n                  className=\"shrink-0 flex items-center justify-center text-zinc-500 dark:text-zinc-600 px-0.5\"\n                >\n                  {separator}\n                </motion.div>\n              )}\n\n              <motion.div\n                ref={(el) => {\n                  cells.current[index] = el;\n                }}\n                data-slot=\"otp-input-cell\"\n                data-filled={isFilled}\n                data-focused={isFocused}\n                layout\n                transition={{\n                  layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                }}\n                whileHover={\n                  disabled ||\n                  reduceMotion ||\n                  status === \"success\" ||\n                  status === \"error\" ||\n                  isCelebrating\n                    ? undefined\n                    : { y: -1.5, scale: 1.02 }\n                }\n                animate={\n                  isCelebrating && !reduceMotion\n                    ? {\n                        y: [0, -10, 0],\n                        scale: [1, 1.07, 1],\n                        transition: {\n                          duration: 0.5,\n                          delay: index * 0.05,\n                          ease: [0.34, 1.56, 0.64, 1],\n                        },\n                      }\n                    : status === \"loading\" && !reduceMotion\n                      ? {\n                          opacity: [0.5, 1, 0.5],\n                          y: [0, -3, 0],\n                          transition: {\n                            repeat: Infinity,\n                            duration: 1.2,\n                            delay: index * 0.12,\n                            ease: \"easeInOut\",\n                          },\n                        }\n                      : isLastPunched && !reduceMotion\n                        ? {\n                            scale: [1, 1.12, 1],\n                            y: [0, -2, 0],\n                            transition: { duration: 0.22, ease: \"easeOut\" },\n                          }\n                        : isFocused && !reduceMotion && status !== \"success\"\n                          ? {\n                              scale: 1.04,\n                              y: -1.5,\n                              transition: {\n                                type: \"spring\",\n                                stiffness: 450,\n                                damping: 28,\n                              },\n                            }\n                          : {\n                              scale: 1,\n                              y: 0,\n                              transition: {\n                                type: \"spring\",\n                                stiffness: 450,\n                                damping: 28,\n                              },\n                            }\n                }\n                className={cn(\n                  \"relative flex items-center justify-center transition-colors duration-200\",\n                  scale.box,\n                  getVariantStyles(isFilled, isFocused),\n                  slotClassName,\n                )}\n              >\n                <AnimatePresence>\n                  {isFocused && !reduceMotion && status !== \"success\" && (\n                    <motion.div\n                      layoutId={\"otp-active-glow-\" + instanceId}\n                      layout\n                      transition={{\n                        layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                        type: \"spring\",\n                        stiffness: 450,\n                        damping: 34,\n                        mass: 0.6,\n                      }}\n                      className={cn(\n                        \"pointer-events-none absolute -inset-0.5 rounded-[inherit] border-2 border-zinc-950/60 shadow-[0_0_18px_rgba(0,0,0,0.12)] dark:border-white/50 dark:shadow-[0_0_18px_rgba(255,255,255,0.2)] z-20\",\n                        variant === \"underlined\" &&\n                          \"border-0 border-b-2 rounded-none! shadow-[0_4px_12px_rgba(0,0,0,0.15)] dark:shadow-[0_4px_12px_rgba(255,255,255,0.3)] inset-x-0 -bottom-0.5 top-auto h-0.5\",\n                        status === \"error\" &&\n                          \"border-red-500/80 shadow-[0_0_20px_rgba(239,68,68,0.4)]\",\n                      )}\n                    />\n                  )}\n                </AnimatePresence>\n\n                <input\n                  ref={(el) => {\n                    inputs.current[index] = el;\n                  }}\n                  data-slot=\"otp-input-slot\"\n                  data-filled={isFilled}\n                  value={slot}\n                  onChange={(event) => handleChange(index, event.target.value)}\n                  onKeyDown={(event) => handleKeyDown(index, event)}\n                  onPaste={(event) => handlePaste(index, event)}\n                  onPointerDown={(event) => handlePointerDown(index, event)}\n                  onFocus={() => {\n                    setFocusedIndex(index);\n                    updateCaretPosition(index);\n                  }}\n                  type={mask ? \"password\" : \"text\"}\n                  inputMode={numeric ? \"numeric\" : \"text\"}\n                  autoCapitalize={numeric ? undefined : \"characters\"}\n                  autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n                  autoFocus={autoFocus && index === 0}\n                  disabled={disabled}\n                  aria-label={\n                    (numeric ? \"Digit \" : \"Character \") +\n                    (index + 1) +\n                    \" of \" +\n                    length\n                  }\n                  className=\"absolute inset-0 size-full opacity-0 cursor-pointer disabled:cursor-not-allowed z-30\"\n                />\n\n                <AnimatePresence>\n                  {status === \"success\" && (\n                    <motion.svg\n                      aria-hidden=\"true\"\n                      data-slot=\"otp-input-ring\"\n                      viewBox={\"0 0 \" + scale.px + \" \" + scale.px}\n                      exit={{ opacity: 0 }}\n                      transition={{ duration: 0.2 }}\n                      className=\"pointer-events-none absolute inset-0 size-full z-20 overflow-visible\"\n                    >\n                      {variant === \"underlined\" ? (\n                        <motion.line\n                          x1={0}\n                          y1={scale.px - 1}\n                          x2={scale.px}\n                          y2={scale.px - 1}\n                          stroke=\"#10b981\"\n                          strokeWidth={2.5}\n                          strokeLinecap=\"round\"\n                          initial={\n                            reduceMotion ? false : { pathLength: 0, opacity: 0 }\n                          }\n                          animate={{ pathLength: 1, opacity: 1 }}\n                          transition={\n                            reduceMotion\n                              ? { duration: 0 }\n                              : {\n                                  duration: 0.45,\n                                  ease: [0.16, 1, 0.3, 1],\n                                  delay: 0.08 + index * 0.05,\n                                }\n                          }\n                          style={{\n                            filter: \"drop-shadow(0 0 6px rgba(16,185,129,0.7))\",\n                          }}\n                        />\n                      ) : (\n                        <motion.rect\n                          x={1.5}\n                          y={1.5}\n                          width={scale.px - 3}\n                          height={scale.px - 3}\n                          rx={scale.radius - 1}\n                          fill=\"none\"\n                          stroke=\"#10b981\"\n                          strokeWidth={2.5}\n                          strokeLinecap=\"round\"\n                          initial={\n                            reduceMotion ? false : { pathLength: 0, opacity: 0 }\n                          }\n                          animate={{ pathLength: 1, opacity: 1 }}\n                          transition={\n                            reduceMotion\n                              ? { duration: 0 }\n                              : {\n                                  duration: 0.5,\n                                  ease: [0.16, 1, 0.3, 1],\n                                  delay: 0.1 + index * 0.05,\n                                }\n                          }\n                          style={{\n                            filter: \"drop-shadow(0 0 6px rgba(16,185,129,0.7))\",\n                          }}\n                        />\n                      )}\n                    </motion.svg>\n                  )}\n                </AnimatePresence>\n\n                <div className=\"pointer-events-none absolute inset-0 grid place-items-center overflow-hidden z-10\">\n                  <AnimatePresence\n                    mode=\"popLayout\"\n                    initial={false}\n                    custom={clearedSlot === index}\n                  >\n                    {slot && (\n                      <motion.span\n                        key={slot + \"-\" + index}\n                        layout=\"position\"\n                        initial={\n                          reduceMotion\n                            ? { opacity: 0 }\n                            : {\n                                y: 26,\n                                opacity: 0,\n                                scale: 0.65,\n                                filter: \"blur(4px)\",\n                              }\n                        }\n                        animate={{\n                          y: 0,\n                          opacity: 1,\n                          scale: 1,\n                          filter: \"blur(0px)\",\n                        }}\n                        exit={\n                          reduceMotion\n                            ? { opacity: 0 }\n                            : {\n                                y: clearedSlot === index ? 22 : -22,\n                                opacity: 0,\n                                scale: 0.7,\n                                filter: \"blur(3px)\",\n                              }\n                        }\n                        transition={{\n                          layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                          ...(reduceMotion ? { duration: 0.15 } : ROLL_SPRING),\n                        }}\n                        data-slot=\"otp-input-char\"\n                        className={cn(\n                          \"font-mono font-semibold tracking-wider text-black dark:text-white select-none inline-flex items-center justify-center\",\n                          scale.text,\n                        )}\n                      >\n                        {mask ? (\n                          <motion.span\n                            initial={{ scale: 0.4 }}\n                            animate={{ scale: 1 }}\n                            transition={{\n                              type: \"spring\",\n                              stiffness: 600,\n                              damping: 28,\n                            }}\n                            className=\"inline-block size-2.5 rounded-full bg-current shadow-[0_0_8px_currentColor]\"\n                          />\n                        ) : (\n                          slot\n                        )}\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </motion.div>\n            </React.Fragment>\n          );\n        })}\n\n        {caretVisible && (\n          <motion.span\n            aria-hidden=\"true\"\n            data-slot=\"otp-input-caret\"\n            initial={false}\n            animate={{\n              x: caretX - 1,\n              y: \"-50%\",\n              height: scale.caretHeight,\n              scaleX: isCaretMoving && !reduceMotion ? 1.4 : 1,\n              scaleY: isCaretMoving && !reduceMotion ? 0.85 : 1,\n              opacity: isCaretMoving ? 1 : [1, 1, 0, 0],\n            }}\n            transition={{\n              x: reduceMotion ? { duration: 0 } : CARET_SPRING,\n              height: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n              scaleX: { duration: 0.15 },\n              scaleY: { duration: 0.15 },\n              opacity: isCaretMoving\n                ? { duration: 0.05 }\n                : {\n                    duration: 1.05,\n                    times: [0, 0.48, 0.5, 1],\n                    repeat: Infinity,\n                    ease: \"linear\",\n                  },\n            }}\n            className=\"pointer-events-none absolute left-0 top-1/2 w-0.5 rounded-full bg-black dark:bg-white shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:shadow-[0_0_8px_rgba(255,255,255,0.8)] z-30\"\n          />\n        )}\n      </motion.div>\n    </div>\n  );\n}\n\nexport default OtpInput;\n",
      "content": "\"use client\";\n\nimport React, {\n  useRef,\n  useState,\n  useId,\n  useEffect,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ClipboardEvent,\n  type PointerEvent,\n} from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst PATTERNS = {\n  numbers: /^[0-9]$/,\n  letters: /^[a-zA-Z]$/,\n  both: /^[a-zA-Z0-9]$/,\n} as const;\n\nexport type OtpStatus = \"idle\" | \"success\" | \"error\" | \"loading\";\nexport type OtpSize = \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type OtpVariant = \"default\" | \"glass\" | \"neon\" | \"underlined\";\n\nexport interface OtpInputProps extends Omit<\n  ComponentProps<\"div\">,\n  \"onChange\" | \"value\" | \"defaultValue\"\n> {\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  onComplete?: (value: string) => void;\n  type?: keyof typeof PATTERNS;\n  size?: OtpSize;\n  variant?: OtpVariant;\n  status?: OtpStatus;\n  mask?: boolean;\n  disabled?: boolean;\n  autoFocus?: boolean;\n  separator?: React.ReactNode;\n  groupSize?: number;\n  slotClassName?: string;\n}\n\nconst SIZES = {\n  sm: {\n    box: \"size-10 rounded-lg\",\n    text: \"text-base\",\n    caret: \"h-5\",\n    caretHeight: 20,\n    gap: \"gap-2\",\n    px: 40,\n    radius: 8,\n  },\n  md: {\n    box: \"size-12 rounded-xl\",\n    text: \"text-lg\",\n    caret: \"h-6\",\n    caretHeight: 24,\n    gap: \"gap-2.5\",\n    px: 48,\n    radius: 12,\n  },\n  lg: {\n    box: \"size-14 rounded-2xl\",\n    text: \"text-xl\",\n    caret: \"h-7\",\n    caretHeight: 28,\n    gap: \"gap-3\",\n    px: 56,\n    radius: 16,\n  },\n  xl: {\n    box: \"size-16 rounded-2xl\",\n    text: \"text-2xl font-semibold\",\n    caret: \"h-8\",\n    caretHeight: 32,\n    gap: \"gap-3.5\",\n    px: 64,\n    radius: 16,\n  },\n} as const;\n\nconst SIZE_SPRING = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 28,\n  mass: 0.7,\n} as const;\n\nconst ROLL_SPRING = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 30,\n  mass: 0.75,\n} as const;\n\nconst CARET_SPRING = {\n  type: \"spring\",\n  stiffness: 600,\n  damping: 36,\n  mass: 0.6,\n} as const;\n\nconst SHAKE_KEYFRAMES = [0, -12, 10, -8, 6, -3, 0];\n\nconst toSlots = (code: string, length: number) =>\n  Array.from({ length }, (_, i) => code[i] ?? \"\");\n\nexport function OtpInput({\n  length = 6,\n  value,\n  defaultValue = \"\",\n  onChange,\n  onComplete,\n  type = \"numbers\",\n  size = \"md\",\n  variant = \"default\",\n  status = \"idle\",\n  mask = false,\n  disabled = false,\n  autoFocus = false,\n  separator,\n  groupSize,\n  className,\n  slotClassName,\n  ...props\n}: OtpInputProps) {\n  const instanceId = useId();\n  const [uncontrolled, setUncontrolled] = useState(() =>\n    toSlots(defaultValue, length),\n  );\n  const [clearedSlot, setClearedSlot] = useState<number | null>(null);\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n  const [caretX, setCaretX] = useState<number>(0);\n  const [isCaretMoving, setIsCaretMoving] = useState(false);\n  const [lastPunchedIndex, setLastPunchedIndex] = useState<number | null>(null);\n  const [prevStatus, setPrevStatus] = useState(status);\n  const [isCelebrating, setIsCelebrating] = useState(false);\n\n  if (prevStatus !== status) {\n    setPrevStatus(status);\n    if (status === \"success\") {\n      setIsCelebrating(true);\n    } else {\n      setIsCelebrating(false);\n    }\n  }\n\n  const inputs = useRef<(HTMLInputElement | null)[]>([]);\n  const cells = useRef<(HTMLDivElement | null)[]>([]);\n  const rowRef = useRef<HTMLDivElement | null>(null);\n  const editingAt = useRef<number | null>(null);\n  const caretTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const reduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (isCelebrating) {\n      const timer = setTimeout(\n        () => {\n          setIsCelebrating(false);\n        },\n        500 + length * 50,\n      );\n      return () => clearTimeout(timer);\n    }\n  }, [isCelebrating, length]);\n\n  const slots =\n    value === undefined\n      ? Array.from({ length }, (_, i) => uncontrolled[i] ?? \"\")\n      : toSlots(value, length);\n\n  const numeric = type === \"numbers\";\n  const scale = SIZES[size];\n  const caretVisible =\n    focusedIndex !== null && !slots[focusedIndex] && status !== \"error\";\n\n  const updateCaretPosition = (index: number) => {\n    const cell = cells.current[index];\n    const row = rowRef.current;\n    if (cell && row) {\n      const cellRect = cell.getBoundingClientRect();\n      const rowRect = row.getBoundingClientRect();\n      const targetX =\n        cell.offsetParent === row\n          ? cell.offsetLeft + cell.offsetWidth / 2\n          : cellRect.left - rowRect.left + cellRect.width / 2;\n\n      setIsCaretMoving(true);\n      setCaretX(targetX);\n\n      if (caretTimerRef.current) clearTimeout(caretTimerRef.current);\n      caretTimerRef.current = setTimeout(() => {\n        setIsCaretMoving(false);\n      }, 240);\n    }\n  };\n\n  useEffect(() => {\n    if (focusedIndex !== null) {\n      updateCaretPosition(focusedIndex);\n      const frame = requestAnimationFrame(() => {\n        updateCaretPosition(focusedIndex);\n      });\n      return () => cancelAnimationFrame(frame);\n    }\n  }, [focusedIndex, size, length]);\n\n  const commit = (next: string[]) => {\n    if (value === undefined) setUncontrolled(next);\n    const code = next.join(\"\");\n    onChange?.(code);\n    if (next.every(Boolean)) onComplete?.(code);\n  };\n\n  const focusAt = (index: number) => {\n    const target = Math.min(Math.max(index, 0), length - 1);\n    const input = inputs.current[target];\n    input?.focus();\n    input?.select();\n    setFocusedIndex(target);\n    updateCaretPosition(target);\n  };\n\n  const setCharAt = (index: number, char: string) => {\n    if (!char) {\n      setClearedSlot(index);\n    } else {\n      setClearedSlot(null);\n      setLastPunchedIndex(index);\n      setTimeout(() => setLastPunchedIndex(null), 180);\n    }\n\n    const next = slots.map((slot, i) => (i === index ? char : slot));\n    commit(next);\n  };\n\n  const fill = (startIndex: number, chars: string[]) => {\n    const availableSpace = Math.min(chars.length, length - startIndex);\n    const next = [...slots];\n    chars.slice(0, availableSpace).forEach((char, i) => {\n      next[startIndex + i] = char;\n    });\n    setClearedSlot(null);\n    commit(next);\n    editingAt.current = null;\n    focusAt(startIndex + availableSpace);\n  };\n\n  const handleChange = (index: number, raw: string) => {\n    const filtered = raw.split(\"\").filter((char) => PATTERNS[type].test(char));\n    if (!filtered.length) {\n      if (raw === \"\") {\n        setCharAt(index, \"\");\n      }\n      return;\n    }\n\n    const typedChar =\n      filtered.length === 1\n        ? filtered[0]\n        : filtered.length === 2 && filtered[0] === slots[index]\n          ? filtered[1]\n          : null;\n\n    if (typedChar === null) {\n      fill(index, filtered);\n      return;\n    }\n\n    if (slots.every(Boolean) && editingAt.current !== index) return;\n\n    setCharAt(index, typedChar);\n    editingAt.current = null;\n    focusAt(index + 1);\n  };\n\n  const handleKeyDown = (\n    index: number,\n    event: KeyboardEvent<HTMLInputElement>,\n  ) => {\n    if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      editingAt.current = Math.max(index - 1, 0);\n      focusAt(index - 1);\n      return;\n    }\n\n    if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      editingAt.current = Math.min(index + 1, length - 1);\n      focusAt(index + 1);\n      return;\n    }\n\n    if (event.key === \"Backspace\") {\n      event.preventDefault();\n      if (slots[index]) {\n        setCharAt(index, \"\");\n      } else if (index > 0) {\n        setCharAt(index - 1, \"\");\n        focusAt(index - 1);\n      }\n      return;\n    }\n\n    if (event.key === \"Delete\") {\n      event.preventDefault();\n      setCharAt(index, \"\");\n      return;\n    }\n  };\n\n  const handlePaste = (\n    index: number,\n    event: ClipboardEvent<HTMLInputElement>,\n  ) => {\n    event.preventDefault();\n    const pasted = event.clipboardData\n      .getData(\"text\")\n      .split(\"\")\n      .filter((char) => PATTERNS[type].test(char));\n    if (pasted.length) fill(index, pasted);\n  };\n\n  const handlePointerDown = (\n    index: number,\n    event: PointerEvent<HTMLInputElement>,\n  ) => {\n    const firstEmpty = slots.findIndex((slot) => !slot);\n    const target = firstEmpty === -1 ? index : Math.min(index, firstEmpty);\n    editingAt.current = target;\n    if (target === index) return;\n    event.preventDefault();\n    focusAt(target);\n  };\n\n  const getVariantStyles = (filled: boolean, isFocused: boolean) => {\n    switch (variant) {\n      case \"glass\":\n        return cn(\n          \"bg-black/[0.03] dark:bg-white/3 backdrop-blur-md border border-black/10 dark:border-white/10 shadow-[inset_0_1px_1px_rgba(0,0,0,0.05)] dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.1)]\",\n          filled &&\n            \"border-black/25 bg-black/[0.06] dark:border-white/25 dark:bg-white/8\",\n          isFocused &&\n            \"border-black/40 shadow-[0_0_20px_rgba(0,0,0,0.1)] dark:border-white/40 dark:shadow-[0_0_20px_rgba(255,255,255,0.15)]\",\n        );\n      case \"neon\":\n        return cn(\n          \"bg-zinc-100 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 shadow-[0_0_12px_rgba(0,0,0,0.05)] dark:shadow-[0_0_12px_rgba(0,0,0,0.5)]\",\n          filled &&\n            \"border-zinc-500 shadow-[0_0_16px_rgba(0,0,0,0.08)] dark:border-zinc-600 dark:shadow-[0_0_16px_rgba(255,255,255,0.06)]\",\n          isFocused &&\n            \"border-zinc-900 shadow-[0_0_24px_rgba(0,0,0,0.15)] dark:border-zinc-300 dark:shadow-[0_0_24px_rgba(255,255,255,0.2)]\",\n        );\n      case \"underlined\":\n        return cn(\n          \"bg-transparent border-b-2 rounded-none! border-zinc-300 dark:border-zinc-700 shadow-none\",\n          filled && \"border-zinc-600 dark:border-zinc-400\",\n          isFocused && \"border-zinc-950 dark:border-white\",\n        );\n      default:\n        return cn(\n          \"bg-zinc-100/90 dark:bg-[#161619] border border-black/10 dark:border-white/10 shadow-[0_2px_8px_rgba(0,0,0,0.04),inset_0_1px_0_rgba(255,255,255,0.8)] dark:shadow-[0_2px_8px_rgba(0,0,0,0.04),inset_0_1px_0_rgba(255,255,255,0.08)]\",\n          filled &&\n            \"border-black/25 dark:border-white/20 bg-zinc-200/60 dark:bg-[#1a1a1e]\",\n          isFocused && \"border-black/40 dark:border-white/35\",\n        );\n    }\n  };\n\n  return (\n    <div\n      data-slot=\"otp-input\"\n      data-status={status}\n      data-variant={variant}\n      role=\"group\"\n      aria-label=\"One-time code\"\n      className={cn(\n        \"relative inline-flex flex-col items-center select-none\",\n        className,\n      )}\n      {...props}\n    >\n      <motion.div\n        ref={rowRef}\n        data-slot=\"otp-input-row\"\n        layout\n        transition={{\n          layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n          duration: 0.42,\n          ease: [0.36, 0.66, 0.04, 1],\n        }}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget as Node)) {\n            setFocusedIndex(null);\n          }\n        }}\n        animate={{\n          x: status === \"error\" && !reduceMotion ? SHAKE_KEYFRAMES : 0,\n        }}\n        className={cn(\"relative flex items-center\", scale.gap)}\n      >\n        {slots.map((slot, index) => {\n          const isFocused = focusedIndex === index;\n          const isFilled = Boolean(slot);\n          const isLastPunched = lastPunchedIndex === index;\n          const showSeparator =\n            separator && groupSize && index > 0 && index % groupSize === 0;\n\n          return (\n            <React.Fragment key={index}>\n              {showSeparator && (\n                <motion.div\n                  layout\n                  transition={{\n                    layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                  }}\n                  aria-hidden=\"true\"\n                  className=\"shrink-0 flex items-center justify-center text-zinc-500 dark:text-zinc-600 px-0.5\"\n                >\n                  {separator}\n                </motion.div>\n              )}\n\n              <motion.div\n                ref={(el) => {\n                  cells.current[index] = el;\n                }}\n                data-slot=\"otp-input-cell\"\n                data-filled={isFilled}\n                data-focused={isFocused}\n                layout\n                transition={{\n                  layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                }}\n                whileHover={\n                  disabled ||\n                  reduceMotion ||\n                  status === \"success\" ||\n                  status === \"error\" ||\n                  isCelebrating\n                    ? undefined\n                    : { y: -1.5, scale: 1.02 }\n                }\n                animate={\n                  isCelebrating && !reduceMotion\n                    ? {\n                        y: [0, -10, 0],\n                        scale: [1, 1.07, 1],\n                        transition: {\n                          duration: 0.5,\n                          delay: index * 0.05,\n                          ease: [0.34, 1.56, 0.64, 1],\n                        },\n                      }\n                    : status === \"loading\" && !reduceMotion\n                      ? {\n                          opacity: [0.5, 1, 0.5],\n                          y: [0, -3, 0],\n                          transition: {\n                            repeat: Infinity,\n                            duration: 1.2,\n                            delay: index * 0.12,\n                            ease: \"easeInOut\",\n                          },\n                        }\n                      : isLastPunched && !reduceMotion\n                        ? {\n                            scale: [1, 1.12, 1],\n                            y: [0, -2, 0],\n                            transition: { duration: 0.22, ease: \"easeOut\" },\n                          }\n                        : isFocused && !reduceMotion && status !== \"success\"\n                          ? {\n                              scale: 1.04,\n                              y: -1.5,\n                              transition: {\n                                type: \"spring\",\n                                stiffness: 450,\n                                damping: 28,\n                              },\n                            }\n                          : {\n                              scale: 1,\n                              y: 0,\n                              transition: {\n                                type: \"spring\",\n                                stiffness: 450,\n                                damping: 28,\n                              },\n                            }\n                }\n                className={cn(\n                  \"relative flex items-center justify-center transition-colors duration-200\",\n                  scale.box,\n                  getVariantStyles(isFilled, isFocused),\n                  slotClassName,\n                )}\n              >\n                <AnimatePresence>\n                  {isFocused && !reduceMotion && status !== \"success\" && (\n                    <motion.div\n                      layoutId={\"otp-active-glow-\" + instanceId}\n                      layout\n                      transition={{\n                        layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                        type: \"spring\",\n                        stiffness: 450,\n                        damping: 34,\n                        mass: 0.6,\n                      }}\n                      className={cn(\n                        \"pointer-events-none absolute -inset-0.5 rounded-[inherit] border-2 border-zinc-950/60 shadow-[0_0_18px_rgba(0,0,0,0.12)] dark:border-white/50 dark:shadow-[0_0_18px_rgba(255,255,255,0.2)] z-20\",\n                        variant === \"underlined\" &&\n                          \"border-0 border-b-2 rounded-none! shadow-[0_4px_12px_rgba(0,0,0,0.15)] dark:shadow-[0_4px_12px_rgba(255,255,255,0.3)] inset-x-0 -bottom-0.5 top-auto h-0.5\",\n                        status === \"error\" &&\n                          \"border-red-500/80 shadow-[0_0_20px_rgba(239,68,68,0.4)]\",\n                      )}\n                    />\n                  )}\n                </AnimatePresence>\n\n                <input\n                  ref={(el) => {\n                    inputs.current[index] = el;\n                  }}\n                  data-slot=\"otp-input-slot\"\n                  data-filled={isFilled}\n                  value={slot}\n                  onChange={(event) => handleChange(index, event.target.value)}\n                  onKeyDown={(event) => handleKeyDown(index, event)}\n                  onPaste={(event) => handlePaste(index, event)}\n                  onPointerDown={(event) => handlePointerDown(index, event)}\n                  onFocus={() => {\n                    setFocusedIndex(index);\n                    updateCaretPosition(index);\n                  }}\n                  type={mask ? \"password\" : \"text\"}\n                  inputMode={numeric ? \"numeric\" : \"text\"}\n                  autoCapitalize={numeric ? undefined : \"characters\"}\n                  autoComplete={index === 0 ? \"one-time-code\" : \"off\"}\n                  autoFocus={autoFocus && index === 0}\n                  disabled={disabled}\n                  aria-label={\n                    (numeric ? \"Digit \" : \"Character \") +\n                    (index + 1) +\n                    \" of \" +\n                    length\n                  }\n                  className=\"absolute inset-0 size-full opacity-0 cursor-pointer disabled:cursor-not-allowed z-30\"\n                />\n\n                <AnimatePresence>\n                  {status === \"success\" && (\n                    <motion.svg\n                      aria-hidden=\"true\"\n                      data-slot=\"otp-input-ring\"\n                      viewBox={\"0 0 \" + scale.px + \" \" + scale.px}\n                      exit={{ opacity: 0 }}\n                      transition={{ duration: 0.2 }}\n                      className=\"pointer-events-none absolute inset-0 size-full z-20 overflow-visible\"\n                    >\n                      {variant === \"underlined\" ? (\n                        <motion.line\n                          x1={0}\n                          y1={scale.px - 1}\n                          x2={scale.px}\n                          y2={scale.px - 1}\n                          stroke=\"#10b981\"\n                          strokeWidth={2.5}\n                          strokeLinecap=\"round\"\n                          initial={\n                            reduceMotion ? false : { pathLength: 0, opacity: 0 }\n                          }\n                          animate={{ pathLength: 1, opacity: 1 }}\n                          transition={\n                            reduceMotion\n                              ? { duration: 0 }\n                              : {\n                                  duration: 0.45,\n                                  ease: [0.16, 1, 0.3, 1],\n                                  delay: 0.08 + index * 0.05,\n                                }\n                          }\n                          style={{\n                            filter: \"drop-shadow(0 0 6px rgba(16,185,129,0.7))\",\n                          }}\n                        />\n                      ) : (\n                        <motion.rect\n                          x={1.5}\n                          y={1.5}\n                          width={scale.px - 3}\n                          height={scale.px - 3}\n                          rx={scale.radius - 1}\n                          fill=\"none\"\n                          stroke=\"#10b981\"\n                          strokeWidth={2.5}\n                          strokeLinecap=\"round\"\n                          initial={\n                            reduceMotion ? false : { pathLength: 0, opacity: 0 }\n                          }\n                          animate={{ pathLength: 1, opacity: 1 }}\n                          transition={\n                            reduceMotion\n                              ? { duration: 0 }\n                              : {\n                                  duration: 0.5,\n                                  ease: [0.16, 1, 0.3, 1],\n                                  delay: 0.1 + index * 0.05,\n                                }\n                          }\n                          style={{\n                            filter: \"drop-shadow(0 0 6px rgba(16,185,129,0.7))\",\n                          }}\n                        />\n                      )}\n                    </motion.svg>\n                  )}\n                </AnimatePresence>\n\n                <div className=\"pointer-events-none absolute inset-0 grid place-items-center overflow-hidden z-10\">\n                  <AnimatePresence\n                    mode=\"popLayout\"\n                    initial={false}\n                    custom={clearedSlot === index}\n                  >\n                    {slot && (\n                      <motion.span\n                        key={slot + \"-\" + index}\n                        layout=\"position\"\n                        initial={\n                          reduceMotion\n                            ? { opacity: 0 }\n                            : {\n                                y: 26,\n                                opacity: 0,\n                                scale: 0.65,\n                                filter: \"blur(4px)\",\n                              }\n                        }\n                        animate={{\n                          y: 0,\n                          opacity: 1,\n                          scale: 1,\n                          filter: \"blur(0px)\",\n                        }}\n                        exit={\n                          reduceMotion\n                            ? { opacity: 0 }\n                            : {\n                                y: clearedSlot === index ? 22 : -22,\n                                opacity: 0,\n                                scale: 0.7,\n                                filter: \"blur(3px)\",\n                              }\n                        }\n                        transition={{\n                          layout: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n                          ...(reduceMotion ? { duration: 0.15 } : ROLL_SPRING),\n                        }}\n                        data-slot=\"otp-input-char\"\n                        className={cn(\n                          \"font-mono font-semibold tracking-wider text-black dark:text-white select-none inline-flex items-center justify-center\",\n                          scale.text,\n                        )}\n                      >\n                        {mask ? (\n                          <motion.span\n                            initial={{ scale: 0.4 }}\n                            animate={{ scale: 1 }}\n                            transition={{\n                              type: \"spring\",\n                              stiffness: 600,\n                              damping: 28,\n                            }}\n                            className=\"inline-block size-2.5 rounded-full bg-current shadow-[0_0_8px_currentColor]\"\n                          />\n                        ) : (\n                          slot\n                        )}\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </motion.div>\n            </React.Fragment>\n          );\n        })}\n\n        {caretVisible && (\n          <motion.span\n            aria-hidden=\"true\"\n            data-slot=\"otp-input-caret\"\n            initial={false}\n            animate={{\n              x: caretX - 1,\n              y: \"-50%\",\n              height: scale.caretHeight,\n              scaleX: isCaretMoving && !reduceMotion ? 1.4 : 1,\n              scaleY: isCaretMoving && !reduceMotion ? 0.85 : 1,\n              opacity: isCaretMoving ? 1 : [1, 1, 0, 0],\n            }}\n            transition={{\n              x: reduceMotion ? { duration: 0 } : CARET_SPRING,\n              height: reduceMotion ? { duration: 0 } : SIZE_SPRING,\n              scaleX: { duration: 0.15 },\n              scaleY: { duration: 0.15 },\n              opacity: isCaretMoving\n                ? { duration: 0.05 }\n                : {\n                    duration: 1.05,\n                    times: [0, 0.48, 0.5, 1],\n                    repeat: Infinity,\n                    ease: \"linear\",\n                  },\n            }}\n            className=\"pointer-events-none absolute left-0 top-1/2 w-0.5 rounded-full bg-black dark:bg-white shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:shadow-[0_0_8px_rgba(255,255,255,0.8)] z-30\"\n          />\n        )}\n      </motion.div>\n    </div>\n  );\n}\n\nexport default OtpInput;\n",
      "type": "registry:ui",
      "target": "@ui/otp-input.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "OTP Input"
}
