@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
@@ -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,41 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { resolvePlaybackAdapter } from "./playbackAdapterResolution";
5
+
6
+ function timeline(duration: number) {
7
+ return {
8
+ duration: vi.fn(() => duration),
9
+ time: vi.fn(() => 0),
10
+ seek: vi.fn(),
11
+ play: vi.fn(),
12
+ pause: vi.fn(),
13
+ isActive: vi.fn(() => false),
14
+ };
15
+ }
16
+
17
+ describe("resolvePlaybackAdapter", () => {
18
+ it("uses the __timelines key matching the document root, not the first key", () => {
19
+ const first = timeline(12);
20
+ const second = timeline(12);
21
+ const third = timeline(12);
22
+ const iframe = document.createElement("iframe");
23
+ const doc = document.implementation.createHTMLDocument("preview");
24
+ doc.body.innerHTML = '<div data-composition-id="second" data-duration="10"></div>';
25
+ Object.defineProperty(iframe, "contentDocument", { configurable: true, value: doc });
26
+ const win = {
27
+ __timelines: { first, second, third },
28
+ } as never;
29
+
30
+ const adapter = resolvePlaybackAdapter(iframe, win, {
31
+ cache: { current: null },
32
+ warned: { current: false },
33
+ });
34
+
35
+ expect(adapter).not.toBeNull();
36
+ adapter?.seek(7);
37
+ expect(second.seek).toHaveBeenCalledWith(7);
38
+ expect(first.seek).not.toHaveBeenCalled();
39
+ expect(third.seek).not.toHaveBeenCalled();
40
+ });
41
+ });