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 {
@@ -58,6 +59,11 @@ export {
5859 getDefaultDroppedTrack ,
5960} from "./timelineLayout" ;
6061
62+ export {
63+ getTimelineScrollTopForGeometryChange ,
64+ getTimelineVisibleTimeRange ,
65+ } from "./timelineViewportGeometry" ;
66+
6167export const Timeline = memo ( function Timeline ( {
6268 onSeek,
6369 onDrillDown,
@@ -76,6 +82,7 @@ export const Timeline = memo(function Timeline({
7682 onSplitElement : onSplitElementOverride ,
7783 onSelectElement,
7884 theme : themeOverrides ,
85+ sessionEpoch = 0 ,
7986} : TimelineProps = { } ) {
8087 const {
8188 onMoveElement,
@@ -108,7 +115,7 @@ export const Timeline = memo(function Timeline({
108115 const rawElements = usePlayerStore ( ( s ) => s . elements ) ;
109116 const expandedElements = useExpandedTimelineElements ( ) ;
110117 const beatAnalysis = usePlayerStore ( ( s ) => s . beatAnalysis ) ;
111- const musicElement = usePlayerStore ( ( s ) => s . elements . find ( isMusicTrack ) ?? null ) ;
118+ const musicElement = usePlayerStore ( ( s ) => getTimelineElementIndexes ( s . elements ) . musicElement ) ;
112119 const beatEdits = usePlayerStore ( ( s ) => s . beatEdits ) ;
113120 const adjustedBeatAnalysis = useMemo (
114121 ( ) => remapBeatAnalysisToComposition ( beatAnalysis , musicElement , beatEdits ) ,
@@ -166,8 +173,20 @@ export const Timeline = memo(function Timeline({
166173
167174 const keyframeCache = usePlayerStore ( ( s ) => s . keyframeCache ) ;
168175 useAutoExpandKeyframedClips ( gsapAnimations ) ;
169- const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
170- useTimelineTrackLayout ( expandedElements , gsapAnimations , selectedElementId , selectedElementIds ) ;
176+ const {
177+ tracks,
178+ trackStyles,
179+ trackOrder,
180+ trackOrderRef,
181+ laneCounts,
182+ rowGeometry,
183+ rowGeometryRef,
184+ } = useTimelineTrackLayout (
185+ expandedElements ,
186+ gsapAnimations ,
187+ selectedElementId ,
188+ selectedElementIds ,
189+ ) ;
171190 const expandedElementsRef = useRef ( expandedElements ) ;
172191 expandedElementsRef . current = expandedElements ;
173192
@@ -234,7 +253,7 @@ export const Timeline = memo(function Timeline({
234253 ppsRef,
235254 durationRef,
236255 trackOrderRef,
237- rowHeightsRef ,
256+ rowGeometryRef ,
238257 onMoveElement : pinnedOnMoveElement ,
239258 onMoveElements : pinnedOnMoveElements ,
240259 onResizeElement : pinnedOnResizeElement ,
@@ -253,20 +272,47 @@ export const Timeline = memo(function Timeline({
253272 ppsRef,
254273 durationRef,
255274 trackOrderRef,
256- rowHeightsRef ,
275+ rowGeometryRef ,
257276 contentOrigin,
258277 onFileDrop : pinnedOnFileDrop ,
259278 onAssetDrop : pinnedOnAssetDrop ,
260279 onBlockDrop : pinnedOnBlockDrop ,
261280 onCompositionDrop : pinnedOnCompositionDrop ,
262281 } ) ;
263282
264- const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowHeights ) ;
265- const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport ( scrollRef , [
266- timelineReady ,
267- expandedElements . length ,
268- displayLayout . totalH ,
269- ] ) ;
283+ const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowGeometry ) ;
284+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
285+ useTimelineScrollViewport ( scrollRef , [
286+ timelineReady ,
287+ expandedElements . length ,
288+ displayLayout . totalH ,
289+ ] ) ;
290+ const previousLayoutRef = useRef ( displayLayout . rowGeometry ) ;
291+ const previousSessionEpochRef = useRef ( sessionEpoch ) ;
292+ useLayoutEffect ( ( ) => {
293+ const scroll = scrollRef . current ;
294+ const previousGeometry = previousLayoutRef . current ;
295+ if ( previousSessionEpochRef . current !== sessionEpoch ) {
296+ previousSessionEpochRef . current = sessionEpoch ;
297+ lastScrollLeftRef . current = 0 ;
298+ if ( scroll ) {
299+ scroll . scrollLeft = 0 ;
300+ scroll . scrollTop = 0 ;
301+ syncScrollViewport ( scroll ) ;
302+ }
303+ } else if ( scroll && previousGeometry !== displayLayout . rowGeometry ) {
304+ const nextScrollTop = getTimelineScrollTopForGeometryChange (
305+ previousGeometry ,
306+ displayLayout . rowGeometry ,
307+ scroll . scrollTop ,
308+ ) ;
309+ if ( nextScrollTop !== scroll . scrollTop ) {
310+ scroll . scrollTop = nextScrollTop ;
311+ syncScrollViewport ( scroll ) ;
312+ }
313+ }
314+ previousLayoutRef . current = displayLayout . rowGeometry ;
315+ } , [ displayLayout . rowGeometry , sessionEpoch , syncScrollViewport ] ) ;
270316 const selectedKeyframes = usePlayerStore ( ( s ) => s . selectedKeyframes ) ;
271317 const toggleSelectedKeyframe = usePlayerStore ( ( s ) => s . toggleSelectedKeyframe ) ;
272318 const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
@@ -289,7 +335,7 @@ export const Timeline = memo(function Timeline({
289335 zoomModeRef,
290336 manualZoomPercentRef,
291337 } = useTimelineGeometry ( {
292- viewportWidth,
338+ viewportWidth : viewport . clientWidth ,
293339 effectiveDuration,
294340 zoomMode,
295341 manualZoomPercent,
@@ -372,7 +418,7 @@ export const Timeline = memo(function Timeline({
372418 setShowPopover,
373419 elementsRef : expandedElementsRef ,
374420 trackOrderRef,
375- rowHeightsRef ,
421+ rowGeometryRef ,
376422 onSelectElement,
377423 contentOrigin,
378424 } ) ;
@@ -406,6 +452,7 @@ export const Timeline = memo(function Timeline({
406452 < div
407453 ref = { setContainerRef }
408454 aria-label = "Timeline"
455+ data-timeline-element-count = { expandedElements . length }
409456 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" } ` }
410457 onMouseMove = { updateRazorGuide }
411458 onMouseLeave = { clearRazorGuide }
@@ -417,10 +464,12 @@ export const Timeline = memo(function Timeline({
417464 >
418465 < div
419466 ref = { setScrollRef }
467+ data-timeline-scroll-viewport
420468 tabIndex = { - 1 }
421469 className = { `${ zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto" } overflow-y-auto h-full outline-none` }
422470 onScroll = { ( e ) => {
423471 lastScrollLeftRef . current = e . currentTarget . scrollLeft ; // restored across post-edit reload
472+ syncScrollViewport ( e . currentTarget , true ) ;
424473 } }
425474 onDragOver = { handleAssetDragOver }
426475 onDragLeave = { ( ) => clearDropPreview ( ) }
0 commit comments