import { useEffect, useRef } from 'react' import { normalizeFullWidthDigits } from '../../utils/stringUtils.js' // Delay before accepting a digit as a response, to prevent accidental // submissions when users start messages with numbers (e.g., numbered lists). // Short enough to feel instant for intentional presses, long enough to // cancel when the user types more characters. const DEFAULT_DEBOUNCE_MS = 400 /** * Detects when the user types a single valid digit into the prompt input, * debounces to avoid accidental submissions (e.g., "1. First item"), * trims the digit from the input, and fires a callback. * * Used by survey components that accept numeric responses typed directly * into the main prompt input. */ export function useDebouncedDigitInput({ inputValue, setInputValue, isValidDigit, onDigit, enabled = true, once = false, debounceMs = DEFAULT_DEBOUNCE_MS, }: { inputValue: string setInputValue: (value: string) => void isValidDigit: (char: string) => char is T onDigit: (digit: T) => void enabled?: boolean once?: boolean debounceMs?: number }): void { const initialInputValue = useRef(inputValue) const hasTriggeredRef = useRef(false) const debounceRef = useRef | null>(null) // Latest-ref pattern so callers can pass inline callbacks without causing // the effect to re-run (which would reset the debounce timer every render). const callbacksRef = useRef({ setInputValue, isValidDigit, onDigit }) callbacksRef.current = { setInputValue, isValidDigit, onDigit } useEffect(() => { if (!enabled || (once && hasTriggeredRef.current)) { return } if (debounceRef.current !== null) { clearTimeout(debounceRef.current) debounceRef.current = null } if (inputValue !== initialInputValue.current) { const lastChar = normalizeFullWidthDigits(inputValue.slice(-1)) if (callbacksRef.current.isValidDigit(lastChar)) { const trimmed = inputValue.slice(0, -1) debounceRef.current = setTimeout( (debounceRef, hasTriggeredRef, callbacksRef, trimmed, lastChar) => { debounceRef.current = null hasTriggeredRef.current = true callbacksRef.current.setInputValue(trimmed) callbacksRef.current.onDigit(lastChar) }, debounceMs, debounceRef, hasTriggeredRef, callbacksRef, trimmed, lastChar, ) } } return () => { if (debounceRef.current !== null) { clearTimeout(debounceRef.current) debounceRef.current = null } } }, [inputValue, enabled, once, debounceMs]) }