1- import { useRef , useMemo , useCallback , useState , memo } from "react" ;
1+ import { useRef , useMemo , useCallback , useState , useLayoutEffect , memo } from "react" ;
22import { useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis" ;
3- import { isMusicTrack } from "../../utils/timelineInspector" ;
43import { remapBeatAnalysisToComposition } from "../../utils/beatEditActions" ;
54import { usePlayerStore , type TimelineElement } from "../store/playerStore" ;
65import { useExpandedTimelineElements } from "../hooks/useExpandedTimelineElements" ;
@@ -42,6 +41,8 @@ import {
4241import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle" ;
4342import { useTimelineShiftModifier } from "./useTimelineShiftModifier" ;
4443import { useTimelineTicks } from "./useTimelineTicks" ;
44+ import { getTimelineElementIndexes } from "../lib/timelineElementIndexes" ;
45+ import { getTimelineScrollTopForGeometryChange } from "./timelineViewportGeometry" ;
4546
4647// Re-export pure utilities so existing imports from "./Timeline" still resolve.
4748export {
@@ -57,6 +58,11 @@ export {
5758} from "./timelineLayout" ;
5859export { formatTimelineTickLabel , generateTicks } from "./timelineRulerGeometry" ;
5960
61+ export {
62+ getTimelineScrollTopForGeometryChange ,
63+ getTimelineVisibleTimeRange ,
64+ } from "./timelineViewportGeometry" ;
65+
6066export const Timeline = memo ( function Timeline ( {
6167 onSeek,
6268 onDrillDown,
@@ -75,6 +81,7 @@ export const Timeline = memo(function Timeline({
7581 onSplitElement : onSplitElementOverride ,
7682 onSelectElement,
7783 theme : themeOverrides ,
84+ sessionEpoch = 0 ,
7885} : TimelineProps = { } ) {
7986 const {
8087 onMoveElement,
@@ -106,7 +113,7 @@ export const Timeline = memo(function Timeline({
106113 const rawElements = usePlayerStore ( ( s ) => s . elements ) ;
107114 const expandedElements = useExpandedTimelineElements ( ) ;
108115 const beatAnalysis = usePlayerStore ( ( s ) => s . beatAnalysis ) ;
109- const musicElement = usePlayerStore ( ( s ) => s . elements . find ( isMusicTrack ) ?? null ) ;
116+ const musicElement = usePlayerStore ( ( s ) => getTimelineElementIndexes ( s . elements ) . musicElement ) ;
110117 const beatEdits = usePlayerStore ( ( s ) => s . beatEdits ) ;
111118 const adjustedBeatAnalysis = useMemo (
112119 ( ) => remapBeatAnalysisToComposition ( beatAnalysis , musicElement , beatEdits ) ,
@@ -164,8 +171,20 @@ export const Timeline = memo(function Timeline({
164171
165172 const keyframeCache = usePlayerStore ( ( s ) => s . keyframeCache ) ;
166173 useAutoExpandKeyframedClips ( gsapAnimations ) ;
167- const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
168- useTimelineTrackLayout ( expandedElements , gsapAnimations , selectedElementId , selectedElementIds ) ;
174+ const {
175+ tracks,
176+ trackStyles,
177+ trackOrder,
178+ trackOrderRef,
179+ laneCounts,
180+ rowGeometry,
181+ rowGeometryRef,
182+ } = useTimelineTrackLayout (
183+ expandedElements ,
184+ gsapAnimations ,
185+ selectedElementId ,
186+ selectedElementIds ,
187+ ) ;
169188 const expandedElementsRef = useRef ( expandedElements ) ;
170189 expandedElementsRef . current = expandedElements ;
171190
@@ -232,7 +251,7 @@ export const Timeline = memo(function Timeline({
232251 ppsRef,
233252 durationRef,
234253 trackOrderRef,
235- rowHeightsRef ,
254+ rowGeometryRef ,
236255 onMoveElement : pinnedOnMoveElement ,
237256 onMoveElements : pinnedOnMoveElements ,
238257 onResizeElement : pinnedOnResizeElement ,
@@ -251,25 +270,52 @@ export const Timeline = memo(function Timeline({
251270 ppsRef,
252271 durationRef,
253272 trackOrderRef,
254- rowHeightsRef ,
273+ rowGeometryRef ,
255274 contentOrigin,
256275 onFileDrop : pinnedOnFileDrop ,
257276 onAssetDrop : pinnedOnAssetDrop ,
258277 onBlockDrop : pinnedOnBlockDrop ,
259278 onCompositionDrop : pinnedOnCompositionDrop ,
260279 } ) ;
261280
262- const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowHeights ) ;
281+ const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowGeometry ) ;
263282 const { recordTimelineScroll } = useTimelinePerformanceTelemetry ( {
264283 totalClipCount : expandedElements . length ,
265284 totalRowCount : displayLayout . displayTrackOrder . length ,
266285 zoomMode,
267286 } ) ;
268- const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport ( scrollRef , [
269- timelineReady ,
270- expandedElements . length ,
271- displayLayout . totalH ,
272- ] ) ;
287+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
288+ useTimelineScrollViewport ( scrollRef , [
289+ timelineReady ,
290+ expandedElements . length ,
291+ displayLayout . totalH ,
292+ ] ) ;
293+ const previousLayoutRef = useRef ( displayLayout . rowGeometry ) ;
294+ const previousSessionEpochRef = useRef ( sessionEpoch ) ;
295+ useLayoutEffect ( ( ) => {
296+ const scroll = scrollRef . current ;
297+ const previousGeometry = previousLayoutRef . current ;
298+ if ( previousSessionEpochRef . current !== sessionEpoch ) {
299+ previousSessionEpochRef . current = sessionEpoch ;
300+ lastScrollLeftRef . current = 0 ;
301+ if ( scroll ) {
302+ scroll . scrollLeft = 0 ;
303+ scroll . scrollTop = 0 ;
304+ syncScrollViewport ( scroll ) ;
305+ }
306+ } else if ( scroll && previousGeometry !== displayLayout . rowGeometry ) {
307+ const nextScrollTop = getTimelineScrollTopForGeometryChange (
308+ previousGeometry ,
309+ displayLayout . rowGeometry ,
310+ scroll . scrollTop ,
311+ ) ;
312+ if ( nextScrollTop !== scroll . scrollTop ) {
313+ scroll . scrollTop = nextScrollTop ;
314+ syncScrollViewport ( scroll ) ;
315+ }
316+ }
317+ previousLayoutRef . current = displayLayout . rowGeometry ;
318+ } , [ displayLayout . rowGeometry , sessionEpoch , syncScrollViewport ] ) ;
273319 const selectedKeyframes = usePlayerStore ( ( s ) => s . selectedKeyframes ) ;
274320 const toggleSelectedKeyframe = usePlayerStore ( ( s ) => s . toggleSelectedKeyframe ) ;
275321 const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
@@ -292,7 +338,7 @@ export const Timeline = memo(function Timeline({
292338 zoomModeRef,
293339 manualZoomPercentRef,
294340 } = useTimelineGeometry ( {
295- viewportWidth,
341+ viewportWidth : viewport . clientWidth ,
296342 effectiveDuration,
297343 zoomMode,
298344 manualZoomPercent,
@@ -375,7 +421,7 @@ export const Timeline = memo(function Timeline({
375421 setShowPopover,
376422 elementsRef : expandedElementsRef ,
377423 trackOrderRef,
378- rowHeightsRef ,
424+ rowGeometryRef ,
379425 onSelectElement,
380426 contentOrigin,
381427 } ) ;
@@ -409,6 +455,7 @@ export const Timeline = memo(function Timeline({
409455 < div
410456 ref = { setContainerRef }
411457 aria-label = "Timeline"
458+ data-timeline-element-count = { expandedElements . length }
412459 className = { `relative border-t select-none h-full overflow-hidden ${ isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : "" } ${ activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default" } ` }
413460 onMouseMove = { updateRazorGuide }
414461 onMouseLeave = { clearRazorGuide }
@@ -420,11 +467,13 @@ export const Timeline = memo(function Timeline({
420467 >
421468 < div
422469 ref = { setScrollRef }
470+ data-timeline-scroll-viewport
423471 tabIndex = { - 1 }
424472 className = { `${ zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto" } overflow-y-auto h-full outline-none` }
425473 onScroll = { ( e ) => {
426474 lastScrollLeftRef . current = e . currentTarget . scrollLeft ; // restored across post-edit reload
427475 recordTimelineScroll ( e . currentTarget ) ;
476+ syncScrollViewport ( e . currentTarget , true ) ;
428477 } }
429478 onDragOver = { handleAssetDragOver }
430479 onDragLeave = { ( ) => clearDropPreview ( ) }
0 commit comments