"use client"; import React, { useRef, useEffect } from "react"; import { toEnglishDigits } from "../lib/utils"; interface OtpInput5Props { value: string; onChange: (val: string) => void; disabled?: boolean; autoFocus?: boolean; onComplete?: (code: string) => void; } export default function OtpInput5({ value, onChange, disabled = false, autoFocus = true, onComplete, }: OtpInput5Props) { const inputRefs = useRef<(HTMLInputElement | null)[]>([]); // Array of 5 characters const digits = Array.from({ length: 5 }, (_, i) => value[i] || ""); useEffect(() => { if (autoFocus && inputRefs.current[0]) { const timer = setTimeout(() => { inputRefs.current[0]?.focus({ preventScroll: true }); }, 80); return () => clearTimeout(timer); } }, [autoFocus]); const handleDigitChange = (index: number, char: string) => { const cleanChars = toEnglishDigits(char).replace(/[^0-9]/g, ""); if (!cleanChars) { // Empty / Backspace in controlled input const newDigits = [...digits]; newDigits[index] = ""; const newVal = newDigits.join(""); onChange(newVal); return; } // If multiple chars pasted or typed if (cleanChars.length > 1) { const slice5 = cleanChars.slice(0, 5); onChange(slice5); const nextIndex = Math.min(slice5.length, 4); inputRefs.current[nextIndex]?.focus(); if (slice5.length === 5 && onComplete) { onComplete(slice5); } return; } const single = cleanChars[0]; const newDigits = [...digits]; newDigits[index] = single; const newVal = newDigits.join(""); onChange(newVal); if (index < 4) { inputRefs.current[index + 1]?.focus(); } if (newVal.length === 5 && onComplete) { onComplete(newVal); } }; const handleKeyDown = (index: number, e: React.KeyboardEvent) => { if (e.key === "Backspace") { if (!digits[index] && index > 0) { // Move to previous and clear inputRefs.current[index - 1]?.focus(); const newDigits = [...digits]; newDigits[index - 1] = ""; onChange(newDigits.join("")); } } else if (e.key === "ArrowLeft") { // In LTR inputs, Left goes to previous if (index > 0) { inputRefs.current[index - 1]?.focus(); } } else if (e.key === "ArrowRight") { // In LTR inputs, Right goes to next if (index < 4) { inputRefs.current[index + 1]?.focus(); } } }; const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const pasted = e.clipboardData.getData("text"); const clean = toEnglishDigits(pasted).replace(/[^0-9]/g, "").slice(0, 5); if (clean) { onChange(clean); const targetIndex = Math.min(clean.length, 4); inputRefs.current[targetIndex]?.focus(); if (clean.length === 5 && onComplete) { onComplete(clean); } } }; return (
{[0, 1, 2, 3, 4].map((idx) => { const val = digits[idx] || ""; const isFilled = Boolean(val); return ( { inputRefs.current[idx] = el; }} type="tel" inputMode="numeric" pattern="[0-9]*" // idx === 0: no maxLength so browser OTP autofill can inject the full code. // The handleDigitChange handler distributes multi-char input across all boxes. maxLength={idx === 0 ? undefined : 1} autoComplete={idx === 0 ? "one-time-code" : "off"} value={val} disabled={disabled} onChange={(e) => handleDigitChange(idx, e.target.value)} onKeyDown={(e) => handleKeyDown(idx, e)} onPaste={handlePaste} onFocus={(e) => e.target.select()} className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-xl sm:text-2xl font-black font-mono rounded-2xl border-2 transition-all outline-none shadow-xs ${ isFilled ? "border-canina-blue bg-canina-blue/5 text-canina-blue ring-2 ring-canina-blue/20" : "border-medical-gray-200 bg-medical-gray-50/80 text-medical-gray-900 focus:border-canina-blue focus:bg-white focus:ring-4 focus:ring-canina-blue/15" } ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-text"}`} /> ); })}
); }