@hyperframes/studio 0.8.55 → 0.8.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/assets/hyperframes-player-c23wnIfc.js +464 -0
  2. package/dist/assets/{index-DrHQAuGO.css → index-DKJpHYYd.css} +1 -1
  3. package/dist/assets/{index-BEIRytvV.js → index-DgkAzx3n.js} +1 -1
  4. package/dist/assets/{index-DaNpNFJC.js → index-IDNr3X1H.js} +1 -1
  5. package/dist/assets/{index-TcBZyzNA.js → index-gbOCDCuZ.js} +202 -202
  6. package/dist/index.d.ts +111 -17
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +21534 -20683
  9. package/dist/index.js.map +1 -1
  10. package/dist/styles.css +1 -1
  11. package/package.json +7 -7
  12. package/src/App.tsx +18 -1
  13. package/src/components/EditorShell.readOnlyHostMount.test.tsx +116 -0
  14. package/src/components/EditorShell.selectionSync.test.tsx +57 -67
  15. package/src/components/EditorShell.tsx +62 -51
  16. package/src/components/EditorShellTestMocks.ts +46 -0
  17. package/src/components/ProjectUnreachableBanner.test.tsx +113 -0
  18. package/src/components/ProjectUnreachableBanner.tsx +89 -0
  19. package/src/components/editor/DomEditOverlay.tsx +19 -55
  20. package/src/components/editor/DomEditOverlayHover.test.tsx +1 -15
  21. package/src/components/editor/DomEditSelectionChrome.tsx +11 -9
  22. package/src/components/editor/GridOverlay.tsx +12 -23
  23. package/src/components/editor/MotionPathOverlay.test.tsx +66 -0
  24. package/src/components/editor/MotionPathOverlay.tsx +3 -1
  25. package/src/components/editor/PreviewGuides.test.tsx +6 -1
  26. package/src/components/editor/PreviewGuides.tsx +8 -10
  27. package/src/components/editor/PreviewOverlayProvider.test.tsx +90 -0
  28. package/src/components/editor/PreviewOverlayProvider.tsx +86 -0
  29. package/src/components/editor/PropertyPanel.deselect.test.tsx +71 -0
  30. package/src/components/editor/PropertyPanel.tsx +4 -3
  31. package/src/components/editor/SnapToolbar.test.tsx +26 -12
  32. package/src/components/editor/SnapToolbar.tsx +30 -59
  33. package/src/components/editor/domEditOverlayTestMocks.ts +16 -0
  34. package/src/components/editor/previewReadOnly.overlay.test.tsx +260 -0
  35. package/src/components/editor/previewReadOnlyContext.tsx +59 -0
  36. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +9 -0
  37. package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +2 -2
  38. package/src/components/editor/propertyPanelFlatMediaSection.tsx +46 -12
  39. package/src/components/editor/propertyPanelFlatPrimitives.tsx +4 -1
  40. package/src/components/editor/propertyPanelInputCoverage.test.tsx +3 -13
  41. package/src/components/editor/useDomEditNudge.ts +9 -0
  42. package/src/components/editor/useDomEditOverlayGestures.ts +4 -0
  43. package/src/components/editor/useOffCanvasIndicators.ts +50 -0
  44. package/src/components/editor/usePreviewCompositionRect.test.tsx +19 -13
  45. package/src/components/editor/usePreviewCompositionRect.ts +12 -12
  46. package/src/components/editor/useVolumeAutomation.test.tsx +68 -0
  47. package/src/components/editor/useVolumeAutomation.ts +57 -16
  48. package/src/components/nle/NLEContext.tsx +40 -4
  49. package/src/components/nle/NLEPreview.test.ts +66 -1
  50. package/src/components/nle/NLEPreview.tsx +88 -44
  51. package/src/components/nle/PreviewOverlays.topologyLens.test.tsx +4 -35
  52. package/src/components/nle/PreviewOverlays.tsx +19 -24
  53. package/src/components/nle/PreviewOverlaysTestMocks.ts +40 -0
  54. package/src/components/nle/PreviewPane.tsx +17 -2
  55. package/src/components/nle/TimelinePane.tsx +5 -1
  56. package/src/components/nle/previewReadOnly.test.tsx +129 -0
  57. package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
  58. package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -6
  59. package/src/components/nle/usePreviewBlockDrop.ts +3 -1
  60. package/src/hooks/appHotkeysDispatch.ts +16 -1
  61. package/src/hooks/newTweenTargetHooks.test.tsx +1 -0
  62. package/src/hooks/timelineAudioGroupCreate.ts +3 -4
  63. package/src/hooks/timelineTrackVisibility.ts +7 -8
  64. package/src/hooks/useAppHotkeys.test.ts +71 -3
  65. package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
  66. package/src/hooks/useAppHotkeys.ts +4 -0
  67. package/src/hooks/useCompositionDimensions.test.tsx +38 -0
  68. package/src/hooks/useCompositionDimensions.ts +17 -1
  69. package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
  70. package/src/hooks/useDomEditAttributeCommits.ts +14 -3
  71. package/src/hooks/useDomEditCommits.test.tsx +1 -0
  72. package/src/hooks/useDomEditCommits.ts +3 -0
  73. package/src/hooks/useDomEditSession.test.tsx +1 -0
  74. package/src/hooks/useDomEditSession.ts +7 -4
  75. package/src/hooks/useDomEditWiring.ts +40 -40
  76. package/src/hooks/useDomGeometryCommits.test.tsx +85 -2
  77. package/src/hooks/useDomGeometryCommits.ts +26 -20
  78. package/src/hooks/useFileTree.test.tsx +56 -0
  79. package/src/hooks/useFileTree.ts +8 -1
  80. package/src/hooks/useGestureCommit.test.tsx +29 -0
  81. package/src/hooks/useGestureCommit.ts +23 -0
  82. package/src/hooks/useInlineTextEdit.test.tsx +23 -2
  83. package/src/hooks/useInlineTextEdit.ts +11 -2
  84. package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
  85. package/src/hooks/useSdkSession.ts +46 -10
  86. package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
  87. package/src/hooks/useServerConnection.ts +81 -8
  88. package/src/hooks/useTimelineAssetDropOps.test.tsx +3 -4
  89. package/src/hooks/useTimelineAssetDropOps.ts +1 -6
  90. package/src/hooks/useTimelineEditing.test.tsx +10 -7
  91. package/src/hooks/useTimelineEditing.ts +2 -7
  92. package/src/index.ts +12 -0
  93. package/src/player/components/Player.test.ts +141 -13
  94. package/src/player/components/Player.tsx +58 -2
  95. package/src/player/components/ShortcutsPanel.test.tsx +4 -10
  96. package/src/player/components/Timeline.tsx +23 -573
  97. package/src/player/components/TimelineCanvas.tsx +9 -30
  98. package/src/player/components/TimelineClip.test.tsx +52 -0
  99. package/src/player/components/TimelineClip.tsx +13 -2
  100. package/src/player/components/TimelineOverlays.test.ts +67 -32
  101. package/src/player/components/TimelineOverlays.tsx +46 -107
  102. package/src/player/components/TimelineProvider.tsx +191 -0
  103. package/src/player/components/TimelineTrackHeader.test.tsx +26 -5
  104. package/src/player/components/TimelineTrackHeader.tsx +1 -1
  105. package/src/player/components/testRootHarness.ts +18 -0
  106. package/src/player/components/timelineMotionStyles.test.ts +3 -3
  107. package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
  108. package/src/player/components/timelineProviderStateBuilders.ts +83 -0
  109. package/src/player/components/timelineTheme.test.ts +4 -4
  110. package/src/player/components/timelineTheme.ts +34 -30
  111. package/src/player/components/timelineTrackDisplay.ts +1 -4
  112. package/src/player/components/useTimelineClipContextMenu.ts +1 -1
  113. package/src/player/components/useTimelineOverlaysState.ts +100 -0
  114. package/src/player/components/useTimelineProviderState.tsx +583 -0
  115. package/src/player/components/useTimelineTrackDerivations.ts +2 -2
  116. package/src/player/hooks/playbackAdapterResolution.ts +98 -0
  117. package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
  118. package/src/player/hooks/timelineSyncHydration.test.ts +0 -18
  119. package/src/player/hooks/timelineSyncHydration.ts +0 -23
  120. package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
  121. package/src/player/hooks/useShadowPreviewReload.ts +183 -0
  122. package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
  123. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
  124. package/src/player/hooks/useTimelinePlayer.ts +65 -99
  125. package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -91
  126. package/src/player/store/playerStore.ts +0 -4
  127. package/src/player/store/timelineElement.ts +2 -4
  128. package/src/player/store/timelineResetState.ts +0 -1
  129. package/src/styles/hex-baseline.json +3 -4
  130. package/src/styles/studio.css +7 -7
  131. package/src/styles/theme.css +29 -0
  132. package/src/telemetry/events.test.ts +18 -0
  133. package/src/telemetry/events.ts +10 -0
  134. package/src/telemetry/system.test.ts +49 -0
  135. package/src/telemetry/system.ts +6 -0
  136. package/src/telemetry/tabId.test.ts +43 -0
  137. package/src/telemetry/tabId.ts +44 -0
  138. package/src/utils/previewCompositionSize.ts +26 -0
  139. package/src/utils/reloadDebug.ts +2 -4
  140. package/src/utils/sourcePatcher.ts +1 -1
  141. package/src/utils/studioTelemetry.test.ts +14 -0
  142. package/src/utils/studioTelemetry.ts +4 -0
  143. package/dist/assets/hyperframes-player-BjAGc2ID.js +0 -464
  144. package/src/player/hooks/useTimelineRowElements.test.ts +0 -28
  145. package/src/player/hooks/useTimelineRowElements.ts +0 -17
