import { useEffect, useMemo, useRef } from "react" const OTP_LENGTH = 5 const normalizeDigits = (value: string) => value .replace(/[۰-۹]/g, (digit) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(digit))) .replace(/[٠-٩]/g, (digit) => String("٠١٢٣٤٥٦٧٨٩".indexOf(digit))) const sanitizeOtp = (value: string) => normalizeDigits(value).replace(/\D/g, "").slice(0, OTP_LENGTH) export function AuthOtpInput({ id, value, disabled, onChange, onComplete, }: { id: string value: string disabled?: boolean onChange: (value: string) => void onComplete?: (value: string) => void }) { const inputRefs = useRef>([]) const lastCompletedValueRef = useRef("") const normalizedValue = useMemo(() => sanitizeOtp(value), [value]) const digits = useMemo( () => Array.from({ length: OTP_LENGTH }, (_, index) => normalizedValue[index] ?? ""), [normalizedValue], ) useEffect(() => { if (normalizedValue.length !== OTP_LENGTH) { lastCompletedValueRef.current = "" return } if (normalizedValue !== lastCompletedValueRef.current) { lastCompletedValueRef.current = normalizedValue onComplete?.(normalizedValue) } }, [normalizedValue, onComplete]) const focusIndex = (index: number) => { inputRefs.current[index]?.focus() inputRefs.current[index]?.select() } const handleSlotChange = (index: number, nextRawValue: string) => { const nextValue = sanitizeOtp(nextRawValue) if (!nextValue) { const updated = digits.slice() updated[index] = "" onChange(updated.join("")) return } if (nextValue.length > 1) { onChange(nextValue) const focusTarget = Math.min(nextValue.length, OTP_LENGTH - 1) focusIndex(focusTarget) return } const updated = digits.slice() updated[index] = nextValue const combined = updated.join("") onChange(combined) if (index < OTP_LENGTH - 1) { focusIndex(index + 1) } } const handleKeyDown = (index: number, event: React.KeyboardEvent) => { if (event.key === "Backspace" && !digits[index] && index > 0) { const updated = digits.slice() updated[index - 1] = "" onChange(updated.join("")) focusIndex(index - 1) event.preventDefault() return } if (event.key === "ArrowLeft" && index > 0) { focusIndex(index - 1) event.preventDefault() return } if (event.key === "ArrowRight" && index < OTP_LENGTH - 1) { focusIndex(index + 1) event.preventDefault() } } const handlePaste = (event: React.ClipboardEvent) => { event.preventDefault() const pasted = sanitizeOtp(event.clipboardData.getData("text")) if (!pasted) { return } onChange(pasted) focusIndex(Math.min(pasted.length, OTP_LENGTH - 1)) } return (
{digits.map((digit, index) => ( { inputRefs.current[index] = element }} id={index === 0 ? id : undefined} type="text" inputMode="numeric" autoComplete="one-time-code" pattern="[0-9]*" maxLength={OTP_LENGTH} disabled={disabled} value={digit} onChange={(event) => handleSlotChange(index, event.target.value)} onKeyDown={(event) => handleKeyDown(index, event)} onPaste={handlePaste} className="h-12 w-12 rounded-2xl border border-slate-200 bg-white text-center text-lg font-semibold tracking-[0.18em] text-slate-900 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-200 dark:border-slate-700 dark:bg-slate-900 dark:text-white dark:focus:ring-sky-500/20 sm:h-14 sm:w-14" /> ))}
) }