import { useId, useRef, type ClipboardEvent, type KeyboardEvent, type ReactNode, } from "react"; import { cn } from "../../lib/cn"; export type OtpInputProps = { disabled?: boolean; label?: ReactNode; length?: number; onChange?: (value: string) => void; obscure?: boolean; obscureCharacter?: string; value?: string; }; export function OtpInput({ disabled = false, label, length = 6, onChange, obscure = false, obscureCharacter = "•", value = "", }: OtpInputProps) { const inputRefs = useRef>([]); const fieldId = useId(); const cells = Array.from({ length }, (_, index) => value[index] ?? ""); const customObscure = obscure && obscureCharacter !== "•"; function focusCell(index: number) { inputRefs.current[Math.max(0, Math.min(length - 1, index))]?.focus(); } function updateCell(index: number, nextValue: string) { const digit = nextValue.replace(/\D/g, "").slice(-1); const next = cells.slice(); next[index] = digit; onChange?.(next.join("")); if (digit && index < length - 1) { focusCell(index + 1); } } function handleKeyDown(index: number, event: KeyboardEvent) { if (event.key === "Backspace" && !cells[index] && index > 0) { event.preventDefault(); const next = cells.slice(); next[index - 1] = ""; onChange?.(next.join("")); focusCell(index - 1); } else if (event.key === "ArrowLeft") { event.preventDefault(); focusCell(index - 1); } else if (event.key === "ArrowRight") { event.preventDefault(); focusCell(index + 1); } } function handlePaste(event: ClipboardEvent) { event.preventDefault(); const pasted = event.clipboardData.getData("text").replace(/\D/g, "").slice(0, length); if (!pasted) { return; } onChange?.(pasted); focusCell(Math.min(pasted.length, length - 1)); } return (
{label ? : null}
{cells.map((cell, index) => ( updateCell(index, event.target.value)} onKeyDown={(event) => handleKeyDown(index, event)} onPaste={handlePaste} ref={(element) => { inputRefs.current[index] = element; }} type={obscure && !customObscure ? "password" : "text"} value={cell} /> {customObscure && cell ? : null} ))}
); }