import { useRef, useState, KeyboardEvent, ClipboardEvent } from "react"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; interface OtpInputProps { length?: number; value: string; onChange: (value: string) => void; onComplete?: (value: string) => void; disabled?: boolean; error?: boolean; } export function OtpInput({ length = 6, value, onChange, onComplete, disabled = false, error = false, }: OtpInputProps) { const inputRefs = useRef<(HTMLInputElement | null)[]>([]); const [focusedIndex, setFocusedIndex] = useState(null); const handleChange = (index: number, inputValue: string) => { // Only allow digits const digit = inputValue.replace(/[^0-9]/g, ""); if (digit.length === 0) { // Handle backspace/delete const newValue = value.split(""); newValue[index] = ""; const updatedValue = newValue.join(""); onChange(updatedValue); // Move to previous input if (index > 0) { inputRefs.current[index - 1]?.focus(); } return; } // Update the value at the current index const newValue = value.split(""); newValue[index] = digit[0]; const updatedValue = newValue.join(""); onChange(updatedValue); // Move to next input if not the last one if (index < length - 1) { inputRefs.current[index + 1]?.focus(); } // Check if OTP is complete if (updatedValue.length === length && onComplete) { onComplete(updatedValue); } }; const handleKeyDown = (index: number, e: KeyboardEvent) => { if (e.key === "Backspace" && !value[index] && index > 0) { // If current input is empty and backspace is pressed, move to previous inputRefs.current[index - 1]?.focus(); } else if (e.key === "ArrowLeft" && index > 0) { inputRefs.current[index - 1]?.focus(); } else if (e.key === "ArrowRight" && index < length - 1) { inputRefs.current[index + 1]?.focus(); } }; const handlePaste = (e: ClipboardEvent) => { e.preventDefault(); const pastedData = e.clipboardData.getData("text/plain"); const digits = pastedData.replace(/[^0-9]/g, "").slice(0, length); onChange(digits); // Focus the next empty input or the last input const nextIndex = Math.min(digits.length, length - 1); inputRefs.current[nextIndex]?.focus(); // Check if OTP is complete if (digits.length === length && onComplete) { onComplete(digits); } }; return (
{Array.from({ length }).map((_, index) => ( { inputRefs.current[index] = el; }} type="text" inputMode="numeric" maxLength={1} value={value[index] || ""} onChange={(e) => handleChange(index, e.target.value)} onKeyDown={(e) => handleKeyDown(index, e)} onPaste={handlePaste} onFocus={() => setFocusedIndex(index)} onBlur={() => setFocusedIndex(null)} disabled={disabled} className={cn( "w-12 h-12 text-center text-lg font-semibold", error && "border-destructive focus-visible:ring-destructive", focusedIndex === index && "ring-2 ring-ring", )} aria-label={`Digit ${index + 1}`} /> ))}
); }