'use client'; import clsx from 'clsx'; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { PIN_LENGTH } from '@/libs/crypto/applock'; interface PinInputProps { value: string; onChange: (next: string) => void; autoFocus?: boolean; shake?: boolean; disabled?: boolean; ariaLabel: string; /** * When true, the component keeps focus pinned to the hidden input — * used by the full-screen lock screen so the on-screen keyboard * stays available even after taps elsewhere on the page. */ stickyFocus?: boolean; /** Form-level autocomplete hint. */ autoComplete?: 'one-time-code' | 'current-password' | 'new-password'; } export interface PinInputHandle { focus: () => void; } const PinDot = ({ filled, active }: { filled: boolean; active: boolean }) => (
{active && !filled && (
); const PinInput = forwardRef(function PinInput( { value, onChange, autoFocus, shake, disabled, ariaLabel, stickyFocus, autoComplete = 'one-time-code', }, ref, ) { const inputRef = useRef(null); const [focused, setFocused] = useState(false); useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() })); useEffect(() => { if (autoFocus) { const t = window.setTimeout(() => inputRef.current?.focus(), 0); return () => window.clearTimeout(t); } return undefined; }, [autoFocus]); useEffect(() => { if (!stickyFocus) return; const focus = () => inputRef.current?.focus(); focus(); const t = window.setInterval(focus, 200); return () => window.clearInterval(t); }, [stickyFocus]); const handleClick = () => inputRef.current?.focus(); const handleChange = (e: React.ChangeEvent) => { const next = e.target.value.replace(/\D/g, '').slice(0, PIN_LENGTH); onChange(next); }; return ( ); }); export default PinInput;