import { useEffect, useRef, useState } from 'react'; interface Props { value: string; onChange: (next: string) => void; length?: number; autoFocus?: boolean; disabled?: boolean; ariaLabel: string; onSubmit?: () => void; } export function PinInput({ value, onChange, length = 6, autoFocus, disabled, ariaLabel, onSubmit }: Props) { const ref = useRef(null); const [focused, setFocused] = useState(false); useEffect(() => { if (autoFocus) ref.current?.focus(); }, [autoFocus]); // Index of the next empty slot the next keystroke will fill. When the user // has typed all `length` digits, no slot is "active" — the form should // submit instead of pretending one is still focused. const activeIndex = value.length < length ? value.length : -1; return (
ref.current?.focus()}> onChange(e.target.value.replace(/\D/g, '').slice(0, length))} onKeyDown={(e) => { if (e.key === 'Enter' && value.length === length) onSubmit?.(); }} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} className="absolute h-px w-px overflow-hidden p-0 opacity-0" />
{Array.from({ length }).map((_, i) => { const filled = i < value.length; const active = focused && i === activeIndex; let classes = 'flex h-12 w-10 items-center justify-center rounded-lg border text-lg font-semibold transition '; if (filled) { classes += 'border-brand-400 bg-brand-500/10 text-white'; } else if (active) { // Brand-coloured ring + glow so the user immediately sees where // the next keystroke lands. The animated cursor inside reinforces // the "input is alive" feeling. classes += 'border-brand-400 bg-brand-500/10 text-brand-300 ring-2 ring-brand-400/40 shadow-[0_0_12px_-2px] shadow-brand-500/40'; } else { classes += 'border-white/10 bg-ink-800 text-neutral-500'; } return ( {filled ? '•' : active ? | : ''} ); })}
); }