@@ -1,51 +1,11 @@
1
- import { useRef, useMemo, useCallback, useState, memo } from "react";
2
- import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
3
- import { usePlayerStore, type TimelineElement } from "../store/playerStore";
4
- import { useTimelineRowElements } from "../hooks/useTimelineRowElements";
5
- import { defaultTimelineTheme } from "./timelineTheme";
6
- import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
7
- import { usePublishRangeSelection } from "./usePublishRangeSelection";
8
- import { useTimelinePlayhead } from "./useTimelinePlayhead";
9
- import { useTimelineZoom } from "./useTimelineZoom";
10
- import { useTimelineAssetDrop } from "./timelineDragDrop";
1
+ import { memo } from "react";
2
+ import type { TimelineProps } from "./TimelineTypes";
11
3
  import { TimelineEmptyState } from "./TimelineEmptyState";
12
4
  import { TimelineCanvas } from "./TimelineCanvas";
13
- import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
14
- import { useTimelineClipDrag } from "./useTimelineClipDrag";
15
- import { TimelineOverlays, type ClipContextMenuState } from "./TimelineOverlays";
16
- import { useTimelineEditPinning } from "./useTimelineEditPinning";
17
- import { useTimelineStackingSync } from "./useTimelineStackingSync";
18
- import { useTimelineGeometry } from "./useTimelineGeometry";
19
- import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
20
- import { GUTTER, LABEL_COL_W, TRACKS_LEFT_PAD } from "./timelineLayout";
21
- import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
22
- import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
23
- import type { TimelineProps } from "./TimelineTypes";
24
- import {
25
- getTrackStyle,
26
- useTimelineDisplayLayout,
27
- useTimelineTrackLayout,
28
- } from "./useTimelineTrackLayout";
29
- import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
30
- import { useTrackGapMenu } from "./useTrackGapMenu";
31
- import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
32
- import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
33
- import { useTimelinePerformanceTelemetry } from "./useTimelinePerformanceTelemetry";
34
- import {
35
- getEffectiveTimelineDuration,
36
- getTimelinePreviewElement,
37
- timelineNeedsLabelColumn,
38
- } from "./timelineViewModel";
39
- import { useTimelineSelectionLifecycle } from "./useTimelineSelectionLifecycle";
40
- import { useTimelineShiftModifier } from "./useTimelineShiftModifier";
41
- import { useTimelineTicks } from "./useTimelineTicks";
42
- import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
43
- import { useTimelineClipRenderWindow } from "./useTimelineClipRenderWindow";
44
- import { useTimelineActiveClips } from "./useTimelineActiveClips";
45
- import { useTimelineLaneMoveRefresh } from "./useTimelineLaneMoveRefresh";
46
- import { useTimelineLogicalFocus } from "./useTimelineLogicalFocus";
47
- import { useClipContextMenu } from "./useTimelineClipContextMenu";
5
+ import { TimelineOverlays } from "./TimelineOverlays";
6
+ import { TimelineProvider, useTimelineContext } from "./TimelineProvider";
48
7
 
