diff --git a/frontend/src/hooks/useAudioEffects.ts b/frontend/src/hooks/useAudioEffects.ts index 479a88f..b571b7b 100644 --- a/frontend/src/hooks/useAudioEffects.ts +++ b/frontend/src/hooks/useAudioEffects.ts @@ -83,9 +83,9 @@ export function useAudioEffects({ const wasHoveredRef = useRef(false); const wasActiveRef = useRef(false); - // Track if initial state sync is complete (skip audio on first render) - // User can't hover during mount, so any true state is stale - const didInitRef = useRef(false); + // Delay audio playback after mount/reset to avoid browser autoplay policy + // User can't hover during mount, so any true state within 100ms is stale + const [canPlay, setCanPlay] = useState(false); // Track which URLs we've already fetched to prevent duplicate fetches const lastFetchedHoverUrlRef = useRef(null); @@ -99,6 +99,18 @@ export function useAudioEffects({ const [hoverAudioReady, setHoverAudioReady] = useState(false); const [clickAudioReady, setClickAudioReady] = useState(false); + // Enable audio playback after short delay to skip stale state at mount + // Any hover/active state within 100ms of mount is not from real user interaction + useEffect(() => { + setCanPlay(false); + wasHoveredRef.current = isHovered; + wasActiveRef.current = isActive; + const timer = setTimeout(() => setCanPlay(true), 100); + return () => clearTimeout(timer); + // Only reset on resetKey change, not on isHovered/isActive changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [resetKey]); + // Initialize hover audio element - fetch with credentials for auth useEffect(() => { if (!hoverAudioUrl) { @@ -242,15 +254,9 @@ export function useAudioEffects({ // Play hover audio when hover starts (plays to completion, not interrupted) useEffect(() => { - // Skip audio on initial render - sync state only - // User can't hover during mount, so any true state is stale - if (!didInitRef.current) { - didInitRef.current = true; - wasHoveredRef.current = isHovered; - return; - } - + // Only play if: canPlay is true, hover changed from false→true, audio is ready if ( + canPlay && isHovered && !wasHoveredRef.current && hoverAudioRef.current && @@ -265,18 +271,13 @@ export function useAudioEffects({ }); } wasHoveredRef.current = isHovered; - }, [isHovered, hoverAudioReady]); + }, [canPlay, isHovered, hoverAudioReady]); // Play click audio when active state begins useEffect(() => { - // Skip audio on initial render - sync state only - // Note: didInitRef is already set by hover effect due to effect ordering - if (!didInitRef.current) { - wasActiveRef.current = isActive; - return; - } - + // Only play if: canPlay is true, active changed from false→true, audio is ready if ( + canPlay && isActive && !wasActiveRef.current && clickAudioRef.current && @@ -289,7 +290,7 @@ export function useAudioEffects({ }); } wasActiveRef.current = isActive; - }, [isActive, clickAudioReady]); + }, [canPlay, isActive, clickAudioReady]); // Manual click audio trigger const playClickAudio = useCallback(() => { @@ -312,12 +313,10 @@ export function useAudioEffects({ } }, []); - // Reset audio on key change (e.g., page navigation or element change) + // Stop audio on key change (e.g., page navigation or element change) + // Note: canPlay and refs are reset in the canPlay effect above useEffect(() => { stopAll(); - wasHoveredRef.current = false; - wasActiveRef.current = false; - didInitRef.current = false; // Reset for new element }, [resetKey, stopAll]); return { playClickAudio, stopAll };