import { useCallback, useEffect, useRef, useState } from "react"; const IDLE_THRESHOLD_MS = 30_000; // 30 seconds of inactivity = idle const TICK_INTERVAL_MS = 1_000; // Update display every second const ACTIVITY_EVENTS = [ "mousemove", "mousedown", "keydown", "scroll", "touchstart", ] as const; export function useActiveTimer() { const [activeMs, setActiveMs] = useState(0); const [totalMs, setTotalMs] = useState(0); const [isIdle, setIsIdle] = useState(false); // Refs for values that change frequently but shouldn't trigger re-renders const activeMsRef = useRef(0); const totalMsRef = useRef(0); const lastActivityRef = useRef(Date.now()); const lastTickRef = useRef(Date.now()); const idleRef = useRef(false); const startedRef = useRef(Date.now()); const reset = useCallback(() => { const now = Date.now(); activeMsRef.current = 0; totalMsRef.current = 0; lastActivityRef.current = now; lastTickRef.current = now; startedRef.current = now; idleRef.current = false; setActiveMs(0); setTotalMs(0); setIsIdle(false); }, []); // Mark activity on user interaction useEffect(() => { function onActivity() { lastActivityRef.current = Date.now(); if (idleRef.current) { idleRef.current = false; setIsIdle(false); // Reset the tick baseline so we don't count the idle gap as active lastTickRef.current = Date.now(); } } for (const event of ACTIVITY_EVENTS) { window.addEventListener(event, onActivity, { passive: true }); } // Also track when the tab regains focus window.addEventListener("focus", onActivity); return () => { for (const event of ACTIVITY_EVENTS) { window.removeEventListener(event, onActivity); } window.removeEventListener("focus", onActivity); }; }, []); // Tick loop: accumulate active time and detect idle transitions useEffect(() => { const interval = setInterval(() => { const now = Date.now(); const elapsed = now - lastTickRef.current; lastTickRef.current = now; // Always accumulate wall-clock time totalMsRef.current = now - startedRef.current; setTotalMs(totalMsRef.current); const timeSinceActivity = now - lastActivityRef.current; if (timeSinceActivity >= IDLE_THRESHOLD_MS) { // User is idle — don't accumulate active time if (!idleRef.current) { idleRef.current = true; setIsIdle(true); } } else { // User is active — accumulate activeMsRef.current += elapsed; setActiveMs(activeMsRef.current); } }, TICK_INTERVAL_MS); return () => clearInterval(interval); }, []); return { /** Milliseconds the user was actively engaged (idle time excluded) */ activeMs, /** Total wall-clock milliseconds since last reset */ totalMs, /** Whether the user is currently idle */ isIdle, /** Reset both counters (call when loading a new paragraph) */ reset, }; }