@hyperframes/studio 0.8.56 → 0.8.58

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