diff options
Diffstat (limited to 'apps/web/src/hooks/timeline/use-scroll-sync.ts')
| -rw-r--r-- | apps/web/src/hooks/timeline/use-scroll-sync.ts | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/apps/web/src/hooks/timeline/use-scroll-sync.ts b/apps/web/src/hooks/timeline/use-scroll-sync.ts new file mode 100644 index 00000000..cdd915b0 --- /dev/null +++ b/apps/web/src/hooks/timeline/use-scroll-sync.ts @@ -0,0 +1,173 @@ +import { useEffect, useRef } from "react"; + +interface UseScrollSyncProps { + rulerScrollRef?: React.RefObject<HTMLDivElement>; + tracksScrollRef: React.RefObject<HTMLDivElement>; + trackLabelsScrollRef?: React.RefObject<HTMLDivElement>; + bookmarksScrollRef?: React.RefObject<HTMLDivElement>; +} + +export function useScrollSync({ + tracksScrollRef, + rulerScrollRef, + trackLabelsScrollRef, + bookmarksScrollRef, +}: UseScrollSyncProps) { + const isUpdatingRef = useRef(false); + const lastRulerSync = useRef(0); + const lastTracksSync = useRef(0); + const lastVerticalSync = useRef(0); + const lastBookmarksSync = useRef(0); + + useEffect(() => { + const rulerViewport = rulerScrollRef?.current ?? null; + const tracksViewport = tracksScrollRef.current; + const trackLabelsViewport = trackLabelsScrollRef?.current; + const bookmarksViewport = bookmarksScrollRef?.current; + let handleBookmarksScroll: (() => void) | null = null; + + if (!tracksViewport) return; + + const syncScrollLeft = ({ + target, + scrollLeft, + }: { + target: HTMLDivElement | null | undefined; + scrollLeft: number; + }) => { + if (target) { + target.scrollLeft = scrollLeft; + } + }; + + const handleRulerScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastRulerSync.current < 16) return; + lastRulerSync.current = now; + isUpdatingRef.current = true; + tracksViewport.scrollLeft = rulerViewport?.scrollLeft ?? 0; + syncScrollLeft({ + target: bookmarksViewport, + scrollLeft: rulerViewport?.scrollLeft ?? 0, + }); + isUpdatingRef.current = false; + }; + + const handleTracksScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastTracksSync.current < 16) return; + lastTracksSync.current = now; + isUpdatingRef.current = true; + syncScrollLeft({ + target: rulerViewport, + scrollLeft: tracksViewport.scrollLeft, + }); + syncScrollLeft({ + target: bookmarksViewport, + scrollLeft: tracksViewport.scrollLeft, + }); + isUpdatingRef.current = false; + }; + + if (rulerViewport && rulerViewport !== tracksViewport) { + rulerViewport.addEventListener("scroll", handleRulerScroll); + } + if (tracksViewport !== rulerViewport) { + tracksViewport.addEventListener("scroll", handleTracksScroll); + } + + if (bookmarksViewport) { + handleBookmarksScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastBookmarksSync.current < 16) + return; + lastBookmarksSync.current = now; + isUpdatingRef.current = true; + const nextScrollLeft = bookmarksViewport.scrollLeft; + tracksViewport.scrollLeft = nextScrollLeft; + syncScrollLeft({ + target: rulerViewport, + scrollLeft: nextScrollLeft, + }); + isUpdatingRef.current = false; + }; + + if (bookmarksViewport !== tracksViewport) { + bookmarksViewport.addEventListener("scroll", handleBookmarksScroll); + } + } + + if (trackLabelsViewport) { + const handleTrackLabelsScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastVerticalSync.current < 16) + return; + lastVerticalSync.current = now; + isUpdatingRef.current = true; + tracksViewport.scrollTop = trackLabelsViewport.scrollTop; + isUpdatingRef.current = false; + }; + + const handleTracksVerticalScroll = () => { + const now = Date.now(); + if (isUpdatingRef.current || now - lastVerticalSync.current < 16) + return; + lastVerticalSync.current = now; + isUpdatingRef.current = true; + trackLabelsViewport.scrollTop = tracksViewport.scrollTop; + isUpdatingRef.current = false; + }; + + trackLabelsViewport.addEventListener("scroll", handleTrackLabelsScroll); + tracksViewport.addEventListener("scroll", handleTracksVerticalScroll); + + return () => { + if (rulerViewport && rulerViewport !== tracksViewport) { + rulerViewport.removeEventListener("scroll", handleRulerScroll); + } + if (tracksViewport !== rulerViewport) { + tracksViewport.removeEventListener("scroll", handleTracksScroll); + } + if ( + bookmarksViewport && + bookmarksViewport !== tracksViewport && + handleBookmarksScroll + ) { + bookmarksViewport.removeEventListener( + "scroll", + handleBookmarksScroll, + ); + } + trackLabelsViewport.removeEventListener( + "scroll", + handleTrackLabelsScroll, + ); + tracksViewport.removeEventListener( + "scroll", + handleTracksVerticalScroll, + ); + }; + } + + return () => { + if (rulerViewport && rulerViewport !== tracksViewport) { + rulerViewport.removeEventListener("scroll", handleRulerScroll); + } + if (tracksViewport !== rulerViewport) { + tracksViewport.removeEventListener("scroll", handleTracksScroll); + } + if ( + bookmarksViewport && + bookmarksViewport !== tracksViewport && + handleBookmarksScroll + ) { + bookmarksViewport.removeEventListener("scroll", handleBookmarksScroll); + } + }; + }, [ + rulerScrollRef, + tracksScrollRef, + trackLabelsScrollRef, + bookmarksScrollRef, + ]); +} |