8
+ export * from "./TimelineProvider";
49
9
  export {
50
10
  shouldAutoScrollTimeline,
51
11
  getTimelineScrollLeftForZoomTransition,
@@ -59,542 +19,32 @@ export {
59
19
  getDefaultDroppedTrack,
60
20
  } from "./timelineLayout";
61
21
  export { formatTimelineTickLabel, generateTicks } from "./timelineRulerGeometry";
62
-
63
22
  export {
64
23
  getTimelineScrollTopForGeometryChange,
65
24
  getTimelineVisibleTimeRange,
66
25
  } from "./timelineViewportGeometry";
67
- export const Timeline = memo(function Timeline({
68
- onSeek,
69
- onDrillDown,
70
- renderClipContent,
71
- renderClipOverlay,
72
- onFileDrop,
73
- onAssetDrop,
74
- onBlockDrop,
75
- onCompositionDrop,
76
- onDeleteElement: _onDeleteElement,
77
- onMoveElement: onMoveElementOverride,
78
- onMoveElements: onMoveElementsOverride,
79
- onResizeElement: onResizeElementOverride,
80
- onResizeElements: onResizeElementsOverride,
81
- onBlockedEditAttempt: onBlockedEditAttemptOverride,
82
- onSplitElement: onSplitElementOverride,
83
- onSelectElement,
84
- onRangeSelect,
85
- onCopyClip,
86
- onPasteClip,
87
- onDuplicateClip,
88
- canPasteClip,
89
- theme: themeOverrides,
90
- sessionEpoch = 0,
91
- }: TimelineProps = {}) {
92
- const {
93
- onMoveElement,
94
- onMoveElements,
95
- onResizeElement,
96
- onResizeElements,
97
- onBlockedEditAttempt,
98
- onSplitElement,
99
- onRazorSplitAll,
100
- onDeleteKeyframe,
101
- onDeleteAllKeyframes,
102
- onMoveKeyframeToPlayhead,
103
- onMoveKeyframe,
104
- } = useResolvedTimelineEditCallbacks({
105
- onMoveElement: onMoveElementOverride,
106
- onMoveElements: onMoveElementsOverride,
107
- onResizeElement: onResizeElementOverride,
108
- onResizeElements: onResizeElementsOverride,
109
- onBlockedEditAttempt: onBlockedEditAttemptOverride,
110
- onSplitElement: onSplitElementOverride,
111
- });
112
- const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
113
- const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
114
- useMusicBeatAnalysis();
115
- const rawElements = usePlayerStore((s) => s.elements);
116
- const expandedElements = useTimelineRowElements();
117
- const adjustedBeatAnalysis = useAdjustedBeatAnalysis();
118
- const duration = usePlayerStore((s) => s.duration);
119
- const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
120
- const timelineReady = usePlayerStore((s) => s.timelineReady);
121
- const selectedElementId = usePlayerStore((s) => s.selectedElementId);
122
- const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
123
- const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
124
- const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
125
- const labelMode = useMemo(
126
- () => timelineNeedsLabelColumn(gsapAnimations, expandedElements),
127
- [gsapAnimations, expandedElements],
128
- );
129
- // The label column provides pre-t=0 space; otherwise keep TRACKS_LEFT_PAD after the gutter.
130
- const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
131
- const contentGutter = labelMode ? GUTTER : 0;
132
- const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
133
- const currentTime = usePlayerStore((s) => s.currentTime);
134
- const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
135
- const playheadRef = useRef<HTMLDivElement>(null);
136
- const containerRef = useRef<HTMLDivElement>(null);
137
- const scrollRef = useRef<HTMLDivElement>(null);
138
- const activeTool = usePlayerStore((s) => s.activeTool);
139
- const [hoveredClip, setHoveredClip] = useState<string | null>(null);
140
- const isDragging = useRef(false);
141
- const shiftHeld = useTimelineShiftModifier();
142
- const [showPopover, setShowPopover] = useState(false);
143
- const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
144
- const [clipContextMenu, setClipContextMenu] = useState<ClipContextMenuState | null>(null);
145
- const setContainerRef = useCallback((el: HTMLDivElement | null) => {
146
- containerRef.current = el;
147
- }, []);
148
- const lastScrollLeftRef = useRef(0);
149
- const effectiveDuration = useMemo(
150
- () => getEffectiveTimelineDuration(duration, rawElements),
151
- [duration, rawElements],
152
- );
153
- const keyframeCache = usePlayerStore((s) => s.keyframeCache);
154
- useAutoExpandKeyframedClips(gsapAnimations);
155
- const {
156
- tracks,
157
- trackStyles,
158
- trackOrder,
159
- trackOrderRef,
160
- laneCounts,
161
- rowGeometry,
162
- rowGeometryRef,
163
- groups,
164
- trackGroupOf,
165
- } = useTimelineTrackLayout(
166
- expandedElements,
167
- gsapAnimations,
168
- selectedElementId,
169
- selectedElementIds,
170
- );
171
- const expandedElementsRef = useRef(expandedElements);
172
- expandedElementsRef.current = expandedElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
173
- const ppsRef = useRef(100);
174
- const durationRef = useRef(effectiveDuration);
175
- durationRef.current = effectiveDuration; // oxlint-disable-line react/refs -- event handlers read the latest duration
176
- const fitPpsRef = useRef(100);
177
- const {
178
- pinZoomBeforeEdit,
179
- setRangeSelectionRef,
180
- pinnedOnMoveElement,
181
- pinnedOnMoveElements,
182
- pinnedOnResizeElement,
183
- pinnedOnResizeElements,
184
- pinnedOnFileDrop,
185
- pinnedOnAssetDrop,
186
- pinnedOnBlockDrop,
187
- pinnedOnCompositionDrop,
188
- } = useTimelineEditPinning({
189
- ppsRef,
190
- fitPpsRef,
191
- onMoveElement,
192
- onMoveElements,
193
- onResizeElement,
194
- onResizeElements,
195
- onFileDrop,
196
- onAssetDrop,
197
- onBlockDrop,
198
- onCompositionDrop,
199
- });
200
- const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
201
- expandedElementsRef,
202
- });
203
- const {
204
- gapMenuModel,
205
- gapHighlight,
206
- setHoveredGapAction,
207
- openGapMenu,
208
- dismissGapMenu,
209
- closeTrackGap,
210
- closeAllTrackGaps,
211
- } = useTrackGapMenu({
212
- tracks,
213
- expandedElementsRef,
214
- trackOrderRef,
215
- onMoveElement: pinnedOnMoveElement,
216
- onMoveElements: pinnedOnMoveElements,
217
- });
218
- const onContextMenuClip = useClipContextMenu(onSelectElement, dismissGapMenu, setClipContextMenu);
219
- const {
220
- draggedClip,
221
- setDraggedClip,
222
- resizingClip,
223
- setResizingClip,
224
- blockedClipRef,
225
- suppressClickRef,
226
- } = useTimelineClipDrag({
227
- scrollRef,
228
- ppsRef,
229
- durationRef,
230
- trackOrderRef,
231
- rowGeometryRef,
232
- onMoveElement: pinnedOnMoveElement,
233
- onMoveElements: pinnedOnMoveElements,
234
- onResizeElement: pinnedOnResizeElement,
235
- onResizeElements: pinnedOnResizeElements,
236
- onBlockedEditAttempt,
237
- onSeek,
238
- setShowPopover,
239
- setRangeSelectionRef,
240
- readZIndex: zSyncEnabled ? readClipZIndex : undefined,
241
- onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
242
- refreshAfterLaneMove,
243
- sessionEpoch,
244
- });
245
- const assetDrop = useTimelineAssetDrop({
246
- scrollRef,
247
- ppsRef,
248
- trackOrderRef,
249
- rowGeometryRef,
250
- contentOrigin,
251
- onFileDrop: pinnedOnFileDrop,
252
- onAssetDrop: pinnedOnAssetDrop,
253
- onBlockDrop: pinnedOnBlockDrop,
254
- onCompositionDrop: pinnedOnCompositionDrop,
255
- sessionEpoch,
256
- });
257
- const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowGeometry);
258
- const resizingElementIds =
259
- resizingClip?.groupPreview?.map((change) => change.key) ??
260
- (resizingClip ? [getTimelineElementIdentity(resizingClip.element)] : undefined);
261
- const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
262
- totalClipCount: expandedElements.length,
263
- totalRowCount: displayLayout.displayTrackOrder.length,
264
- zoomMode,
265
- });
266
- const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
267
- useTimelineScrollViewport(scrollRef, [
268
- timelineReady,
269
- expandedElements.length,
270
- displayLayout.totalH,
271
- ]);
272
- const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef, manualZoomPercentRef } =
273
- useTimelineGeometry({
274
- viewportWidth: viewport.clientWidth,
275
- effectiveDuration,
276
- zoomMode,
277
- manualZoomPercent,
278
- ppsRef,
279
- fitPpsRef,
280
- draggedClip,
281
- resizingClip,
282
- expandedElements,
283
- isDragging,
284
- scrollRef,
285
- lastScrollLeftRef,
286
- contentOrigin,
287
- });
288
- const timelineFocus = useTimelineLogicalFocus({
289
- scrollRef,
290
- tracks,
291
- layout: displayLayout,
292
- laneCounts,
293
- selectedElementId,
294
- selectedElementIds,
295
- groups,
296
- trackGroupOf,
297
- gsapAnimations,
298
- elements: expandedElements,
299
- pixelsPerSecond: pps,
300
- contentOrigin,
301
- allowHorizontal: zoomMode === "manual",
302
- viewport,
303
- sessionEpoch,
304
- draggedRowKey: draggedClip?.started ? draggedClip.previewTrack : undefined,
305
- resizingElementIds,
306
- clipContextMenuRowKey: clipContextMenu?.element.track,
307
- keyframeContextMenuRowKey: kfContextMenu?.element.track,
308
- lastScrollLeftRef,
309
- syncScrollViewport,
310
- });
311
- const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
312
- const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
313
- const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
314
- useTimelineKeyframeHandlers({
315
- expandedElements,
316
- keyframeCache,
317
- onSelectElement,
318
- onSeek,
319
- setSelectedElementId,
320
- setKfContextMenu,
321
- toggleSelectedKeyframe,
322
- });
323
- const { clipIndex, renderTimeRange, visibleTimeRange, pinnedClipIdentities } =
324
- useTimelineClipRenderWindow({
325
- tracks,
326
- viewport,
327
- pixelsPerSecond: pps,
328
- contentOrigin,
329
- duration: displayDuration,
330
- selectedElementId: selectedElementId ?? undefined,
331
- draggedElementId: draggedClip ? getTimelineElementIdentity(draggedClip.element) : undefined,
332
- resizingElementIds,
333
- focusedElementId: timelineFocus.pinnedElementId,
334
- focusedEaseElementId: focusedEaseSegment?.elementId,
335
- clipContextMenuElementId: clipContextMenu
336
- ? getTimelineElementIdentity(clipContextMenu.element)
337
- : undefined,
338
- keyframeContextMenuElementId: kfContextMenu
339
- ? getTimelineElementIdentity(kfContextMenu.element)
340
- : undefined,
341
- });
342
- useTimelineActiveClips({
343
- scrollRef,
344
- currentTime,
345
- clipStateVersion: renderTimeRange,
346
- elementStateVersion: expandedElements,
347
- });
348
- const laneGapStrips = useTimelineGapHighlights({
349
- gapHighlight,
350
- tracks,
351
- selectedElementId,
352
- selectedElementIds,
353
- expandedElements,
354
- dragActive: draggedClip?.started === true || resizingClip != null,
355
- displayDuration,
356
- });
357
-
358
- const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
359
- playheadRef,
360
- scrollRef,
361
- ppsRef,
362
- durationRef,
363
- isDragging,
364
- currentTime,
365
- zoomMode,
366
- manualZoomPercent,
367
- zoomModeRef,
368
- manualZoomPercentRef,
369
- fitPps,
370
- fitPpsRef,
371
- effectiveDuration,
372
- pps,
373
- timelineReady,
374
- elementsLength: expandedElements.length,
375
- setZoomMode,
376
- setManualZoomPercent,
377
- onSeek,
378
- contentOrigin,
379
- });
380
- const { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer } =
381
- useTimelineRazorInteraction({
382
- active: activeTool === "razor",
383
- scrollRef,
384
- contentOrigin,
385
- pixelsPerSecond: pps,
386
- onSplitAll: onRazorSplitAll,
387
- });
388
-
389
- const {
390
- rangeSelection,
391
- setRangeSelection,
392
- shiftClickClipRef,
393
- marqueeRect,
394
- isScrubbing,
395
- handlePointerDown,
396
- handlePointerMove,
397
- handlePointerUp,
398
- handlePointerCancel,
399
- } = useTimelineRangeSelection({
400
- scrollRef,
401
- ppsRef,
402
- effectiveDuration,
403
- pps,
404
- onSeek,
405
- seekFromX,
406
- autoScrollDuringDrag,
407
- dragScrollRaf,
408
- isDragging,
409
- setShowPopover,
410
- elementsRef: expandedElementsRef,
411
- clipIndex,
412
- rowGeometryRef,
413
- onSelectElement,
414
- contentOrigin,
415
- sessionEpoch,
416
- });
417
- usePublishRangeSelection(rangeSelection, onRangeSelect);
418
- setRangeSelectionRef.current = setRangeSelection; // oxlint-disable-line react/refs -- stable ref consumed by useTimelineClipDrag
419
-
420
- useTimelineSelectionLifecycle(expandedElements, selectedElementId, setShowPopover, () =>
421
- setRangeSelection(null),
422
- );
423
-
424
- const { major, minor, majorTickInterval } = useTimelineTicks(
425
- displayDuration,
426
- pps,
427
- timeDisplayMode,
428
- timelineFocus.rowVirtualizationActive ? renderTimeRange : undefined,
429
- );
430
-
431
- const getPreviewElement = useCallback(
432
- (element: TimelineElement): TimelineElement => getTimelinePreviewElement(element, resizingClip),
433
- [resizingClip],
434
- );
435
26
 
