{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-otp",
  "type": "registry:ui",
  "title": "Input Otp",
  "description": "A flexible Accessible one-time password component.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/input-otp.tsx",
      "content": "'use client';\nimport React, {\n  useRef,\n  useState,\n  useEffect,\n  createContext,\n  useContext,\n} from 'react';\nimport { motion } from 'motion/react';\nimport { MinusIcon } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\ninterface InputOtpContextType {\n  values: string[];\n  visibleValues: string[];\n  handleChange: (val: string, idx: number) => void;\n  handleKeyDown: (e: React.KeyboardEvent, idx: number) => void;\n  handleFocus: (idx: number) => void;\n  handleBlur: (idx: number) => void;\n  handlePaste: (e: React.ClipboardEvent, idx: number) => void;\n  inputsRef: React.MutableRefObject<(HTMLInputElement | null)[]>;\n  mask: boolean;\n  maskSymbol: string;\n  inputClassName?: string;\n}\n\nconst InputOtpContext = createContext<InputOtpContextType | null>(null);\n\ninterface InputOTPProps {\n  maxLength?: number;\n  onComplete?: (value: string) => void;\n  className?: string;\n  containerClassName?: string;\n  inputClassName?: string;\n  mask?: boolean;\n  maskSymbol?: string;\n  maskDelay?: number;\n  children: React.ReactNode;\n}\n\nfunction InputOTP({\n  maxLength = 6,\n  onComplete,\n  className,\n  containerClassName,\n  inputClassName,\n  mask = false,\n  maskSymbol = '*',\n  maskDelay = 800,\n  children,\n}: InputOTPProps) {\n  const [values, setValues] = useState(Array(maxLength).fill(''));\n  const [visibleValues, setVisibleValues] = useState(Array(maxLength).fill(''));\n  const inputsRef = useRef<(HTMLInputElement | null)[]>([]);\n  const timeoutsRef = useRef<(NodeJS.Timeout | null)[]>(\n    Array(maxLength).fill(null),\n  );\n\n  const clearTimeoutForIndex = (idx: number) => {\n    if (timeoutsRef.current[idx]) {\n      clearTimeout(timeoutsRef.current[idx]!);\n      timeoutsRef.current[idx] = null;\n    }\n  };\n\n  const applyMaskWithDelay = (idx: number, currentValue: string) => {\n    clearTimeoutForIndex(idx);\n    if (mask && currentValue) {\n      timeoutsRef.current[idx] = setTimeout(() => {\n        setVisibleValues((prev) => {\n          const updated = [...prev];\n          if (updated[idx] !== maskSymbol) {\n            updated[idx] = maskSymbol;\n          }\n          return updated;\n        });\n      }, maskDelay);\n    }\n  };\n\n  const handleChange = (val: string, idx: number) => {\n    if (val.length > 1) return;\n\n    clearTimeoutForIndex(idx);\n\n    const newValues = [...values];\n    const newVisibleValues = [...visibleValues];\n    newValues[idx] = val;\n    newVisibleValues[idx] = val;\n    setValues(newValues);\n    setVisibleValues(newVisibleValues);\n\n    if (mask && val) {\n      applyMaskWithDelay(idx, val);\n    }\n\n    if (val && idx < maxLength - 1) {\n      inputsRef.current[idx + 1]?.focus();\n    }\n\n    if (newValues.every((v) => v)) {\n      onComplete?.(newValues.join(''));\n    }\n  };\n\n  const handlePaste = (e: React.ClipboardEvent, startIdx: number) => {\n    e.preventDefault();\n    const pastedText = e.clipboardData.getData('text');\n\n    if (!pastedText) return;\n\n    const newValues = [...values];\n    const newVisibleValues = [...visibleValues];\n\n    timeoutsRef.current.forEach((timeout, i) => {\n      if (timeout) {\n        clearTimeout(timeout);\n        timeoutsRef.current[i] = null;\n      }\n    });\n\n    for (let i = 0; i < pastedText.length && startIdx + i < maxLength; i++) {\n      const char = pastedText[i];\n      newValues[startIdx + i] = char;\n      newVisibleValues[startIdx + i] = char;\n    }\n\n    setValues(newValues);\n    setVisibleValues(newVisibleValues);\n\n    if (mask) {\n      for (let i = 0; i < pastedText.length && startIdx + i < maxLength; i++) {\n        const idx = startIdx + i;\n        if (newValues[idx]) {\n          timeoutsRef.current[idx] = setTimeout(() => {\n            setVisibleValues((prev) => {\n              const updated = [...prev];\n              if (updated[idx] !== maskSymbol) {\n                updated[idx] = maskSymbol;\n              }\n              return updated;\n            });\n          }, maskDelay);\n        }\n      }\n    }\n\n    const nextEmptyIndex = newValues.findIndex((v, i) => i > startIdx && !v);\n    const focusIndex =\n      nextEmptyIndex !== -1\n        ? nextEmptyIndex\n        : Math.min(startIdx + pastedText.length, maxLength - 1);\n\n    setTimeout(() => {\n      inputsRef.current[focusIndex]?.focus();\n    }, 0);\n\n    if (newValues.every((v) => v)) {\n      onComplete?.(newValues.join(''));\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {\n    if (e.key === 'Backspace') {\n      clearTimeoutForIndex(idx);\n      if (!values[idx] && idx > 0) {\n        inputsRef.current[idx - 1]?.focus();\n      } else {\n        const newValues = [...values];\n        const newVisibleValues = [...visibleValues];\n        newValues[idx] = '';\n        newVisibleValues[idx] = '';\n        setValues(newValues);\n        setVisibleValues(newVisibleValues);\n      }\n    }\n  };\n\n  const handleFocus = (idx: number) => {\n    if (mask && values[idx]) {\n      clearTimeoutForIndex(idx);\n      setVisibleValues((prev) => {\n        const updated = [...prev];\n        updated[idx] = values[idx];\n        return updated;\n      });\n      applyMaskWithDelay(idx, values[idx]);\n    }\n  };\n\n  const handleBlur = (idx: number) => {\n    if (mask && values[idx]) {\n      clearTimeoutForIndex(idx);\n      setVisibleValues((prev) => {\n        const updated = [...prev];\n        updated[idx] = maskSymbol;\n        return updated;\n      });\n    }\n  };\n\n  useEffect(() => {\n    const currentTimeouts = timeoutsRef.current;\n\n    return () => {\n      currentTimeouts.forEach((timeout) => {\n        if (timeout) clearTimeout(timeout);\n      });\n    };\n  }, []);\n\n  const contextValue: InputOtpContextType = {\n    values,\n    visibleValues,\n    handleChange,\n    handleKeyDown,\n    handleFocus,\n    handleBlur,\n    handlePaste,\n    inputsRef,\n    mask,\n    maskSymbol,\n    inputClassName,\n  };\n\n  return (\n    <InputOtpContext.Provider value={contextValue}>\n      <div\n        data-slot='input-otp'\n        className={cn(\n          'flex items-center gap-1 sm:gap-2 has-disabled:opacity-50',\n          containerClassName,\n        )}\n      >\n        <div className={cn('flex items-center gap-1 sm:gap-2', className)}>\n          {children}\n        </div>\n      </div>\n    </InputOtpContext.Provider>\n  );\n}\n\nfunction InputOTPGroup({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<'div'>) {\n  return (\n    <div\n      data-slot='input-otp-group'\n      className={cn(\n        'flex items-center gap-1 sm:gap-2 px-1 py-0.5 rounded-md sm:rounded-lg',\n        'bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction InputOTPSlot({\n  index,\n  className,\n  ...props\n}: Omit<React.ComponentProps<'div'>, 'children'> & {\n  index: number;\n}) {\n  const context = useContext(InputOtpContext);\n\n  if (!context) {\n    throw new Error('InputOTPSlot must be used within InputOTP');\n  }\n\n  const {\n    visibleValues,\n    handleChange,\n    handleKeyDown,\n    handleFocus,\n    handleBlur,\n    handlePaste,\n    inputsRef,\n    mask,\n    maskSymbol,\n    inputClassName,\n  } = context;\n\n  return (\n    <motion.div\n      className='relative'\n      initial={{ scale: 1 }}\n      whileFocus={{ scale: 1.05 }}\n      whileHover={{ scale: 1.02 }}\n    >\n      <input\n        ref={(el) => {\n          inputsRef.current[index] = el;\n        }}\n        type='text'\n        inputMode='text'\n        maxLength={1}\n        value={visibleValues[index]}\n        onChange={(e) => handleChange(e.target.value, index)}\n        onKeyDown={(e) => handleKeyDown(e, index)}\n        onFocus={() => handleFocus(index)}\n        onBlur={() => handleBlur(index)}\n        onPaste={(e) => handlePaste(e, index)}\n        className={cn(\n          'w-8 h-10 sm:w-10 sm:h-12 md:w-12 md:h-14',\n          'rounded-lg sm:rounded-xl text-center font-semibold outline-hidden transition-all duration-200',\n          'border border-transparent bg-white/60 dark:bg-white/10 shadow-inner',\n          'focus:ring-2 focus:ring-primary/70 dark:focus:ring-primary/40 focus:border-primary/30',\n          'backdrop-blur-md text-black dark:text-white placeholder-transparent',\n          visibleValues[index] === maskSymbol\n            ? 'text-lg sm:text-xl md:text-2xl'\n            : 'text-sm sm:text-base md:text-lg',\n          'font-mono',\n          inputClassName,\n          className,\n        )}\n      />\n      <motion.div\n        layoutId={`glow-${index}`}\n        className='absolute inset-0 rounded-lg sm:rounded-xl pointer-events-none'\n        style={{ boxShadow: '0 0 4px 1px rgba(0,0,0,0.06)' }}\n      />\n    </motion.div>\n  );\n}\n\nfunction InputOTPSeparator({\n  separatorSymbol,\n  className,\n  ...props\n}: React.ComponentProps<'div'> & {\n  separatorSymbol?: React.ReactNode;\n}) {\n  return (\n    <div\n      data-slot='input-otp-separator'\n      role='separator'\n      className={cn('flex items-center justify-center', className)}\n      {...props}\n    >\n      {separatorSymbol || <MinusIcon className='w-3 h-3 sm:w-4 sm:h-4' />}\n    </div>\n  );\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
