import { useState, useCallback, useRef, useEffect } from 'react' export type TypingEngineResult = { currentIndex: number errors: Set wpm: number accuracy: number isComplete: boolean keyStats: Record handleKeyDown: (e: React.KeyboardEvent) => void reset: (newText?: string) => void } export function useTypingEngine( initialText: string, onComplete?: (result: { wpm: number; accuracy: number; keyStats: Record }) => void, ): TypingEngineResult { const [text, setText] = useState(initialText) const [currentIndex, setCurrentIndex] = useState(0) const [errors, setErrors] = useState>(new Set()) const [totalKeystrokes, setTotalKeystrokes] = useState(0) const [correctKeystrokes, setCorrectKeystrokes] = useState(0) const [isComplete, setIsComplete] = useState(false) const [keyStats, setKeyStats] = useState>({}) const startTimeRef = useRef(null) const [wpm, setWpm] = useState(0) const onCompleteRef = useRef(onComplete) onCompleteRef.current = onComplete const accuracy = totalKeystrokes > 0 ? Math.round((correctKeystrokes / totalKeystrokes) * 100) : 100 // Update WPM periodically useEffect(() => { if (isComplete || currentIndex === 0) return const interval = setInterval(() => { if (startTimeRef.current) { const minutes = (Date.now() - startTimeRef.current) / 60000 if (minutes > 0) { setWpm(Math.round((currentIndex / 5) / minutes)) } } }, 500) return () => clearInterval(interval) }, [currentIndex, isComplete]) const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (isComplete) return // Ignore modifier keys, function keys, etc. if (e.key.length > 1 && e.key !== 'Backspace') return e.preventDefault() if (e.key === 'Backspace') { setCurrentIndex(prev => Math.max(0, prev - 1)) setErrors(prev => { const next = new Set(prev) next.delete(currentIndex - 1) return next }) return } if (!startTimeRef.current) { startTimeRef.current = Date.now() } const expected = text[currentIndex] const typed = e.key const charKey = expected?.toLowerCase() ?? typed.toLowerCase() setTotalKeystrokes(prev => prev + 1) if (typed === expected) { setCorrectKeystrokes(prev => prev + 1) setKeyStats(prev => ({ ...prev, [charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 }, })) const nextIndex = currentIndex + 1 setCurrentIndex(nextIndex) if (nextIndex >= text.length) { setIsComplete(true) const minutes = (Date.now() - (startTimeRef.current ?? Date.now())) / 60000 const finalWpm = minutes > 0 ? Math.round((text.length / 5) / minutes) : 0 setWpm(finalWpm) const finalAccuracy = (totalKeystrokes + 1) > 0 ? Math.round(((correctKeystrokes + 1) / (totalKeystrokes + 1)) * 100) : 100 // Build final keyStats including this last keystroke setKeyStats(prev => { const final = { ...prev, [charKey]: { hits: (prev[charKey]?.hits ?? 0) + 1, misses: prev[charKey]?.misses ?? 0 }, } onCompleteRef.current?.({ wpm: finalWpm, accuracy: finalAccuracy, keyStats: final }) return prev // Don't double-count, already set above }) } } else { setErrors(prev => new Set(prev).add(currentIndex)) setKeyStats(prev => ({ ...prev, [charKey]: { hits: prev[charKey]?.hits ?? 0, misses: (prev[charKey]?.misses ?? 0) + 1 }, })) } }, [currentIndex, text, isComplete, totalKeystrokes, correctKeystrokes]) const reset = useCallback((newText?: string) => { if (newText !== undefined) setText(newText) setCurrentIndex(0) setErrors(new Set()) setTotalKeystrokes(0) setCorrectKeystrokes(0) setIsComplete(false) setKeyStats({}) setWpm(0) startTimeRef.current = null }, []) return { currentIndex, errors, wpm, accuracy, isComplete, keyStats, handleKeyDown, reset } }