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,20 +270,47 @@ 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 ) ;
263- const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport ( scrollRef , [
264- timelineReady ,
265- expandedElements . length ,
266- displayLayout . totalH ,
267- ] ) ;
281+ const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowGeometry ) ;
282+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
283+ useTimelineScrollViewport ( scrollRef , [
284+ timelineReady ,
285+ expandedElements . length ,
286+ displayLayout . totalH ,
287+ ] ) ;
288+ const previousLayoutRef = useRef ( displayLayout . rowGeometry ) ;
289+ const previousSessionEpochRef = useRef ( sessionEpoch ) ;
290+ useLayoutEffect ( ( ) => {
291+ const scroll = scrollRef . current ;
292+ const previousGeometry = previousLayoutRef . current ;
293+ if ( previousSessionEpochRef . current !== sessionEpoch ) {
294+ previousSessionEpochRef . current = sessionEpoch ;
295+ lastScrollLeftRef . current = 0 ;
296+ if ( scroll ) {
297+ scroll . scrollLeft = 0 ;
298+ scroll . scrollTop = 0 ;
299+ syncScrollViewport ( scroll ) ;
300+ }
301+ } else if ( scroll && previousGeometry !== displayLayout . rowGeometry ) {
302+ const nextScrollTop = getTimelineScrollTopForGeometryChange (
303+ previousGeometry ,
304+ displayLayout . rowGeometry ,
305+ scroll . scrollTop ,
306+ ) ;
307+ if ( nextScrollTop !== scroll . scrollTop ) {
308+ scroll . scrollTop = nextScrollTop ;
309+ syncScrollViewport ( scroll ) ;
310+ }
311+ }
312+ previousLayoutRef . current = displayLayout . rowGeometry ;
313+ } , [ displayLayout . rowGeometry , sessionEpoch , syncScrollViewport ] ) ;
268314 const selectedKeyframes = usePlayerStore ( ( s ) => s . selectedKeyframes ) ;
269315 const toggleSelectedKeyframe = usePlayerStore ( ( s ) => s . toggleSelectedKeyframe ) ;
270316 const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
@@ -287,7 +333,7 @@ export const Timeline = memo(function Timeline({
287333 zoomModeRef,
288334 manualZoomPercentRef,
289335 } = useTimelineGeometry ( {
290- viewportWidth,
336+ viewportWidth : viewport . clientWidth ,
291337 effectiveDuration,
292338 zoomMode,
293339 manualZoomPercent,
@@ -370,7 +416,7 @@ export const Timeline = memo(function Timeline({
370416 setShowPopover,
371417 elementsRef : expandedElementsRef ,
372418 trackOrderRef,
373- rowHeightsRef ,
419+ rowGeometryRef ,
374420 onSelectElement,
375421 contentOrigin,
376422 } ) ;
@@ -404,6 +450,7 @@ export const Timeline = memo(function Timeline({
404450 < div
405451 ref = { setContainerRef }
406452 aria-label = "Timeline"
453+ data-timeline-element-count = { expandedElements . length }
407454 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" } ` }
408455 onMouseMove = { updateRazorGuide }
409456 onMouseLeave = { clearRazorGuide }
@@ -415,10 +462,12 @@ export const Timeline = memo(function Timeline({
415462 >
416463 < div
417464 ref = { setScrollRef }
465+ data-timeline-scroll-viewport
418466 tabIndex = { - 1 }
419467 className = { `${ zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto" } overflow-y-auto h-full outline-none` }
420468 onScroll = { ( e ) => {
421469 lastScrollLeftRef . current = e . currentTarget . scrollLeft ; // restored across post-edit reload
470+ syncScrollViewport ( e . currentTarget , true ) ;
422471 } }
423472 onDragOver = { handleAssetDragOver }
424473 onDragLeave = { ( ) => clearDropPreview ( ) }
0 commit comments