diff --git a/src/components/LyricsPane.tsx b/src/components/LyricsPane.tsx index fac4a17d..415598fa 100644 --- a/src/components/LyricsPane.tsx +++ b/src/components/LyricsPane.tsx @@ -38,6 +38,7 @@ export default function LyricsPane({ currentTrack }: Props) { const lineRefs = useRef<(HTMLDivElement | null)[]>([]); const wordRefs = useRef([]); const prevActive = useRef({ line: -1, word: -1 }); + const prevTrackId = useRef(undefined); const isUserScroll = useRef(false); const scrollTimer = useRef | null>(null); @@ -65,12 +66,18 @@ export default function LyricsPane({ currentTrack }: Props) { } }, [sidebarLyricsStyle]); - // Reset refs when track changes. + // Reset refs on actual track change. Skip the initial mount so we don't wipe + // the callback-refs that the commit phase just populated — that would leave + // the tracker effect with an empty lineRefs and freeze the highlight. useEffect(() => { - lineRefs.current = []; - wordRefs.current = []; - prevActive.current = { line: -1, word: -1 }; - scrollerRef.current?.jump(0); + const id = currentTrack?.id ?? null; + if (prevTrackId.current !== undefined && prevTrackId.current !== id) { + lineRefs.current = []; + wordRefs.current = []; + prevActive.current = { line: -1, word: -1 }; + scrollerRef.current?.jump(0); + } + prevTrackId.current = id; }, [currentTrack?.id]); // Imperative tracker — subscribes directly to the store, zero React re-renders per tick.