import { useRef, useCallback } from 'react'; interface DebounceOptions { delay?: number; leading?: boolean; } /** * Hook for debouncing function calls * @param callback Function to debounce * @param options Configuration options * @param options.delay Debounce delay in milliseconds (default: 1000ms) * @param options.leading If true, invoke on leading edge instead of trailing (default: true) * @returns Debounced function and isDebouncing state getter */ export function useDebounce any>( callback: T, options: DebounceOptions = {} ): { debouncedFn: (...args: Parameters) => void; isDebouncing: () => boolean; cancel: () => void; } { const { delay = 1000, leading = true } = options; const timeoutRef = useRef | null>(null); const lastCallTimeRef = useRef(0); const cancel = useCallback(() => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }, []); const isDebouncing = useCallback(() => { const now = Date.now(); return now - lastCallTimeRef.current < delay; }, [delay]); const debouncedFn = useCallback( (...args: Parameters) => { const now = Date.now(); const timeSinceLastCall = now - lastCallTimeRef.current; // If leading edge and enough time has passed, execute immediately if (leading && timeSinceLastCall >= delay) { lastCallTimeRef.current = now; callback(...args); return; } // If already debouncing on leading edge, ignore if (leading && timeSinceLastCall < delay) { return; } // Trailing edge behavior cancel(); timeoutRef.current = setTimeout(() => { lastCallTimeRef.current = Date.now(); callback(...args); timeoutRef.current = null; }, delay); }, [callback, delay, leading, cancel] ); return { debouncedFn, isDebouncing, cancel }; }