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,
@@ -107,7 +114,7 @@ export const Timeline = memo(function Timeline({
107114 const rawElements = usePlayerStore ( ( s ) => s . elements ) ;
108115 const expandedElements = useExpandedTimelineElements ( ) ;
109116 const beatAnalysis = usePlayerStore ( ( s ) => s . beatAnalysis ) ;
110- const musicElement = usePlayerStore ( ( s ) => s . elements . find ( isMusicTrack ) ?? null ) ;
117+ const musicElement = usePlayerStore ( ( s ) => getTimelineElementIndexes ( s . elements ) . musicElement ) ;
111118 const beatEdits = usePlayerStore ( ( s ) => s . beatEdits ) ;
112119 const adjustedBeatAnalysis = useMemo (
113120 ( ) => remapBeatAnalysisToComposition ( beatAnalysis , musicElement , beatEdits ) ,
@@ -165,8 +172,20 @@ export const Timeline = memo(function Timeline({
165172
166173 const keyframeCache = usePlayerStore ( ( s ) => s . keyframeCache ) ;
167174 useAutoExpandKeyframedClips ( gsapAnimations ) ;
168- const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
169- useTimelineTrackLayout ( expandedElements , gsapAnimations , selectedElementId , selectedElementIds ) ;
175+ const {
176+ tracks,
177+ trackStyles,
178+ trackOrder,
179+ trackOrderRef,
180+ laneCounts,
181+ rowGeometry,
182+ rowGeometryRef,
183+ } = useTimelineTrackLayout (
184+ expandedElements ,
185+ gsapAnimations ,
186+ selectedElementId ,
187+ selectedElementIds ,
188+ ) ;
170189 const expandedElementsRef = useRef ( expandedElements ) ;
171190 expandedElementsRef . current = expandedElements ;
172191
@@ -233,7 +252,7 @@ export const Timeline = memo(function Timeline({
233252 ppsRef,
234253 durationRef,
235254 trackOrderRef,
236- rowHeightsRef ,
255+ rowGeometryRef ,
237256 onMoveElement : pinnedOnMoveElement ,
238257 onMoveElements : pinnedOnMoveElements ,
239258 onResizeElement : pinnedOnResizeElement ,
@@ -252,20 +271,47 @@ export const Timeline = memo(function Timeline({
252271 ppsRef,
253272 durationRef,
254273 trackOrderRef,
255- rowHeightsRef ,
274+ rowGeometryRef ,
256275 contentOrigin,
257276 onFileDrop : pinnedOnFileDrop ,
258277 onAssetDrop : pinnedOnAssetDrop ,
259278 onBlockDrop : pinnedOnBlockDrop ,
260279 onCompositionDrop : pinnedOnCompositionDrop ,
261280 } ) ;
262281
263- const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowHeights ) ;
264- const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport ( scrollRef , [
265- timelineReady ,
266- expandedElements . length ,
267- displayLayout . totalH ,
268- ] ) ;
282+ const displayLayout = useTimelineDisplayLayout ( draggedClip , trackOrder , rowGeometry ) ;
283+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
284+ useTimelineScrollViewport ( scrollRef , [
285+ timelineReady ,
286+ expandedElements . length ,
287+ displayLayout . totalH ,
288+ ] ) ;
289+ const previousLayoutRef = useRef ( displayLayout . rowGeometry ) ;
290+ const previousSessionEpochRef = useRef ( sessionEpoch ) ;
291+ useLayoutEffect ( ( ) => {
292+ const scroll = scrollRef . current ;
293+ const previousGeometry = previousLayoutRef . current ;
294+ if ( previousSessionEpochRef . current !== sessionEpoch ) {
295+ previousSessionEpochRef . current = sessionEpoch ;
296+ lastScrollLeftRef . current = 0 ;
297+ if ( scroll ) {
298+ scroll . scrollLeft = 0 ;
299+ scroll . scrollTop = 0 ;
300+ syncScrollViewport ( scroll ) ;
301+ }
302+ } else if ( scroll && previousGeometry !== displayLayout . rowGeometry ) {
303+ const nextScrollTop = getTimelineScrollTopForGeometryChange (
304+ previousGeometry ,
305+ displayLayout . rowGeometry ,
306+ scroll . scrollTop ,
307+ ) ;
308+ if ( nextScrollTop !== scroll . scrollTop ) {
309+ scroll . scrollTop = nextScrollTop ;
310+ syncScrollViewport ( scroll ) ;
311+ }
312+ }
313+ previousLayoutRef . current = displayLayout . rowGeometry ;
314+ } , [ displayLayout . rowGeometry , sessionEpoch , syncScrollViewport ] ) ;
269315 const selectedKeyframes = usePlayerStore ( ( s ) => s . selectedKeyframes ) ;
270316 const toggleSelectedKeyframe = usePlayerStore ( ( s ) => s . toggleSelectedKeyframe ) ;
271317 const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
@@ -288,7 +334,7 @@ export const Timeline = memo(function Timeline({
288334 zoomModeRef,
289335 manualZoomPercentRef,
290336 } = useTimelineGeometry ( {
291- viewportWidth,
337+ viewportWidth : viewport . clientWidth ,
292338 effectiveDuration,
293339 zoomMode,
294340 manualZoomPercent,
@@ -371,7 +417,7 @@ export const Timeline = memo(function Timeline({
371417 setShowPopover,
372418 elementsRef : expandedElementsRef ,
373419 trackOrderRef,
374- rowHeightsRef ,
420+ rowGeometryRef ,
375421 onSelectElement,
376422 contentOrigin,
377423 } ) ;
@@ -405,6 +451,7 @@ export const Timeline = memo(function Timeline({
405451 < div
406452 ref = { setContainerRef }
407453 aria-label = "Timeline"
454+ data-timeline-element-count = { expandedElements . length }
408455 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" } ` }
409456 onMouseMove = { updateRazorGuide }
410457 onMouseLeave = { clearRazorGuide }
@@ -416,10 +463,12 @@ export const Timeline = memo(function Timeline({
416463 >
417464 < div
418465 ref = { setScrollRef }
466+ data-timeline-scroll-viewport
419467 tabIndex = { - 1 }
420468 className = { `${ zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto" } overflow-y-auto h-full outline-none` }
421469 onScroll = { ( e ) => {
422470 lastScrollLeftRef . current = e . currentTarget . scrollLeft ; // restored across post-edit reload
471+ syncScrollViewport ( e . currentTarget , true ) ;
423472 } }
424473 onDragOver = { handleAssetDragOver }
425474 onDragLeave = { ( ) => clearDropPreview ( ) }
0 commit comments