1- import { useRef , useMemo , useCallback , useState , useEffect , memo } from "react" ;
1+ import { useRef , useMemo , useCallback , useState , memo } from "react" ;
22import { useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis" ;
33import { isMusicTrack } from "../../utils/timelineInspector" ;
44import { remapBeatAnalysisToComposition } from "../../utils/beatEditActions" ;
55import { usePlayerStore , type TimelineElement } from "../store/playerStore" ;
66import { useExpandedTimelineElements } from "../hooks/useExpandedTimelineElements" ;
7- import { useMountEffect } from "../../hooks/useMountEffect" ;
87import { defaultTimelineTheme } from "./timelineTheme" ;
98import { useTimelineRangeSelection } from "./useTimelineRangeSelection" ;
109import { useTimelinePlayhead } from "./useTimelinePlayhead" ;
@@ -16,14 +15,12 @@ import { TimelineCanvas } from "./TimelineCanvas";
1615import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu" ;
1716import { useTimelineClipDrag } from "./useTimelineClipDrag" ;
1817import { TimelineOverlays } from "./TimelineOverlays" ;
19- import { animationContributesLane } from "./TimelinePropertyLanes" ;
2018import { useTimelineEditPinning } from "./useTimelineEditPinning" ;
2119import { useTimelineStackingSync } from "./useTimelineStackingSync" ;
2220import { useTimelineGeometry } from "./useTimelineGeometry" ;
2321import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips" ;
24- import { GUTTER , LABEL_COL_W , TRACKS_LEFT_PAD , generateTicks } from "./timelineLayout" ;
22+ import { GUTTER , LABEL_COL_W , TRACKS_LEFT_PAD } from "./timelineLayout" ;
2523import { useTimelineScrollViewport } from "./useTimelineScrollViewport" ;
26- import { STUDIO_PREVIEW_FPS } from "../lib/time" ;
2724import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks" ;
2825import type { TimelineProps } from "./TimelineTypes" ;
2926import {
@@ -37,6 +34,14 @@ import { useTrackGapMenu } from "./useTrackGapMenu";
3734import { useTimelineGapHighlights } from "./useTimelineGapHighlights" ;
3835import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext" ;
3936import { TimelineRazorGuide , useTimelineRazorInteraction } from "./TimelineRazorInteraction" ;
37+ import {
38+ getEffectiveTimelineDuration ,
39+ getTimelinePreviewElement ,
40+ hasKeyframedTimelineClips ,
41+ } from "./timelineViewModel" ;
42+ import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle" ;
43+ import { useTimelineShiftModifier } from "./useTimelineShiftModifier" ;
44+ import { useTimelineTicks } from "./useTimelineTicks" ;
4045
4146// Re-export pure utilities so existing imports from "./Timeline" still resolve.
4247export {
@@ -118,13 +123,7 @@ export const Timeline = memo(function Timeline({
118123 // Label mode = comp has keyframed clips (not just when expanded): keeps the layer
119124 // disclosure + property column visible and reserves a GUTTER before 0s (Figma).
120125 const hasKeyframedClips = useMemo (
121- ( ) =>
122- Array . from ( gsapAnimations . values ( ) ) . some ( ( list ) =>
123- // Same lane-contribution predicate the layout uses: real keyframes OR a
124- // synthesizable flat tween. Checking animation.keyframes alone left a
125- // flat-tween-only comp without its reserved label column.
126- list . some ( ( animation ) => animationContributesLane ( animation ) ) ,
127- ) ,
126+ ( ) => hasKeyframedTimelineClips ( gsapAnimations ) ,
128127 [ gsapAnimations ] ,
129128 ) ;
130129 const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips ;
@@ -143,20 +142,7 @@ export const Timeline = memo(function Timeline({
143142 const activeTool = usePlayerStore ( ( s ) => s . activeTool ) ;
144143 const [ hoveredClip , setHoveredClip ] = useState < string | null > ( null ) ;
145144 const isDragging = useRef ( false ) ;
146- const [ shiftHeld , setShiftHeld ] = useState ( false ) ;
147-
148- useMountEffect ( ( ) => {
149- const key = ( e : KeyboardEvent ) => e . key === "Shift" && setShiftHeld ( e . type === "keydown" ) ;
150- const blur = ( ) => setShiftHeld ( false ) ;
151- window . addEventListener ( "keydown" , key ) ;
152- window . addEventListener ( "keyup" , key ) ;
153- window . addEventListener ( "blur" , blur ) ;
154- return ( ) => {
155- window . removeEventListener ( "keydown" , key ) ;
156- window . removeEventListener ( "keyup" , key ) ;
157- window . removeEventListener ( "blur" , blur ) ;
158- } ;
159- } ) ;
145+ const shiftHeld = useTimelineShiftModifier ( ) ;
160146
161147 const [ showPopover , setShowPopover ] = useState ( false ) ;
162148 const [ kfContextMenu , setKfContextMenu ] = useState < KeyframeDiamondContextMenuState | null > ( null ) ;
@@ -173,12 +159,10 @@ export const Timeline = memo(function Timeline({
173159 // Last horizontal scroll offset, restored across the post-edit iframe reload (pinned zoom).
174160 const lastScrollLeftRef = useRef ( 0 ) ;
175161
176- const effectiveDuration = useMemo ( ( ) => {
177- const safeDur = Number . isFinite ( duration ) ? duration : 0 ;
178- if ( rawElements . length === 0 ) return safeDur ;
179- const result = Math . max ( safeDur , ...rawElements . map ( ( el ) => el . start + el . duration ) ) ;
180- return Number . isFinite ( result ) ? result : safeDur ;
181- } , [ rawElements , duration ] ) ;
162+ const effectiveDuration = useMemo (
163+ ( ) => getEffectiveTimelineDuration ( duration , rawElements ) ,
164+ [ duration , rawElements ] ,
165+ ) ;
182166
183167 const keyframeCache = usePlayerStore ( ( s ) => s . keyframeCache ) ;
184168 useAutoExpandKeyframedClips ( gsapAnimations ) ;
@@ -296,14 +280,6 @@ export const Timeline = memo(function Timeline({
296280 toggleSelectedKeyframe,
297281 } ) ;
298282
299- const selectedElement = useMemo (
300- ( ) =>
301- expandedElements . find ( ( element ) => ( element . key ?? element . id ) === selectedElementId ) ?? null ,
302- [ expandedElements , selectedElementId ] ,
303- ) ;
304- const selectedElementRef = useRef < TimelineElement | null > ( selectedElement ) ;
305- selectedElementRef . current = selectedElement ;
306-
307283 const {
308284 pps,
309285 fitPps,
@@ -402,41 +378,15 @@ export const Timeline = memo(function Timeline({
402378 } ) ;
403379 setRangeSelectionRef . current = setRangeSelection ; // stable ref consumed by useTimelineClipDrag
404380
405- const prevSelectedRef = useRef ( selectedElementRef . current ) ;
406- // eslint-disable-next-line no-restricted-syntax, react-hooks/exhaustive-deps
407- useEffect ( ( ) => {
408- const prev = prevSelectedRef . current ;
409- const curr = selectedElementRef . current ;
410- prevSelectedRef . current = curr ;
411- if ( prev && ! curr ) {
412- setShowPopover ( false ) ;
413- setRangeSelection ( null ) ;
414- }
415- } ) ;
416-
417- // Frame display mode labels ruler ticks as frame numbers — pass the fps so ticks snap to frames.
418- const tickFps = timeDisplayMode === "frame" ? STUDIO_PREVIEW_FPS : undefined ;
419- const { major, minor } = useMemo (
420- ( ) => generateTicks ( displayDuration , pps , tickFps ) ,
421- [ displayDuration , pps , tickFps ] ,
381+ useTimelineSelectionLifecycle ( expandedElements , selectedElementId , setShowPopover , ( ) =>
382+ setRangeSelection ( null ) ,
422383 ) ;
384+
385+ const { major, minor } = useTimelineTicks ( displayDuration , pps , timeDisplayMode ) ;
423386 const majorTickInterval = major . length >= 2 ? major [ 1 ] - major [ 0 ] : effectiveDuration ;
424387
425388 const getPreviewElement = useCallback (
426- ( element : TimelineElement ) : TimelineElement => {
427- if (
428- resizingClip &&
429- ( resizingClip . element . key ?? resizingClip . element . id ) === ( element . key ?? element . id )
430- ) {
431- return {
432- ...element ,
433- start : resizingClip . previewStart ,
434- duration : resizingClip . previewDuration ,
435- playbackStart : resizingClip . previewPlaybackStart ,
436- } ;
437- }
438- return element ;
439- } ,
389+ ( element : TimelineElement ) : TimelineElement => getTimelinePreviewElement ( element , resizingClip ) ,
440390 [ resizingClip ] ,
441391 ) ;
442392
0 commit comments