@hyperframes/studio 0.8.56 → 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 (82) hide show
  1. package/dist/assets/hyperframes-player-c23wnIfc.js +464 -0
  2. package/dist/assets/{index-ZmBtVrEm.css → index-DKJpHYYd.css} +1 -1
  3. package/dist/assets/{index-DFlILHJe.js → index-DgkAzx3n.js} +1 -1
  4. package/dist/assets/{index-CxR7F2YQ.js → index-IDNr3X1H.js} +1 -1
  5. package/dist/assets/{index-BlkJ_Axe.js → index-gbOCDCuZ.js} +194 -194
  6. package/dist/index.d.ts +95 -9
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +15573 -14835
  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 +66 -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 +40 -4
  33. package/src/components/nle/NLEPreview.test.ts +66 -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/hooks/useCompositionDimensions.test.tsx +38 -0
  39. package/src/hooks/useCompositionDimensions.ts +17 -1
  40. package/src/hooks/useDomEditAttributeCommits.test.ts +23 -0
  41. package/src/hooks/useDomEditAttributeCommits.ts +14 -3
  42. package/src/hooks/useFileTree.test.tsx +56 -0
  43. package/src/hooks/useFileTree.ts +8 -1
  44. package/src/hooks/useSdkSession.lifecycle.test.tsx +80 -0
  45. package/src/hooks/useSdkSession.ts +46 -10
  46. package/src/hooks/useServerConnection.hashProject.test.tsx +303 -0
  47. package/src/hooks/useServerConnection.ts +81 -8
  48. package/src/index.ts +12 -0
  49. package/src/player/components/Player.test.ts +141 -13
  50. package/src/player/components/Player.tsx +58 -2
  51. package/src/player/components/ShortcutsPanel.test.tsx +4 -10
  52. package/src/player/components/Timeline.tsx +23 -572
  53. package/src/player/components/TimelineCanvas.tsx +9 -30
  54. package/src/player/components/TimelineOverlays.test.ts +67 -32
  55. package/src/player/components/TimelineOverlays.tsx +46 -107
  56. package/src/player/components/TimelineProvider.tsx +191 -0
  57. package/src/player/components/testRootHarness.ts +18 -0
  58. package/src/player/components/timelineProviderStateBuilders.test.ts +109 -0
  59. package/src/player/components/timelineProviderStateBuilders.ts +83 -0
  60. package/src/player/components/useTimelineClipContextMenu.ts +1 -1
  61. package/src/player/components/useTimelineOverlaysState.ts +100 -0
  62. package/src/player/components/useTimelineProviderState.tsx +583 -0
  63. package/src/player/hooks/playbackAdapterResolution.ts +98 -0
  64. package/src/player/hooks/timelinePlayerTestHarness.ts +120 -0
  65. package/src/player/hooks/usePreviewFirstFrameTelemetry.ts +51 -0
  66. package/src/player/hooks/useShadowPreviewReload.ts +183 -0
  67. package/src/player/hooks/useTimelinePlayer.seek.test.ts +8 -88
  68. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +457 -0
  69. package/src/player/hooks/useTimelinePlayer.ts +65 -99
  70. package/src/player/hooks/useTimelineSyncCallbacks.ts +159 -88
  71. package/src/telemetry/events.test.ts +18 -0
  72. package/src/telemetry/events.ts +10 -0
  73. package/src/telemetry/system.test.ts +49 -0
  74. package/src/telemetry/system.ts +6 -0
  75. package/src/telemetry/tabId.test.ts +43 -0
  76. package/src/telemetry/tabId.ts +44 -0
  77. package/src/utils/previewCompositionSize.ts +26 -0
  78. package/src/utils/reloadDebug.ts +2 -4
  79. package/src/utils/sourcePatcher.ts +1 -1
  80. package/src/utils/studioTelemetry.test.ts +14 -0
  81. package/src/utils/studioTelemetry.ts +4 -0
  82. package/dist/assets/hyperframes-player-k8LzMtXX.js +0 -464