436
- if (!timelineReady || expandedElements.length === 0) {
437
- return (
438
- <TimelineEmptyState
439
- isDragOver={assetDrop.isDragOver}
440
- onFileDrop={!!onFileDrop}
441
- onDragOver={assetDrop.handleAssetDragOver}
442
- onDragLeave={assetDrop.handleAssetDragLeave}
443
- onDrop={assetDrop.handleAssetDrop}
444
- />
445
- );
27
+ function TimelineView() {
28
+ const { state, meta } = useTimelineContext();
29
+ const { timelineReady, elements } = state;
30
+ if (!timelineReady || elements.length === 0) {
31
+ return <TimelineEmptyState {...meta.emptyState} />;
446
32
  }
447
-
448
33
  return (
449
- <div
450
- ref={setContainerRef}
451
- aria-label="Timeline track view"
452
- data-timeline-element-count={expandedElements.length}
453
- className={`relative border-t select-none h-full overflow-hidden ${assetDrop.isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
454
- onMouseMove={updateRazorGuide}
455
- onMouseLeave={clearRazorGuide}
456
- style={{
457
- touchAction: "pan-x pan-y",
458
- background: theme.shellBackground,
459
- borderColor: theme.shellBorder,
460
- }}
461
- >
462
- <div
463
- ref={setScrollRef}
464
- // Stable owner for gestures that must survive virtual row/clip unmounts.
465
- data-timeline-scroll-viewport
466
- data-timeline-auto-scroll-left-inset={labelMode ? LABEL_COL_W : 0}
467
- tabIndex={-1}
468
- className={`${zoomMode === "fit" ? "overflow-x-hidden" : "overflow-x-auto"} overflow-y-auto h-full outline-hidden`}
469
- onScroll={(e) => {
470
- lastScrollLeftRef.current = e.currentTarget.scrollLeft; // restored across post-edit reload
471
- recordTimelineScroll(e.currentTarget);
472
- syncScrollViewport(e.currentTarget, true);
473
- }}
474
- {...timelineFocus.timelineFocusProps}
475
- onDragOver={assetDrop.handleAssetDragOver}
476
- onDragLeave={assetDrop.handleAssetDragLeave}
477
- onDrop={assetDrop.handleAssetDrop}
478
- onPointerDown={(e) => {
479
- // Interactive controls own their clicks; scrubbing would preventDefault and eat them.
480
- if (e.target instanceof Element && e.target.closest("button, input, select, a")) return;
481
- if (splitAllAtPointer(e)) return;
482
- handlePointerDown(e);
483
- }}
484
- onPointerMove={handlePointerMove}
485
- onPointerUp={handlePointerUp}
486
- onPointerCancel={handlePointerCancel}
487
- onLostPointerCapture={handlePointerCancel}
488
- >
489
- <TimelineCanvas
490
- major={major}
491
- minor={minor}
492
- pps={pps}
493
- contentOrigin={contentOrigin}
494
- contentGutter={contentGutter}
495
- trackContentWidth={displayContentWidth}
496
- totalH={displayLayout.totalH}
497
- effectiveDuration={effectiveDuration}
498
- majorTickInterval={majorTickInterval}
499
- rangeSelection={rangeSelection}
500
- marqueeRect={marqueeRect}
501
- laneGapStrips={laneGapStrips}
502
- dropPreview={assetDrop.dropPreview}
503
- theme={theme}
504
- displayTrackOrder={displayLayout.displayTrackOrder}
505
- rowHeights={displayLayout.displayRowHeights}
506
- rowGeometry={displayLayout.rowGeometry}
507
- virtualRows={timelineFocus.virtualRows}
508
- logicalRows={timelineFocus.logicalRows}
509
- focusedTargetId={timelineFocus.focusedTargetId}
510
- rowsVirtualized={timelineFocus.rowVirtualizationActive}
511
- clipIndex={clipIndex}
512
- renderTimeRange={renderTimeRange}
513
- visibleTimeRange={visibleTimeRange}
514
- pinnedClipIdentities={pinnedClipIdentities}
515
- trackOrder={trackOrder}
516
- tracks={tracks}
517
- trackStyles={trackStyles}
518
- groups={groups}
519
- laneCounts={laneCounts}
520
- selectedElementId={selectedElementId}
521
- selectedElementIds={selectedElementIds}
522
- hoveredClip={hoveredClip}
523
- draggedClip={draggedClip}
524
- resizingClip={resizingClip}
525
- isScrubbing={isScrubbing}
526
- blockedClipRef={blockedClipRef}
527
- suppressClickRef={suppressClickRef}
528
- scrollRef={scrollRef}
529
- // Windowing drops content to mount a row cheaply; unvirtualized it is pure cost.
530
- renderClipContent={
531
- timelineFocus.rowVirtualizationActive && viewport.isScrolling
532
- ? undefined
533
- : renderClipContent
534
- }
535
- renderClipOverlay={renderClipOverlay}
536
- playheadRef={playheadRef}
537
- onDrillDown={onDrillDown}
538
- onSelectElement={onSelectElement}
539
- setHoveredClip={setHoveredClip}
540
- setShowPopover={setShowPopover}
541
- setRangeSelection={setRangeSelection}
542
- setResizingClip={setResizingClip}
543
- setDraggedClip={setDraggedClip}
544
- setSelectedElementId={setSelectedElementId}
545
- shiftClickClipRef={shiftClickClipRef}
546
- getPreviewElement={getPreviewElement}
547
- getTrackStyle={getTrackStyle}
548
- keyframeCache={keyframeCache}
549
- gsapAnimations={gsapAnimations}
550
- selectedKeyframes={selectedKeyframes}
551
- currentTime={currentTime}
552
- onSeek={onSeek}
553
- beatAnalysis={adjustedBeatAnalysis}
554
- onSelectSegment={onSelectSegment}
555
- onClickKeyframe={onClickKeyframe}
556
- onShiftClickKeyframe={onShiftClickKeyframe}
557
- onMoveKeyframe={onMoveKeyframe}
558
- onContextMenuKeyframe={onContextMenuKeyframe}
559
- onContextMenuClip={onContextMenuClip}
560
- onContextMenuLane={(e, track, time) => {
561
- if (draggedClip?.started || resizingClip) return;
562
- setClipContextMenu(null);
563
- openGapMenu({ x: e.clientX, y: e.clientY, track, time });
564
- }}
565
- />
566
- {activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
34
+ <div {...meta.containerProps}>
35
+ <div {...meta.viewportProps}>
36
+ <TimelineCanvas />
37
+ {meta.razorGuide}
567
38
  </div>
568
- <TimelineOverlays
569
- elements={expandedElements}
570
- elementsRef={expandedElementsRef}
571
- theme={theme}
572
- showShortcutHint={showShortcutHint}
573
- showPopover={showPopover}
574
- rangeSelection={rangeSelection}
575
- setShowPopover={setShowPopover}
576
- setRangeSelection={setRangeSelection}
577
- kfContextMenu={kfContextMenu}
578
- setKfContextMenu={setKfContextMenu}
579
- onDeleteKeyframe={onDeleteKeyframe}
580
- onDeleteAllKeyframes={onDeleteAllKeyframes}
581
- onMoveKeyframeToPlayhead={onMoveKeyframeToPlayhead}
582
- clipContextMenu={clipContextMenu}
583
- setClipContextMenu={setClipContextMenu}
584
- currentTime={currentTime}
585
- onSplitElement={onSplitElement}
586
- pinZoomBeforeEdit={pinZoomBeforeEdit}
587
- onDeleteElement={_onDeleteElement}
588
- onCopyClip={onCopyClip}
589
- onPasteClip={onPasteClip}
590
- onDuplicateClip={onDuplicateClip}
591
- canPasteClip={canPasteClip}
592
- gapContextMenu={gapMenuModel}
593
- onDismissGapContextMenu={dismissGapMenu}
594
- onCloseTrackGap={closeTrackGap}
595
- onCloseAllTrackGaps={closeAllTrackGaps}
596
- onHoverGapAction={setHoveredGapAction}
597
- />
39
+ <TimelineOverlays />
598
40
  </div>
599
41
  );
42
+ }
43
+
44
+ export const Timeline = memo(function Timeline(props: TimelineProps = {}) {
45
+ return (
46
+ <TimelineProvider {...props}>
47
+ <TimelineView />
48
+ </TimelineProvider>
49
+ );
600
50
  });
@@ -1,8 +1,6 @@
1
1
  import { memo } from "react";
2
2
  import { TimelineRuler } from "./TimelineRuler";
3
3
  import { PlayheadIndicator } from "./PlayheadIndicator";
4
- import type { TimelineRangeSelection } from "./timelineEditing";
5
- import type { TimelineDropPlacement } from "./timelineCallbacks";
6
4
  import {
7
5
  RULER_H,
8
6
  CLIP_Y,
@@ -14,41 +12,20 @@ import {
14
12
  getTimelineRowTop,
15
13
  getTimelineRowHeight,
16
14
  } from "./timelineLayout";
17
- import { usePlayerStore } from "../store/playerStore";
18
- import type { ResizingClipState } from "./useTimelineClipDrag";
19
15
  import { type MultiDragPreviewInput } from "./timelineMultiDragPreview";
20
16
  import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
21
- import type { Rect } from "../../utils/marqueeGeometry";
22
17
  import { TimelineLanes } from "./TimelineLanes";
23
- import type { TimelineLaneBaseProps } from "./timelineLaneProps";
24
- import type { TimelineLaneGapStrips } from "./useTimelineGapHighlights";
25
18
  import { getTimelineElementIdentity } from "../lib/timelineElementHelpers";
26
19
  import { TimelineGestureOverlay } from "./TimelineGestureOverlay";
27
20
  import { resolveSnapGuide } from "./timelineSnapping";
28
-
29
- interface TimelineCanvasProps extends TimelineLaneBaseProps {
30
- major: number[];
31
- minor: number[];
32
- totalH: number;
33
- effectiveDuration: number;
34
- majorTickInterval: number;
35
- rangeSelection: TimelineRangeSelection | null;
36
- /** Live rubber-band multi-select rectangle (canvas coordinates), or null. */
37
- marqueeRect: Rect | null;
38
- resizingClip: ResizingClipState | null;
39
- /** Playhead is being actively scrubbed — fills the grab-handle head. */
40
- isScrubbing: boolean;
41
- playheadRef: React.RefObject<HTMLDivElement | null>;
42
- /** Gap strips: loud on gap-menu-row hover, quiet on the selected clip's lane. */
43
- laneGapStrips: TimelineLaneGapStrips[];
44
- /** Landing spot of an outside drag in progress, or null. */
45
- dropPreview: TimelineDropPlacement | null;
46
- }
21
+ import { useTimelineContext } from "./TimelineProvider";
47
22
 
48
23
  // A dropped clip's length is unknown until it lands; the preview shows a default.
49
24
  const DROP_PREVIEW_SECONDS = 3;
50
25
 
51
- export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
26
+ export const TimelineCanvas = memo(function TimelineCanvas() {
27
+ const { state, actions } = useTimelineContext();
28
+ const props = state.canvas;
52
29
  const { draggedClip, resizingClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
53
30
  const snapGuide = resolveSnapGuide(draggedClip, resizingClip);
54
31
  const draggedRowIndex =
@@ -74,7 +51,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
74
51
  onRazorSplit,
75
52
  onRazorSplitAll,
76
53
  } = useTimelineEditContextOptional();
77
- const beatDragging = usePlayerStore((s) => s.beatDragging);
54
+ const beatDragging = props.beatDragging;
78
55
  const draggedElement = draggedClip?.element ?? null;
79
56
  const draggedElementIdentity = draggedElement ? getTimelineElementIdentity(draggedElement) : null;
80
57
  // The drag ghost follows the cursor freely (both axes) — CapCut-style. The
@@ -122,6 +99,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
122
99
 
123
100
  <TimelineLanes
124
101
  {...props}
102
+ renderClipContent={actions.renderClipContent}
103
+ renderClipOverlay={actions.renderClipOverlay}
125
104
  snapGuide={snapGuide}
126
105
  draggedElement={draggedElement}
127
106
  multiDragPreview={multiDragPreview}
@@ -253,8 +232,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
253
232
  currentTime={props.currentTime}
254
233
  theme={props.theme}
255
234
  getTrackStyle={props.getTrackStyle}
256
- renderClipContent={props.renderClipContent}
257
- renderClipOverlay={props.renderClipOverlay}
235
+ renderClipContent={actions.renderClipContent}
236
+ renderClipOverlay={actions.renderClipOverlay}
258
237
  />
259
238
 
260
239
  {/* Marquee (rubber-band) multi-select rectangle — mirrors the canvas