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" ;
@@ -41,6 +40,8 @@ import {
4140import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle" ;
4241import { useTimelineShiftModifier } from "./useTimelineShiftModifier" ;
4342import { useTimelineTicks } from "./useTimelineTicks" ;
43+ import { getTimelineElementIndexes } from "../lib/timelineElementIndexes" ;
44+ import { getTimelineScrollTopForGeometryChange } from "./timelineViewportGeometry" ;
4445
4546// Re-export pure utilities so existing imports from "./Timeline" still resolve.
4647export {
@@ -56,6 +57,11 @@ export {
5657} from "./timelineLayout" ;
5758export { formatTimelineTickLabel , generateTicks } from "./timelineRulerGeometry" ;
5859
60+ export {
61+ getTimelineScrollTopForGeometryChange ,
62+ getTimelineVisibleTimeRange ,
63+ } from "./timelineViewportGeometry" ;
64+
5965export const Timeline = memo ( function Timeline ( {
6066 onSeek,
6167 onDrillDown,
@@ -74,6 +80,7 @@ export const Timeline = memo(function Timeline({
7480 onSplitElement : onSplitElementOverride ,
7581 onSelectElement,
7682 theme : themeOverrides ,
83+ sessionEpoch = 0 ,
7784} : TimelineProps = { } ) {
7885 const {
7986 onMoveElement,
@@ -105,7 +112,7 @@ export const Timeline = memo(function Timeline({
105112 const rawElements = usePlayerStore ( ( s ) => s . elements ) ;
106113 const expandedElements = useExpandedTimelineElements ( ) ;
107114 const beatAnalysis = usePlayerStore ( ( s ) => s . beatAnalysis ) ;
108- const musicElement = usePlayerStore ( ( s ) => s . elements . find ( isMusicTrack ) ?? null ) ;
115+ const musicElement = usePlayerStore ( ( s ) => getTimelineElementIndexes ( s . elements ) . musicElement ) ;
109116 const beatEdits = usePlayerStore ( ( s ) => s . beatEdits ) ;
110117 const adjustedBeatAnalysis = useMemo (
111118 ( ) => remapBeatAnalysisToComposition ( beatAnalysis , musicElement , beatEdits ) ,
@@ -163,8 +170,20 @@ export const Timeline = memo(function Timeline({
163170
164171 const keyframeCache = usePlayerStore ( ( s ) => s . keyframeCache ) ;
165172 useAutoExpandKeyframedClips ( gsapAnimations ) ;
166- const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
167- useTimelineTrackLayout ( expandedElements , gsapAnimations , selectedElementId , selectedElementIds ) ;
173+ const {
174+ tracks,
175+ trackStyles,
176+ trackOrder,
177+ trackOrderRef,
178+ laneCounts,
179+ rowGeometry,
180+ rowGeometryRef,
181+ } = useTimelineTrackLayout (
182+ expandedElements ,
183+ gsapAnimations ,
184+ selectedElementId ,
185+ selectedElementIds ,
186+ ) ;
168187 const expandedElementsRef = useRef ( expandedElements ) ;
169188 expandedElementsRef . current = expandedElements ;
170189
@@ -231,7 +250,7 @@ export const Timeline = memo(function Timeline({
231250 ppsRef,
232251 durationRef,
233252 trackOrderRef,
234- rowHeightsRef ,
253+ rowGeometryRef ,
235254 onMoveElement : pinnedOnMoveElement ,
236255 onMoveElements : pinnedOnMoveElements ,
237256 onResizeElement : pinnedOnResizeElement ,
@@ -250,20 +269,47 @@ export const Timeline = memo(function Timeline({
250269 ppsRef,
251270 durationRef,
252271 trackOrderRef,
253- rowHeightsRef ,
272+ rowGeometryRef ,
254273 contentOrigin,
255274 onFileDrop : pinnedOnFileDrop ,
256275 onAssetDrop : pinnedOnAssetDrop ,
257276 onBlockDrop : pinnedOnBlockDrop ,
258277 onCompositionDrop : pinnedOnCompositionDrop ,
259278 } ) ;
260279
261- const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowHeights ) ;
262- const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport ( scrollRef , [
263- timelineReady ,
264- expandedElements . length ,
265- displayLayout . totalH ,
266- ] ) ;
280+ const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowGeometry ) ;
281+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
282+ useTimelineScrollViewport ( scrollRef , [
283+ timelineReady ,
284+ expandedElements . length ,
285+ displayLayout . totalH ,
286+ ] ) ;
287+ const previousLayoutRef = useRef ( displayLayout . rowGeometry ) ;
288+ const previousSessionEpochRef = useRef ( sessionEpoch ) ;
289+ useLayoutEffect ( ( ) => {
290+ const scroll = scrollRef . current ;
291+ const previousGeometry = previousLayoutRef . current ;
292+ if ( previousSessionEpochRef . current !== sessionEpoch ) {
293+ previousSessionEpochRef . current = sessionEpoch ;
294+ lastScrollLeftRef . current = 0 ;
295+ if ( scroll ) {
296+ scroll . scrollLeft = 0 ;
297+ scroll . scrollTop = 0 ;
298+ syncScrollViewport ( scroll ) ;
299+ }
300+ } else if ( scroll && previousGeometry !== displayLayout . rowGeometry ) {
301+ const nextScrollTop = getTimelineScrollTopForGeometryChange (
302+ previousGeometry ,
303+ displayLayout . rowGeometry ,
304+ scroll . scrollTop ,
305+ ) ;
306+ if ( nextScrollTop !== scroll . scrollTop ) {
307+ scroll . scrollTop = nextScrollTop ;
308+ syncScrollViewport ( scroll ) ;
309+ }
310+ }
311+ previousLayoutRef . current = displayLayout . rowGeometry ;
312+ } , [ displayLayout . rowGeometry , sessionEpoch , syncScrollViewport ] ) ;
267313 const selectedKeyframes = usePlayerStore ( ( s ) => s . selectedKeyframes ) ;
268314 const toggleSelectedKeyframe = usePlayerStore ( ( s ) => s . toggleSelectedKeyframe ) ;
269315 const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
@@ -286,7 +332,7 @@ export const Timeline = memo(function Timeline({
286332 zoomModeRef,
287333 manualZoomPercentRef,
288334 } = useTimelineGeometry ( {
289- viewportWidth,
335+ viewportWidth : viewport . clientWidth ,
290336 effectiveDuration,
291337 zoomMode,
292338 manualZoomPercent,
@@ -369,7 +415,7 @@ export const Timeline = memo(function Timeline({
369415 setShowPopover,
370416 elementsRef : expandedElementsRef ,
371417 trackOrderRef,
372- rowHeightsRef ,
418+ rowGeometryRef ,
373419 onSelectElement,
374420 contentOrigin,
375421 } ) ;
@@ -403,6 +449,7 @@ export const Timeline = memo(function Timeline({
403449 < div
404450 ref = { setContainerRef }
405451 aria-label = "Timeline"
452+ data-timeline-element-count = { expandedElements . length }
406453 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" } ` }
407454 onMouseMove = { updateRazorGuide }
408455 onMouseLeave = { clearRazorGuide }
@@ -414,10 +461,12 @@ export const Timeline = memo(function Timeline({
414461 >
415462 < div
416463 ref = { setScrollRef }
464+ data-timeline-scroll-viewport
417465 tabIndex = { - 1 }
418466 className = { `${ zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto" } overflow-y-auto h-full outline-none` }
419467 onScroll = { ( e ) => {
420468 lastScrollLeftRef . current = e . currentTarget . scrollLeft ; // restored across post-edit reload
469+ syncScrollViewport ( e . currentTarget , true ) ;
421470 } }
422471 onDragOver = { handleAssetDragOver }
423472 onDragLeave = { ( ) => clearDropPreview ( ) }
0 commit comments