@@ -0,0 +1,583 @@
1
+ import { useRef, useMemo, useCallback, useState } from "react";
2
+ import { useAdjustedBeatAnalysis, useMusicBeatAnalysis } from "../../hooks/useMusicBeatAnalysis";
3
+ import { usePlayerStore, type TimelineElement } from "../store/playerStore";
4
+ import { defaultTimelineTheme } from "./timelineTheme";
5
+ import { useTimelinePlayhead } from "./useTimelinePlayhead";
6
+ import { useTimelineZoom } from "./useTimelineZoom";
7
+ import { useTimelineAssetDrop } from "./timelineDragDrop";
8
+ import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
9
+ import { useTimelineClipDrag } from "./useTimelineClipDrag";
10
+ import type { ClipContextMenuState, TimelineContextValue } from "./TimelineProvider";
11
+ import {
12
+ buildTimelineMeta,
13
+ resolveRenderClipContent,
14
+ resolveResizingElementIds,
15
+ shouldIgnoreTimelinePointerDown,
16
+ } from "./timelineProviderStateBuilders";
17
+ import { useTimelineOverlaysState } from "./useTimelineOverlaysState";
18
+ import { useTimelineEditPinning } from "./useTimelineEditPinning";
19
+ import { useTimelineStackingSync } from "./useTimelineStackingSync";
20
+ import { useTimelineGeometry } from "./useTimelineGeometry";
21
+ import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
22
+ import { GUTTER, LABEL_COL_W, TRACKS_LEFT_PAD } from "./timelineLayout";
23
+ import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
24
+ import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
25
+ import type { TimelineProps } from "./TimelineTypes";
26
+ import {
27
+ getTrackStyle,
28
+ useTimelineDisplayLayout,
29
+ useTimelineTrackLayout,
30
+ } from "./useTimelineTrackLayout";
31
+ import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
32
+ import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
33
+ import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
34
+ import { useTimelinePerformanceTelemetry } from "./useTimelinePerformanceTelemetry";
35
+ import {
36
+ getEffectiveTimelineDuration,
37
+ getTimelinePreviewElement,
38
+ timelineNeedsLabelColumn,
39
+ } from "./timelineViewModel";
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
+ export function useTimelineProviderState({
48
+ onSeek,
49
+ onDrillDown,
50
+ renderClipContent,
51
+ renderClipOverlay,
52
+ onFileDrop,
53
+ onAssetDrop,
54
+ onBlockDrop,
55
+ onCompositionDrop,
56
+ onDeleteElement: _onDeleteElement,
57
+ onMoveElement: onMoveElementOverride,
58
+ onMoveElements: onMoveElementsOverride,
59
+ onResizeElement: onResizeElementOverride,
60
+ onResizeElements: onResizeElementsOverride,
61
+ onBlockedEditAttempt: onBlockedEditAttemptOverride,
62
+ onSplitElement: onSplitElementOverride,
63
+ onSelectElement,
64
+ onRangeSelect,
65
+ onCopyClip,
66
+ onPasteClip,
67
+ onDuplicateClip,
68
+ canPasteClip,
69
+ theme: themeOverrides,
70
+ sessionEpoch = 0,
71
+ }: TimelineProps = {}): TimelineContextValue {
72
+ const {
73
+ onMoveElement,
74
+ onMoveElements,
75
+ onResizeElement,
76
+ onResizeElements,
77
+ onBlockedEditAttempt,
78
+ onSplitElement,
79
+ onRazorSplitAll,
80
+ onDeleteKeyframe,
81
+ onDeleteAllKeyframes,
82
+ onMoveKeyframeToPlayhead,
83
+ onMoveKeyframe,
84
+ } = useResolvedTimelineEditCallbacks({
85
+ onMoveElement: onMoveElementOverride,
86
+ onMoveElements: onMoveElementsOverride,
87
+ onResizeElement: onResizeElementOverride,
88
+ onResizeElements: onResizeElementsOverride,
89
+ onBlockedEditAttempt: onBlockedEditAttemptOverride,
90
+ onSplitElement: onSplitElementOverride,
91
+ });
92
+ const theme = useMemo(() => ({ ...defaultTimelineTheme, ...themeOverrides }), [themeOverrides]);
93
+ const refreshAfterLaneMove = useTimelineLaneMoveRefresh();
94
+ useMusicBeatAnalysis();
95
+ const rawElements = usePlayerStore((s) => s.elements);
96
+ const timelineElements = rawElements;
97
+ const adjustedBeatAnalysis = useAdjustedBeatAnalysis();
98
+ const duration = usePlayerStore((s) => s.duration);
99
+ const timeDisplayMode = usePlayerStore((s) => s.timeDisplayMode);
100
+ const timelineReady = usePlayerStore((s) => s.timelineReady);
101
+ const selectedElementId = usePlayerStore((s) => s.selectedElementId);
102
+ const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
103
+ const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
104
+ const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
105
+ const labelMode = useMemo(
106
+ () => timelineNeedsLabelColumn(gsapAnimations, timelineElements),
107
+ [gsapAnimations, timelineElements],
108
+ );
109
+ // The label column provides pre-t=0 space; otherwise keep TRACKS_LEFT_PAD after the gutter.
110
+ const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
111
+ const contentGutter = labelMode ? GUTTER : 0;
112
+ const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
113
+ const currentTime = usePlayerStore((s) => s.currentTime);
114
+ const beatDragging = usePlayerStore((s) => s.beatDragging);
115
+ const timelineSessionEpoch = usePlayerStore((s) => s.timelineSessionEpoch);
116
+ const setFocusedEaseSegment = usePlayerStore((s) => s.setFocusedEaseSegment);
117
+ const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
118
+ const playheadRef = useRef<HTMLDivElement>(null);
119
+ const containerRef = useRef<HTMLDivElement>(null);
120
+ const scrollRef = useRef<HTMLDivElement>(null);
121
+ const activeTool = usePlayerStore((s) => s.activeTool);
122
+ const [hoveredClip, setHoveredClip] = useState<string | null>(null);
123
+ const isDragging = useRef(false);
124
+ const shiftHeld = useTimelineShiftModifier();
125
+ const [showPopover, setShowPopover] = useState(false);
126
+ const [kfContextMenu, setKfContextMenu] = useState<KeyframeDiamondContextMenuState | null>(null);
127
+ const [clipContextMenu, setClipContextMenu] = useState<ClipContextMenuState | null>(null);
128
+ const setContainerRef = useCallback((el: HTMLDivElement | null) => {
129
+ containerRef.current = el;
130
+ }, []);
131
+ const lastScrollLeftRef = useRef(0);
132
+ const effectiveDuration = useMemo(
133
+ () => getEffectiveTimelineDuration(duration, rawElements),
134
+ [duration, rawElements],
135
+ );
136
+ const keyframeCache = usePlayerStore((s) => s.keyframeCache);
137
+ useAutoExpandKeyframedClips(gsapAnimations);
138
+ const {
139
+ tracks,
140
+ trackStyles,
141
+ trackOrder,
142
+ trackOrderRef,
143
+ laneCounts,
144
+ rowGeometry,
145
+ rowGeometryRef,
146
+ groups,
147
+ trackGroupOf,
148
+ } = useTimelineTrackLayout(
149
+ timelineElements,
150
+ gsapAnimations,
151
+ selectedElementId,
152
+ selectedElementIds,
153
+ );
154
+ const timelineElementsRef = useRef(timelineElements);
155
+ timelineElementsRef.current = timelineElements; // oxlint-disable-line react/refs -- event handlers read the latest elements
156
+ const ppsRef = useRef(100);
157
+ const durationRef = useRef(effectiveDuration);
158
+ durationRef.current = effectiveDuration; // oxlint-disable-line react/refs -- event handlers read the latest duration
159
+ const fitPpsRef = useRef(100);
160
+ const {
161
+ pinZoomBeforeEdit,
162
+ setRangeSelectionRef,
163
+ pinnedOnMoveElement,
164
+ pinnedOnMoveElements,
165
+ pinnedOnResizeElement,
166
+ pinnedOnResizeElements,
167
+ pinnedOnFileDrop,
168
+ pinnedOnAssetDrop,
169
+ pinnedOnBlockDrop,
170
+ pinnedOnCompositionDrop,
171
+ } = useTimelineEditPinning({
172
+ ppsRef,
173
+ fitPpsRef,
174
+ onMoveElement,
175
+ onMoveElements,
176
+ onResizeElement,
177
+ onResizeElements,
178
+ onFileDrop,
179
+ onAssetDrop,
180
+ onBlockDrop,
181
+ onCompositionDrop,
182
+ });
183
+ const { readClipZIndex, applyStackingPatches, zSyncEnabled } = useTimelineStackingSync({
184
+ expandedElementsRef: timelineElementsRef,
185
+ });
186
+ const {
187
+ draggedClip,
188
+ setDraggedClip,
189
+ resizingClip,
190
+ setResizingClip,
191
+ blockedClipRef,
192
+ suppressClickRef,
193
+ } = useTimelineClipDrag({
194
+ scrollRef,
195
+ ppsRef,
196
+ durationRef,
197
+ trackOrderRef,
198
+ rowGeometryRef,
199
+ onMoveElement: pinnedOnMoveElement,
200
+ onMoveElements: pinnedOnMoveElements,
201
+ onResizeElement: pinnedOnResizeElement,
202
+ onResizeElements: pinnedOnResizeElements,
203
+ onBlockedEditAttempt,
204
+ onSeek,
205
+ setShowPopover,
206
+ setRangeSelectionRef,
207
+ readZIndex: zSyncEnabled ? readClipZIndex : undefined,
208
+ onStackingPatches: zSyncEnabled ? applyStackingPatches : undefined,
209
+ refreshAfterLaneMove,
210
+ sessionEpoch,
211
+ });
212
+ const assetDrop = useTimelineAssetDrop({
213
+ scrollRef,
214
+ ppsRef,
215
+ trackOrderRef,
216
+ rowGeometryRef,
217
+ contentOrigin,
218
+ onFileDrop: pinnedOnFileDrop,
219
+ onAssetDrop: pinnedOnAssetDrop,
220
+ onBlockDrop: pinnedOnBlockDrop,
221
+ onCompositionDrop: pinnedOnCompositionDrop,
222
+ sessionEpoch,
223
+ });
224
+ const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowGeometry);
225
+ const resizingElementIds = resolveResizingElementIds(resizingClip);
226
+ const { recordTimelineScroll } = useTimelinePerformanceTelemetry({
227
+ totalClipCount: timelineElements.length,
228
+ totalRowCount: displayLayout.displayTrackOrder.length,
229
+ zoomMode,
230
+ });
231
+ const { viewport, showShortcutHint, setScrollRef, syncScrollViewport } =
232
+ useTimelineScrollViewport(scrollRef, [
233
+ timelineReady,
234
+ timelineElements.length,
235
+ displayLayout.totalH,
236
+ ]);
237
+ const { pps, fitPps, displayContentWidth, displayDuration, zoomModeRef, manualZoomPercentRef } =
238
+ useTimelineGeometry({
239
+ viewportWidth: viewport.clientWidth,
240
+ effectiveDuration,
241
+ zoomMode,
242
+ manualZoomPercent,
243
+ ppsRef,
244
+ fitPpsRef,
245
+ draggedClip,
246
+ resizingClip,
247
+ expandedElements: timelineElements,
248
+ isDragging,
249
+ scrollRef,
250
+ lastScrollLeftRef,
251
+ contentOrigin,
252
+ });
253
+ const timelineFocus = useTimelineLogicalFocus({
254
+ scrollRef,
255
+ tracks,
256
+ layout: displayLayout,
257
+ laneCounts,
258
+ selectedElementId,
259
+ selectedElementIds,
260
+ groups,
261
+ trackGroupOf,
262
+ gsapAnimations,
263
+ elements: timelineElements,
264
+ pixelsPerSecond: pps,
265
+ contentOrigin,
266
+ allowHorizontal: zoomMode === "manual",
267
+ viewport,
268
+ sessionEpoch,
269
+ draggedRowKey: draggedClip?.started ? draggedClip.previewTrack : undefined,
270
+ resizingElementIds,
271
+ clipContextMenuRowKey: clipContextMenu?.element.track,
272
+ keyframeContextMenuRowKey: kfContextMenu?.element.track,
273
+ lastScrollLeftRef,
274
+ syncScrollViewport,
275
+ });
276
+ const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
277
+ const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
278
+ const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
279
+ useTimelineKeyframeHandlers({
280
+ expandedElements: timelineElements,
281
+ keyframeCache,
282
+ onSelectElement,
283
+ onSeek,
284
+ setSelectedElementId,
285
+ setKfContextMenu,
286
+ toggleSelectedKeyframe,
287
+ });
288
+ const { clipIndex, renderTimeRange, visibleTimeRange, pinnedClipIdentities } =
289
+ useTimelineClipRenderWindow({
290
+ tracks,
291
+ viewport,
292
+ pixelsPerSecond: pps,
293
+ contentOrigin,
294
+ duration: displayDuration,
295
+ selectedElementId: selectedElementId ?? undefined,
296
+ draggedElementId: draggedClip ? getTimelineElementIdentity(draggedClip.element) : undefined,
297
+ resizingElementIds,
298
+ focusedElementId: timelineFocus.pinnedElementId,
299
+ focusedEaseElementId: focusedEaseSegment?.elementId,
300
+ clipContextMenuElementId: clipContextMenu
301
+ ? getTimelineElementIdentity(clipContextMenu.element)
302
+ : undefined,
303
+ keyframeContextMenuElementId: kfContextMenu
304
+ ? getTimelineElementIdentity(kfContextMenu.element)
305
+ : undefined,
306
+ });
307
+ useTimelineActiveClips({
308
+ scrollRef,
309
+ currentTime,
310
+ clipStateVersion: renderTimeRange,
311
+ elementStateVersion: timelineElements,
312
+ });
313
+ const { seekFromX, autoScrollDuringDrag, dragScrollRaf } = useTimelinePlayhead({
314
+ playheadRef,
315
+ scrollRef,
316
+ ppsRef,
317
+ durationRef,
318
+ isDragging,
319
+ currentTime,
320
+ zoomMode,
321
+ manualZoomPercent,
322
+ zoomModeRef,
323
+ manualZoomPercentRef,
324
+ fitPps,
325
+ fitPpsRef,
326
+ effectiveDuration,
327
+ pps,
328
+ timelineReady,
329
+ elementsLength: timelineElements.length,
330
+ setZoomMode,
331
+ setManualZoomPercent,
332
+ onSeek,
333
+ contentOrigin,
334
+ });
335
+ const { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer } =
336
+ useTimelineRazorInteraction({
337
+ active: activeTool === "razor",
338
+ scrollRef,
339
+ contentOrigin,
340
+ pixelsPerSecond: pps,
341
+ onSplitAll: onRazorSplitAll,
342
+ });
343
+ const overlaysProps = useTimelineOverlaysState({
344
+ elements: timelineElements,
345
+ elementsRef: timelineElementsRef,
346
+ theme,
347
+ showShortcutHint,
348
+ showPopover,
349
+ setShowPopover,
350
+ kfContextMenu,
351
+ setKfContextMenu,
352
+ onDeleteKeyframe,
353
+ onDeleteAllKeyframes,
354
+ onMoveKeyframeToPlayhead,
355
+ clipContextMenu,
356
+ setClipContextMenu,
357
+ currentTime,
358
+ onSplitElement,
359
+ pinZoomBeforeEdit,
360
+ onDeleteElement: _onDeleteElement,
361
+ onCopyClip,
362
+ onPasteClip,
363
+ onDuplicateClip,
364
+ canPasteClip,
365
+ onSelectElement,
366
+ selectedElementId,
367
+ setRangeSelectionRef,
368
+ rangeSelection: {
369
+ scrollRef,
370
+ ppsRef,
371
+ effectiveDuration,
372
+ pps,
373
+ onSeek,
374
+ seekFromX,
375
+ autoScrollDuringDrag,
376
+ dragScrollRaf,
377
+ isDragging,
378
+ setShowPopover,
379
+ elementsRef: timelineElementsRef,
380
+ clipIndex,
381
+ rowGeometryRef,
382
+ onSelectElement,
383
+ contentOrigin,
384
+ sessionEpoch,
385
+ onRangeSelect,
386
+ },
387
+ gapMenu: {
388
+ tracks,
389
+ expandedElementsRef: timelineElementsRef,
390
+ trackOrderRef,
391
+ onMoveElement: pinnedOnMoveElement,
392
+ onMoveElements: pinnedOnMoveElements,
393
+ },
394
+ });
395
+ const {
396
+ overlays,
397
+ gapHighlight,
398
+ openGapMenu,
399
+ onContextMenuClip,
400
+ shiftClickClipRef,
401
+ marqueeRect,
402
+ isScrubbing,
403
+ handlePointerDown,
404
+ handlePointerMove,
405
+ handlePointerUp,
406
+ handlePointerCancel,
407
+ } = overlaysProps;
408
+ const { rangeSelection, setRangeSelection } = overlays;
409
+
410
+ const laneGapStrips = useTimelineGapHighlights({
411
+ gapHighlight,
412
+ tracks,
413
+ selectedElementId,
414
+ selectedElementIds,
415
+ expandedElements: timelineElements,
416
+ dragActive: draggedClip?.started === true || resizingClip != null,
417
+ displayDuration,
418
+ });
419
+
420
+ const { major, minor, majorTickInterval } = useTimelineTicks(
421
+ displayDuration,
422
+ pps,
423
+ timeDisplayMode,
424
+ timelineFocus.rowVirtualizationActive ? renderTimeRange : undefined,
425
+ );
426
+
427
+ const getPreviewElement = useCallback(
428
+ (element: TimelineElement): TimelineElement => getTimelinePreviewElement(element, resizingClip),
429
+ [resizingClip],
430
+ );
431
+
432
+ const canvasProps = {
433
+ major,
434
+ minor,
435
+ pps,
436
+ contentOrigin,
437
+ contentGutter,
438
+ trackContentWidth: displayContentWidth,
439
+ totalH: displayLayout.totalH,
440
+ effectiveDuration,
441
+ majorTickInterval,
442
+ rangeSelection,
443
+ marqueeRect,
444
+ laneGapStrips,
445
+ dropPreview: assetDrop.dropPreview,
446
+ theme,
447
+ displayTrackOrder: displayLayout.displayTrackOrder,
448
+ rowHeights: displayLayout.displayRowHeights,
449
+ rowGeometry: displayLayout.rowGeometry,
450
+ virtualRows: timelineFocus.virtualRows,
451
+ logicalRows: timelineFocus.logicalRows,
452
+ focusedTargetId: timelineFocus.focusedTargetId,
453
+ rowsVirtualized: timelineFocus.rowVirtualizationActive,
454
+ clipIndex,
455
+ renderTimeRange,
456
+ visibleTimeRange,
457
+ pinnedClipIdentities,
458
+ trackOrder,
459
+ tracks,
460
+ trackStyles,
461
+ groups,
462
+ laneCounts,
463
+ selectedElementId,
464
+ selectedElementIds,
465
+ hoveredClip,
466
+ draggedClip,
467
+ resizingClip,
468
+ isScrubbing,
469
+ blockedClipRef,
470
+ suppressClickRef,
471
+ scrollRef,
472
+ playheadRef,
473
+ onDrillDown,
474
+ onSelectElement,
475
+ setHoveredClip,
476
+ setShowPopover,
477
+ setRangeSelection,
478
+ setResizingClip,
479
+ setDraggedClip,
480
+ setSelectedElementId,
481
+ shiftClickClipRef,
482
+ getPreviewElement,
483
+ getTrackStyle,
484
+ keyframeCache,
485
+ gsapAnimations,
486
+ selectedKeyframes,
487
+ currentTime,
488
+ onSeek,
489
+ beatAnalysis: adjustedBeatAnalysis,
490
+ onSelectSegment,
491
+ onClickKeyframe,
492
+ onShiftClickKeyframe,
493
+ onMoveKeyframe,
494
+ onContextMenuKeyframe,
495
+ onContextMenuClip,
496
+ onContextMenuLane: (e: React.MouseEvent, track: number, time: number) => {
497
+ if (draggedClip?.started || resizingClip) return;
498
+ setClipContextMenu(null);
499
+ openGapMenu({ x: e.clientX, y: e.clientY, track, time });
500
+ },
501
+ onResizeElement,
502
+ onMoveElement,
503
+ beatDragging,
504
+ };
505
+ const timelineRenderClipContent = resolveRenderClipContent(
506
+ timelineFocus.rowVirtualizationActive,
507
+ viewport.isScrolling,
508
+ renderClipContent,
509
+ );
510
+ const timelineMeta = buildTimelineMeta({
511
+ emptyState: {
512
+ isDragOver: assetDrop.isDragOver,
513
+ onFileDrop: !!onFileDrop,
514
+ onDragOver: assetDrop.handleAssetDragOver,
515
+ onDragLeave: assetDrop.handleAssetDragLeave,
516
+ onDrop: assetDrop.handleAssetDrop,
517
+ },
518
+ container: {
519
+ ref: setContainerRef,
520
+ "aria-label": "Timeline track view",
521
+ "data-timeline-element-count": timelineElements.length,
522
+ isDragOver: assetDrop.isDragOver,
523
+ activeTool,
524
+ shiftHeld,
525
+ accentClass: "ring-1 ring-inset ring-studio-accent/60",
526
+ onMouseMove: updateRazorGuide,
527
+ onMouseLeave: clearRazorGuide,
528
+ style: {
529
+ touchAction: "pan-x pan-y",
530
+ background: theme.shellBackground,
531
+ borderColor: theme.shellBorder,
532
+ },
533
+ },
534
+ viewport: {
535
+ ref: setScrollRef,
536
+ tabIndex: -1,
537
+ labelMode,
538
+ zoomMode,
539
+ onScroll: (e) => {
540
+ lastScrollLeftRef.current = e.currentTarget.scrollLeft;
541
+ recordTimelineScroll(e.currentTarget);
542
+ syncScrollViewport(e.currentTarget, true);
543
+ },
544
+ ...timelineFocus.timelineFocusProps,
545
+ onDragOver: assetDrop.handleAssetDragOver,
546
+ onDragLeave: assetDrop.handleAssetDragLeave,
547
+ onDrop: assetDrop.handleAssetDrop,
548
+ onPointerDown: (e) => {
549
+ if (shouldIgnoreTimelinePointerDown(e.target)) return;
550
+ if (splitAllAtPointer(e)) return;
551
+ handlePointerDown(e);
552
+ },
553
+ onPointerMove: handlePointerMove,
554
+ onPointerUp: handlePointerUp,
555
+ onPointerCancel: handlePointerCancel,
556
+ onLostPointerCapture: handlePointerCancel,
557
+ },
558
+ elementCount: timelineElements.length,
559
+ labelColumnWidth: LABEL_COL_W,
560
+ razorGuide:
561
+ activeTool === "razor" && razorGuideX !== null ? (
562
+ <TimelineRazorGuide x={razorGuideX} />
563
+ ) : null,
564
+ });
565
+ const contextValue: TimelineContextValue = {
566
+ state: {
567
+ timelineReady,
568
+ elements: timelineElements,
569
+ selectedElementId,
570
+ sessionEpoch: timelineSessionEpoch,
571
+ keyframeCache,
572
+ canvas: canvasProps,
573
+ overlays,
574
+ },
575
+ actions: {
576
+ renderClipContent: timelineRenderClipContent,
577
+ renderClipOverlay,
578
+ setFocusedEaseSegment,
579
+ },
580
+ meta: timelineMeta,
581
+ };
582
+ return contextValue;
583
+ }
@@ -0,0 +1,98 @@
1
+ // Picks the PlaybackAdapter an iframe's runtime exposes, wrapping it for static seek when needed.
2
+ import type { MutableRefObject } from "react";
3
+ import { usePlayerStore } from "../store/playerStore";
4
+ import type { PlaybackAdapter, IframeWindow } from "../lib/playbackTypes";
5
+ import {
6
+ getAdapterDuration,
7
+ wrapTimeline,
8
+ getDefaultStaticSeekPlaybackClock,
9
+ releaseStaticSeekCache,
10
+ resolveStaticSeekFallback,
11
+ type StaticSeekCacheEntry,
12
+ } from "../lib/playbackAdapter";
13
+ import { readTimelineDurationFromDocument } from "../lib/timelineDOM";
14
+
15
+ /**
16
+ * Timeline-based adapters (`__timeline`, then `__timelines`): `match` is the first that covers
17
+ * the document duration; otherwise `fallback` is the first with any duration.
18
+ */
19
+ function pickTimelineAdapter(
20
+ iframe: HTMLIFrameElement,
21
+ win: IframeWindow,
22
+ docDuration: number,
23
+ ): { match: PlaybackAdapter | null; fallback: PlaybackAdapter | null } {
24
+ let fallback: PlaybackAdapter | null = null;
25
+ for (const adapter of timelineCandidates(iframe, win)) {
26
+ const dur = getAdapterDuration(adapter);
27
+ if (dur > 0 && docDuration <= dur) return { match: adapter, fallback };
28
+ if (dur > 0) fallback ??= adapter;
29
+ }
30
+ return { match: null, fallback };
31
+ }
32
+
33
+ function* timelineCandidates(
34
+ iframe: HTMLIFrameElement,
35
+ win: IframeWindow,
36
+ ): Generator<PlaybackAdapter> {
37
+ if (win.__timeline) yield wrapTimeline(win.__timeline);
38
+ const keys = win.__timelines ? Object.keys(win.__timelines) : [];
39
+ if (!win.__timelines || keys.length === 0) return;
40
+ // The outermost [data-composition-id] is the master; Object.keys() order would let a
41
+ // sub-composition hijack transport.
42
+ const rootId = iframe.contentDocument
43
+ ?.querySelector("[data-composition-id]")
44
+ ?.getAttribute("data-composition-id");
45
+ const key = rootId && rootId in win.__timelines ? rootId : keys[keys.length - 1];
46
+ yield wrapTimeline(win.__timelines[key]);
47
+ }
48
+
49
+ interface StaticSeekRefs {
50
+ cache: MutableRefObject<StaticSeekCacheEntry | null>;
51
+ warned: MutableRefObject<boolean>;
52
+ }
53
+
54
+ export function resolvePlaybackAdapter(
55
+ iframe: HTMLIFrameElement,
56
+ win: IframeWindow,
57
+ refs: StaticSeekRefs,
58
+ ): PlaybackAdapter | null {
59
+ const playerAdapter =
60
+ win.__player && typeof win.__player.play === "function" ? win.__player : null;
61
+ const docDuration = readTimelineDurationFromDocument(iframe.contentDocument);
62
+ const adapterDur = getAdapterDuration(playerAdapter);
63
+
64
+ if (adapterDur > 0 && docDuration <= adapterDur) {
65
+ releaseStaticSeekCache(refs.cache, refs.warned);
66
+ return playerAdapter;
67
+ }
68
+
69
+ const timelines = pickTimelineAdapter(iframe, win, docDuration);
70
+ if (timelines.match) {
71
+ releaseStaticSeekCache(refs.cache, refs.warned);
72
+ return timelines.match;
73
+ }
74
+ const timelineAdapter = timelines.fallback;
75
+
76
+ // The document timeline extends past every native adapter's duration.
77
+ // Wrap the best available adapter with the effective duration so the
78
+ // seek slider, seek clamping, and duration display cover the full range.
79
+ const bestAdapter = playerAdapter ?? timelineAdapter;
80
+ const effectiveDuration = Math.max(usePlayerStore.getState().duration, docDuration, adapterDur);
81
+ if (
82
+ bestAdapter &&
83
+ effectiveDuration > 0 &&
84
+ ("renderSeek" in bestAdapter || typeof bestAdapter.seek === "function")
85
+ ) {
86
+ return resolveStaticSeekFallback({
87
+ cache: refs.cache,
88
+ warned: refs.warned,
89
+ bestAdapter,
90
+ effectiveDuration,
91
+ docDuration,
92
+ clock: getDefaultStaticSeekPlaybackClock(win),
93
+ getPlaybackRate: () => usePlayerStore.getState().playbackRate,
94
+ });
95
+ }
96
+
97
+ return bestAdapter;
98
+ }