"use client" import * as React from "react" import { cn } from "@/lib/utils" export interface OTPInputProps { length?: number value?: string onChange?: (value: string) => void onComplete?: (value: string) => void type?: "text" | "password" | "number" placeholder?: string disabled?: boolean className?: string } const OTPInput = React.forwardRef( ( { length = 6, value = "", onChange, onComplete, type = "text", placeholder = "○", disabled = false, className, ...props }, ref, ) => { const [values, setValues] = React.useState(Array(length).fill("")) const inputRefs = React.useRef<(HTMLInputElement | null)[]>([]) // Update internal state when value prop changes React.useEffect(() => { const newValues = value.split("").slice(0, length) while (newValues.length < length) { newValues.push("") } setValues(newValues) }, [value, length]) const handleChange = (index: number, inputValue: string) => { // Only allow single character const newValue = inputValue.slice(-1) const newValues = [...values] newValues[index] = newValue setValues(newValues) const fullValue = newValues.join("") onChange?.(fullValue) // Auto-focus next input if (newValue && index < length - 1) { inputRefs.current[index + 1]?.focus() } // Call onComplete when all fields are filled if (fullValue.length === length) { onComplete?.(fullValue) } } const handleKeyDown = (index: number, e: React.KeyboardEvent) => { // Handle backspace if (e.key === "Backspace" && !values[index] && index > 0) { inputRefs.current[index - 1]?.focus() } // Handle arrow keys if (e.key === "ArrowLeft" && index > 0) { inputRefs.current[index - 1]?.focus() } if (e.key === "ArrowRight" && index < length - 1) { inputRefs.current[index + 1]?.focus() } } const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault() const pastedData = e.clipboardData.getData("text").slice(0, length) const newValues = pastedData.split("") while (newValues.length < length) { newValues.push("") } setValues(newValues) onChange?.(pastedData) // Focus the next empty input or the last input const nextEmptyIndex = newValues.findIndex((val) => !val) const focusIndex = nextEmptyIndex === -1 ? length - 1 : nextEmptyIndex inputRefs.current[focusIndex]?.focus() if (pastedData.length === length) { onComplete?.(pastedData) } } return (
{values.map((value, index) => ( (inputRefs.current[index] = el)} type={type === "password" ? "password" : "text"} inputMode={type === "number" ? "numeric" : "text"} pattern={type === "number" ? "[0-9]*" : undefined} value={value} onChange={(e) => handleChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} onPaste={handlePaste} placeholder={placeholder} disabled={disabled} className={cn( "flex h-12 w-12 items-center justify-center border border-input-border input-overlay-bg input-hover-effect shadow-sm text-center text-lg font-medium ring-offset-background", "focus-visible:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", "disabled:cursor-not-allowed disabled:opacity-50", // Remove right border except for last input to avoid double borders index < length - 1 && "border-r-0", // Only round left corners on first input index === 0 && "rounded-l-md", // Only round right corners on last input index === length - 1 && "rounded-r-md", // Remove border radius from middle inputs index > 0 && index < length - 1 && "rounded-none", type === "password" && "text-2xl", )} maxLength={1} autoComplete="one-time-code" /> ))}
) }, ) OTPInput.displayName = "OTPInput" export { OTPInput }