@hyperframes/studio 0.7.75 → 0.7.77

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 (134) hide show
  1. package/dist/assets/{hyperframes-player-BrfzYqX3.js → hyperframes-player-CEggaaxR.js} +1 -1
  2. package/dist/assets/{index-BvGKYVrH.js → index-B1INQNmj.js} +1 -1
  3. package/dist/assets/{index-CjGZkNxS.js → index-B1Tjjdse.js} +1 -1
  4. package/dist/assets/index-Bp4jAYZG.js +428 -0
  5. package/dist/assets/index-CBAfprvx.css +1 -0
  6. package/dist/index.d.ts +64 -17
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +18040 -16293
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +16 -17
  12. package/src/components/EditorShell.tsx +3 -0
  13. package/src/components/TimelineToolbar.test.tsx +48 -3
  14. package/src/components/TimelineToolbar.tsx +89 -36
  15. package/src/components/editor/AnimationCard.test.tsx +18 -75
  16. package/src/components/editor/AnimationCard.tsx +26 -2
  17. package/src/components/editor/GsapAddAnimationControl.tsx +68 -0
  18. package/src/components/editor/GsapAnimationSection.tsx +18 -157
  19. package/src/components/editor/KeyframeEaseList.tsx +5 -1
  20. package/src/components/editor/KeyframeNavigation.tsx +46 -14
  21. package/src/components/editor/KeyframeNavigationDiamond.test.tsx +59 -0
  22. package/src/components/editor/MotionPathOverlay.tsx +32 -10
  23. package/src/components/editor/PropertyPanel.tsx +15 -5
  24. package/src/components/editor/PropertyPanelFlat.tsx +48 -23
  25. package/src/components/editor/gsapAnimationCallbacks.test.ts +122 -0
  26. package/src/components/editor/gsapAnimationCallbacks.ts +108 -4
  27. package/src/components/editor/keyframeRetime.test.ts +25 -22
  28. package/src/components/editor/keyframeRetime.ts +1 -2
  29. package/src/components/editor/propertyPanelFlatLayoutSection.tsx +2 -2
  30. package/src/components/editor/propertyPanelFlatMotionSection.tsx +25 -139
  31. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +32 -16
  32. package/src/components/editor/propertyPanelFlatTextSection.tsx +21 -4
  33. package/src/components/nle/PreviewOverlays.tsx +3 -1
  34. package/src/components/nle/useTimelineEditCallbacks.test.ts +30 -0
  35. package/src/components/nle/useTimelineEditCallbacks.test.tsx +693 -0
  36. package/src/components/nle/useTimelineEditCallbacks.ts +197 -57
  37. package/src/contexts/TimelineEditContext.tsx +1 -1
  38. package/src/hooks/deleteSelectedKeyframes.ts +24 -8
  39. package/src/hooks/gsapDragCommit.ts +2 -2
  40. package/src/hooks/gsapDragPositionCommit.ts +84 -5
  41. package/src/hooks/gsapKeyframeCacheHelpers.test.ts +176 -1
  42. package/src/hooks/gsapKeyframeCacheHelpers.ts +93 -52
  43. package/src/hooks/gsapRuntimeBridge.test.ts +100 -0
  44. package/src/hooks/gsapScriptCommitHelpers.ts +3 -2
  45. package/src/hooks/gsapScriptCommitTypes.ts +2 -0
  46. package/src/hooks/gsapShared.test.ts +109 -1
  47. package/src/hooks/gsapShared.ts +142 -17
  48. package/src/hooks/gsapTweenSynth.test.ts +30 -1
  49. package/src/hooks/gsapTweenSynth.ts +61 -14
  50. package/src/hooks/keyframeCacheAstLoad.ts +181 -0
  51. package/src/hooks/timelineEditingHelpers.test.ts +80 -0
  52. package/src/hooks/timelineMoveAdapter.ts +9 -2
  53. package/src/hooks/useDomEditWiring.ts +2 -2
  54. package/src/hooks/useDomSelection.ts +4 -0
  55. package/src/hooks/useEnableKeyframes.test.ts +122 -0
  56. package/src/hooks/useEnableKeyframes.ts +116 -56
  57. package/src/hooks/useGestureCommit.ts +2 -2
  58. package/src/hooks/useGsapAnimationFetchFallback.ts +1 -1
  59. package/src/hooks/useGsapKeyframeOps.test.tsx +92 -13
  60. package/src/hooks/useGsapKeyframeOps.ts +30 -13
  61. package/src/hooks/useGsapScriptCommits.test.tsx +21 -0
  62. package/src/hooks/useGsapScriptCommits.ts +2 -0
  63. package/src/hooks/useGsapSelectionHandlers.test.tsx +69 -3
  64. package/src/hooks/useGsapSelectionHandlers.ts +90 -65
  65. package/src/hooks/useGsapTweenCache.test.ts +16 -0
  66. package/src/hooks/useGsapTweenCache.ts +64 -178
  67. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +68 -0
  68. package/src/hooks/useStudioContextValue.test.ts +93 -0
  69. package/src/hooks/useStudioContextValue.ts +17 -3
  70. package/src/hooks/useStudioTestHooks.ts +63 -0
  71. package/src/hooks/useTimelineEditing.test.tsx +187 -22
  72. package/src/hooks/useTimelineEditing.ts +43 -32
  73. package/src/hooks/useTimelineEditingTypes.ts +2 -0
  74. package/src/hooks/useTimelineGroupEditing.ts +106 -82
  75. package/src/player/components/KeyframeDiamondContextMenu.test.tsx +69 -0
  76. package/src/player/components/KeyframeDiamondContextMenu.tsx +19 -8
  77. package/src/player/components/LayerDisclosureRow.tsx +76 -0
  78. package/src/player/components/Timeline.test.ts +420 -90
  79. package/src/player/components/Timeline.tsx +80 -78
  80. package/src/player/components/TimelineCanvas.tsx +33 -21
  81. package/src/player/components/TimelineClip.tsx +3 -1
  82. package/src/player/components/TimelineClipDiamonds.test.tsx +540 -16
  83. package/src/player/components/TimelineClipDiamonds.tsx +320 -117
  84. package/src/player/components/TimelineDiamondConnectors.tsx +145 -0
  85. package/src/player/components/TimelineLanes.tsx +153 -79
  86. package/src/player/components/TimelineOverlays.tsx +4 -18
  87. package/src/player/components/TimelinePropertyLanes.test.tsx +450 -0
  88. package/src/player/components/TimelinePropertyLanes.tsx +191 -0
  89. package/src/player/components/TimelineRazorInteraction.tsx +60 -0
  90. package/src/player/components/TimelineRuler.tsx +11 -21
  91. package/src/player/components/TimelineTrackHeader.test.tsx +346 -0
  92. package/src/player/components/TimelineTrackHeader.tsx +363 -0
  93. package/src/player/components/TrackClipCount.tsx +17 -0
  94. package/src/player/components/timelineCallbacks.ts +21 -6
  95. package/src/player/components/timelineClipDragPreview.test.ts +50 -3
  96. package/src/player/components/timelineClipDragPreview.ts +33 -15
  97. package/src/player/components/timelineCollision.ts +4 -13
  98. package/src/player/components/timelineDiamondTypes.ts +120 -0
  99. package/src/player/components/timelineDragDrop.ts +8 -3
  100. package/src/player/components/timelineEditing.test.ts +19 -27
  101. package/src/player/components/timelineEditing.ts +7 -8
  102. package/src/player/components/timelineKeyframeIdentity.test.ts +49 -0
  103. package/src/player/components/timelineKeyframeIdentity.ts +66 -0
  104. package/src/player/components/timelineLayout.test.ts +162 -9
  105. package/src/player/components/timelineLayout.ts +198 -38
  106. package/src/player/components/timelineMarquee.test.ts +76 -21
  107. package/src/player/components/timelineMarquee.ts +17 -14
  108. package/src/player/components/trackHeaderLaneState.ts +121 -0
  109. package/src/player/components/trackHeaderLaneValues.ts +139 -0
  110. package/src/player/components/useAutoExpandKeyframedClips.test.tsx +75 -0
  111. package/src/player/components/useAutoExpandKeyframedClips.ts +50 -0
  112. package/src/player/components/useTimelineClipDrag.ts +4 -1
  113. package/src/player/components/useTimelineGeometry.ts +4 -1
  114. package/src/player/components/useTimelineKeyframeHandlers.test.tsx +108 -0
  115. package/src/player/components/useTimelineKeyframeHandlers.ts +50 -15
  116. package/src/player/components/useTimelinePlayhead.ts +21 -12
  117. package/src/player/components/useTimelineRangeSelection.ts +35 -14
  118. package/src/player/components/useTimelineTrackLayout.test.ts +53 -0
  119. package/src/player/components/useTimelineTrackLayout.ts +162 -0
  120. package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
  121. package/src/player/hooks/useExpandedTimelineElements.ts +21 -12
  122. package/src/player/lib/timelineElementHelpers.ts +26 -11
  123. package/src/player/store/keyframeSlice.ts +122 -0
  124. package/src/player/store/playerStore.test.ts +25 -0
  125. package/src/player/store/playerStore.ts +7 -43
  126. package/src/telemetry/events.test.ts +9 -3
  127. package/src/telemetry/events.ts +16 -7
  128. package/src/utils/globalTimeCompiler.test.ts +10 -0
  129. package/src/utils/globalTimeCompiler.ts +3 -3
  130. package/dist/assets/index-B37rWXo4.css +0 -1
  131. package/dist/assets/index-DzDajONI.js +0 -428
  132. package/src/player/components/TimelineDragGhost.tsx +0 -59
  133. package/src/utils/keyframeSelection.test.ts +0 -45
  134. package/src/utils/keyframeSelection.ts +0 -29
@@ -9,7 +9,6 @@ import { defaultTimelineTheme } from "./timelineTheme";
9
9
  import { useTimelineRangeSelection } from "./useTimelineRangeSelection";
10
10
  import { useTimelinePlayhead } from "./useTimelinePlayhead";
11
11
  import { useTimelineActiveClips } from "./useTimelineActiveClips";
12
- import { getTrackStyle } from "./timelineIcons";
13
12
  import { useTimelineZoom } from "./useTimelineZoom";
14
13
  import { useTimelineAssetDrop } from "./timelineDragDrop";
15
14
  import { TimelineEmptyState } from "./TimelineEmptyState";
@@ -17,18 +16,27 @@ import { TimelineCanvas } from "./TimelineCanvas";
17
16
  import { type KeyframeDiamondContextMenuState } from "./KeyframeDiamondContextMenu";
18
17
  import { useTimelineClipDrag } from "./useTimelineClipDrag";
19
18
  import { TimelineOverlays } from "./TimelineOverlays";
19
+ import { animationContributesLane } from "./TimelinePropertyLanes";
20
20
  import { useTimelineEditPinning } from "./useTimelineEditPinning";
21
21
  import { useTimelineStackingSync } from "./useTimelineStackingSync";
22
22
  import { useTimelineGeometry } from "./useTimelineGeometry";
23
- import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
24
- import { GUTTER, TRACKS_LEFT_PAD, generateTicks, getTimelineCanvasHeight } from "./timelineLayout";
23
+ import { useAutoExpandKeyframedClips } from "./useAutoExpandKeyframedClips";
24
+ import { GUTTER, LABEL_COL_W, TRACKS_LEFT_PAD, generateTicks } from "./timelineLayout";
25
25
  import { useTimelineScrollViewport } from "./useTimelineScrollViewport";
26
26
  import { STUDIO_PREVIEW_FPS } from "../lib/time";
27
27
  import { useResolvedTimelineEditCallbacks } from "./useResolvedTimelineEditCallbacks";
28
28
  import type { TimelineProps } from "./TimelineTypes";
29
+ import {
30
+ getTrackStyle,
31
+ useTimelineDisplayLayout,
32
+ useTimelineTrackLayout,
33
+ } from "./useTimelineTrackLayout";
34
+ import { useTimelineKeyframeHandlers } from "./useTimelineKeyframeHandlers";
35
+ import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
29
36
  import { useTrackGapMenu } from "./useTrackGapMenu";
30
37
  import { useTimelineGapHighlights } from "./useTimelineGapHighlights";
31
38
  import { useStudioPlaybackContextOptional } from "../../contexts/StudioContext";
39
+ import { TimelineRazorGuide, useTimelineRazorInteraction } from "./TimelineRazorInteraction";
32
40
 
33
41
  // Re-export pure utilities so existing imports from "./Timeline" still resolve.
34
42
  export {
@@ -74,7 +82,6 @@ export const Timeline = memo(function Timeline({
74
82
  onRazorSplitAll,
75
83
  onDeleteKeyframe,
76
84
  onDeleteAllKeyframes,
77
- onChangeKeyframeEase,
78
85
  onMoveKeyframeToPlayhead,
79
86
  onMoveKeyframe,
80
87
  } = useResolvedTimelineEditCallbacks({
@@ -106,6 +113,25 @@ export const Timeline = memo(function Timeline({
106
113
  const timelineReady = usePlayerStore((s) => s.timelineReady);
107
114
  const selectedElementId = usePlayerStore((s) => s.selectedElementId);
108
115
  const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
116
+ const gsapAnimations = usePlayerStore((s) => s.gsapAnimations);
117
+ // Label mode = comp has keyframed clips (not just when expanded): keeps the layer
118
+ // disclosure + property column visible and reserves a GUTTER before 0s (Figma).
119
+ const hasKeyframedClips = useMemo(
120
+ () =>
121
+ Array.from(gsapAnimations.values()).some((list) =>
122
+ // Same lane-contribution predicate the layout uses: real keyframes OR a
123
+ // synthesizable flat tween. Checking animation.keyframes alone left a
124
+ // flat-tween-only comp without its reserved label column.
125
+ list.some((animation) => animationContributesLane(animation)),
126
+ ),
127
+ [gsapAnimations],
128
+ );
129
+ const labelMode = STUDIO_KEYFRAMES_ENABLED && hasKeyframedClips;
130
+ // Without the label column the pre-t=0 breathing room is still TRACKS_LEFT_PAD
131
+ // (dropping it would jam clip 0 against the gutter on every non-keyframed
132
+ // composition); in label mode the 232px label column already provides it.
133
+ const contentOrigin = labelMode ? LABEL_COL_W + GUTTER : GUTTER + TRACKS_LEFT_PAD;
134
+ const contentGutter = labelMode ? GUTTER : 0;
109
135
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
110
136
  const currentTime = usePlayerStore((s) => s.currentTime);
111
137
  const { zoomMode, manualZoomPercent, setZoomMode, setManualZoomPercent } = useTimelineZoom();
@@ -117,7 +143,6 @@ export const Timeline = memo(function Timeline({
117
143
  const [hoveredClip, setHoveredClip] = useState<string | null>(null);
118
144
  const isDragging = useRef(false);
119
145
  const [shiftHeld, setShiftHeld] = useState(false);
120
- const [razorGuideX, setRazorGuideX] = useState<number | null>(null);
121
146
 
122
147
  useMountEffect(() => {
123
148
  const key = (e: KeyboardEvent) => e.key === "Shift" && setShiftHeld(e.type === "keydown");
@@ -154,9 +179,10 @@ export const Timeline = memo(function Timeline({
154
179
  return Number.isFinite(result) ? result : safeDur;
155
180
  }, [rawElements, duration]);
156
181
 
157
- const { tracks, trackStyles, trackOrder } = useTimelineTrackDerivations(expandedElements);
158
- const trackOrderRef = useRef(trackOrder);
159
- trackOrderRef.current = trackOrder;
182
+ const keyframeCache = usePlayerStore((s) => s.keyframeCache);
183
+ useAutoExpandKeyframedClips(gsapAnimations);
184
+ const { tracks, trackStyles, trackOrder, trackOrderRef, laneCounts, rowHeights, rowHeightsRef } =
185
+ useTimelineTrackLayout(expandedElements, gsapAnimations, selectedElementId, selectedElementIds);
160
186
  const expandedElementsRef = useRef(expandedElements);
161
187
  expandedElementsRef.current = expandedElements;
162
188
 
@@ -223,6 +249,7 @@ export const Timeline = memo(function Timeline({
223
249
  ppsRef,
224
250
  durationRef,
225
251
  trackOrderRef,
252
+ rowHeightsRef,
226
253
  onMoveElement: pinnedOnMoveElement,
227
254
  onMoveElements: pinnedOnMoveElements,
228
255
  onResizeElement: pinnedOnResizeElement,
@@ -241,26 +268,32 @@ export const Timeline = memo(function Timeline({
241
268
  ppsRef,
242
269
  durationRef,
243
270
  trackOrderRef,
271
+ rowHeightsRef,
272
+ contentOrigin,
244
273
  onFileDrop: pinnedOnFileDrop,
245
274
  onAssetDrop: pinnedOnAssetDrop,
246
275
  onBlockDrop: pinnedOnBlockDrop,
247
276
  onCompositionDrop: pinnedOnCompositionDrop,
248
277
  });
249
278
 
250
- const displayTrackOrder = useMemo(() => {
251
- if (!draggedClip?.started || trackOrder.includes(draggedClip.previewTrack)) return trackOrder;
252
- return [...trackOrder, draggedClip.previewTrack].sort((a, b) => a - b);
253
- }, [draggedClip, trackOrder]);
254
-
255
- const totalH = getTimelineCanvasHeight(displayTrackOrder.length);
279
+ const displayLayout = useTimelineDisplayLayout(draggedClip, trackOrder, rowHeights);
256
280
  const { viewportWidth, showShortcutHint, setScrollRef } = useTimelineScrollViewport(scrollRef, [
257
281
  timelineReady,
258
282
  expandedElements.length,
259
- totalH,
283
+ displayLayout.totalH,
260
284
  ]);
261
- const keyframeCache = usePlayerStore((s) => s.keyframeCache);
262
285
  const selectedKeyframes = usePlayerStore((s) => s.selectedKeyframes);
263
286
  const toggleSelectedKeyframe = usePlayerStore((s) => s.toggleSelectedKeyframe);
287
+ const { onClickKeyframe, onSelectSegment, onShiftClickKeyframe, onContextMenuKeyframe } =
288
+ useTimelineKeyframeHandlers({
289
+ expandedElements,
290
+ keyframeCache,
291
+ onSelectElement,
292
+ onSeek,
293
+ setSelectedElementId,
294
+ setKfContextMenu,
295
+ toggleSelectedKeyframe,
296
+ });
264
297
 
265
298
  const selectedElement = useMemo(
266
299
  () =>
@@ -291,6 +324,7 @@ export const Timeline = memo(function Timeline({
291
324
  isDragging,
292
325
  scrollRef,
293
326
  lastScrollLeftRef,
327
+ contentOrigin,
294
328
  });
295
329
 
296
330
  const laneGapStrips = useTimelineGapHighlights({
@@ -323,12 +357,21 @@ export const Timeline = memo(function Timeline({
323
357
  setZoomMode,
324
358
  setManualZoomPercent,
325
359
  onSeek,
360
+ contentOrigin,
326
361
  });
327
362
  useTimelineActiveClips({
328
363
  scrollRef,
329
364
  currentTime,
330
365
  clipStateVersion,
331
366
  });
367
+ const { razorGuideX, updateRazorGuide, clearRazorGuide, splitAllAtPointer } =
368
+ useTimelineRazorInteraction({
369
+ active: activeTool === "razor",
370
+ scrollRef,
371
+ contentOrigin,
372
+ pixelsPerSecond: pps,
373
+ onSplitAll: onRazorSplitAll,
374
+ });
332
375
 
333
376
  const {
334
377
  rangeSelection,
@@ -352,7 +395,9 @@ export const Timeline = memo(function Timeline({
352
395
  setShowPopover,
353
396
  elementsRef: expandedElementsRef,
354
397
  trackOrderRef,
398
+ rowHeightsRef,
355
399
  onSelectElement,
400
+ contentOrigin,
356
401
  });
357
402
  setRangeSelectionRef.current = setRangeSelection; // stable ref consumed by useTimelineClipDrag
358
403
 
@@ -411,13 +456,8 @@ export const Timeline = memo(function Timeline({
411
456
  ref={setContainerRef}
412
457
  aria-label="Timeline"
413
458
  className={`relative border-t select-none h-full overflow-hidden ${isDragOver ? "ring-1 ring-inset ring-studio-accent/60" : ""} ${activeTool === "razor" ? "cursor-crosshair" : shiftHeld ? "cursor-crosshair" : "cursor-default"}`}
414
- onMouseMove={(e) => {
415
- if (activeTool === "razor" && scrollRef.current) {
416
- const rect = scrollRef.current.getBoundingClientRect();
417
- setRazorGuideX(e.clientX - rect.left + scrollRef.current.scrollLeft);
418
- }
419
- }}
420
- onMouseLeave={() => setRazorGuideX(null)}
459
+ onMouseMove={updateRazorGuide}
460
+ onMouseLeave={clearRazorGuide}
421
461
  style={{
422
462
  touchAction: "pan-x pan-y",
423
463
  background: theme.shellBackground,
@@ -435,14 +475,10 @@ export const Timeline = memo(function Timeline({
435
475
  onDragLeave={() => clearDropPreview()}
436
476
  onDrop={handleAssetDrop}
437
477
  onPointerDown={(e) => {
438
- if (activeTool === "razor" && e.shiftKey && e.button === 0 && scrollRef.current) {
439
- const rect = scrollRef.current.getBoundingClientRect();
440
- const x =
441
- e.clientX - rect.left + scrollRef.current.scrollLeft - GUTTER - TRACKS_LEFT_PAD;
442
- const splitTime = Math.max(0, x / pps);
443
- onRazorSplitAll?.(splitTime);
444
- return;
445
- }
478
+ // Let interactive controls (keyframe nav/toggle, caret, inputs) handle
479
+ // their own clicks — scrubbing here would preventDefault and eat them.
480
+ if (e.target instanceof Element && e.target.closest("button, input, select, a")) return;
481
+ if (splitAllAtPointer(e)) return;
446
482
  handlePointerDown(e);
447
483
  }}
448
484
  onPointerMove={handlePointerMove}
@@ -453,18 +489,22 @@ export const Timeline = memo(function Timeline({
453
489
  major={major}
454
490
  minor={minor}
455
491
  pps={pps}
492
+ contentOrigin={contentOrigin}
493
+ contentGutter={contentGutter}
456
494
  trackContentWidth={displayContentWidth}
457
- totalH={totalH}
495
+ totalH={displayLayout.totalH}
458
496
  effectiveDuration={effectiveDuration}
459
497
  majorTickInterval={majorTickInterval}
460
498
  rangeSelection={rangeSelection}
461
499
  marqueeRect={marqueeRect}
462
500
  laneGapStrips={laneGapStrips}
463
501
  theme={theme}
464
- displayTrackOrder={displayTrackOrder}
502
+ displayTrackOrder={displayLayout.displayTrackOrder}
503
+ rowHeights={displayLayout.displayRowHeights}
465
504
  trackOrder={trackOrder}
466
505
  tracks={tracks}
467
506
  trackStyles={trackStyles}
507
+ laneCounts={laneCounts}
468
508
  selectedElementId={selectedElementId}
469
509
  selectedElementIds={selectedElementIds}
470
510
  hoveredClip={hoveredClip}
@@ -490,43 +530,16 @@ export const Timeline = memo(function Timeline({
490
530
  getPreviewElement={getPreviewElement}
491
531
  getTrackStyle={getTrackStyle}
492
532
  keyframeCache={keyframeCache}
533
+ gsapAnimations={gsapAnimations}
493
534
  selectedKeyframes={selectedKeyframes}
494
535
  currentTime={currentTime}
536
+ onSeek={onSeek}
495
537
  beatAnalysis={adjustedBeatAnalysis}
496
- onClickKeyframe={(el, pct) => {
497
- usePlayerStore.getState().clearSelectedKeyframes();
498
- const elKey = el.key ?? el.id;
499
- setSelectedElementId(elKey);
500
- onSelectElement?.(el);
501
- // Select the clicked diamond (matches shift-click); cleared above so this single-selects.
502
- toggleSelectedKeyframe(`${elKey}:${pct}`);
503
- const absTime = el.start + (pct / 100) * el.duration;
504
- onSeek?.(absTime);
505
- const kfData = keyframeCache?.get(elKey);
506
- const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.5);
507
- usePlayerStore.getState().setActiveKeyframePct(kf?.tweenPercentage ?? null);
508
- }}
509
- onShiftClickKeyframe={(elId, pct) => {
510
- toggleSelectedKeyframe(`${elId}:${pct}`);
511
- }}
538
+ onSelectSegment={onSelectSegment}
539
+ onClickKeyframe={onClickKeyframe}
540
+ onShiftClickKeyframe={onShiftClickKeyframe}
512
541
  onMoveKeyframe={onMoveKeyframe}
513
- onContextMenuKeyframe={(e, elId, pct) => {
514
- const el = expandedElements.find((x) => (x.key ?? x.id) === elId);
515
- if (el) {
516
- setSelectedElementId(elId);
517
- onSelectElement?.(el);
518
- }
519
- const kfData = keyframeCache.get(elId);
520
- const kf = kfData?.keyframes.find((k) => Math.abs(k.percentage - pct) < 0.2);
521
- setKfContextMenu({
522
- x: e.clientX + 4,
523
- y: e.clientY + 2,
524
- elementId: elId,
525
- percentage: pct,
526
- tweenPercentage: kf?.tweenPercentage,
527
- currentEase: kf?.ease ?? kfData?.ease,
528
- });
529
- }}
542
+ onContextMenuKeyframe={onContextMenuKeyframe}
530
543
  onContextMenuClip={(e, el) => {
531
544
  e.preventDefault();
532
545
  setSelectedElementId(el.key ?? el.id);
@@ -540,16 +553,7 @@ export const Timeline = memo(function Timeline({
540
553
  openGapMenu({ x: e.clientX, y: e.clientY, track, time });
541
554
  }}
542
555
  />
543
- {activeTool === "razor" && razorGuideX !== null && (
544
- <div
545
- className="absolute top-0 bottom-0 pointer-events-none z-10"
546
- style={{
547
- left: razorGuideX,
548
- width: 1,
549
- background: "rgba(239,68,68,0.7)",
550
- }}
551
- />
552
- )}
556
+ {activeTool === "razor" && razorGuideX !== null && <TimelineRazorGuide x={razorGuideX} />}
553
557
  </div>
554
558
  <TimelineOverlays
555
559
  theme={theme}
@@ -562,9 +566,7 @@ export const Timeline = memo(function Timeline({
562
566
  setKfContextMenu={setKfContextMenu}
563
567
  onDeleteKeyframe={onDeleteKeyframe}
564
568
  onDeleteAllKeyframes={onDeleteAllKeyframes}
565
- onChangeKeyframeEase={onChangeKeyframeEase}
566
569
  onMoveKeyframeToPlayhead={onMoveKeyframeToPlayhead}
567
- keyframeCache={keyframeCache}
568
570
  clipContextMenu={clipContextMenu}
569
571
  setClipContextMenu={setClipContextMenu}
570
572
  currentTime={currentTime}
@@ -4,16 +4,15 @@ import { PlayheadIndicator } from "./PlayheadIndicator";
4
4
  import { getTimelineEditCapabilities, type TimelineRangeSelection } from "./timelineEditing";
5
5
  import { getRenderedTimelineElement } from "./timelineTheme";
6
6
  import {
7
- GUTTER,
8
- TRACK_H,
9
7
  RULER_H,
10
8
  CLIP_Y,
11
9
  TRACKS_TOP_PAD,
12
10
  TRACKS_BOTTOM_PAD,
13
- TRACKS_LEFT_PAD,
11
+ TRACK_H,
14
12
  PLAYHEAD_HEAD_W,
15
13
  getTimelinePlayheadLeft,
16
14
  getTimelineRowTop,
15
+ getTimelineRowHeight,
17
16
  } from "./timelineLayout";
18
17
  import { usePlayerStore } from "../store/playerStore";
19
18
  import type { ResizingClipState } from "./useTimelineClipDrag";
@@ -45,8 +44,22 @@ interface TimelineCanvasProps extends TimelineLaneBaseProps {
45
44
 
46
45
  export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvasProps) {
47
46
  const { draggedClip, scrollRef, selectedElementIds, displayTrackOrder } = props;
48
- const { onResizeElement, onMoveElement, onToggleTrackHidden, onRazorSplit, onRazorSplitAll } =
49
- useTimelineEditContextOptional();
47
+ const draggedRowIndex =
48
+ draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
49
+ const draggedRowHeight = getTimelineRowHeight(draggedRowIndex, props.rowHeights);
50
+ // A clip bar in an EXPANDED row still renders at TRACK_H (the property lanes
51
+ // occupy the rest of the row — see TimelineLanes' clipHeight), so the drag
52
+ // ghost and drop placeholder must clamp to it or they stretch to the full
53
+ // expanded row height and stop matching the clip being dragged.
54
+ const draggedClipHeight = Math.min(draggedRowHeight, TRACK_H) - CLIP_Y * 2;
55
+ const {
56
+ onResizeElement,
57
+ onMoveElement,
58
+ onToggleTrackHidden,
59
+ onTogglePropertyGroupKeyframe,
60
+ onRazorSplit,
61
+ onRazorSplitAll,
62
+ } = useTimelineEditContextOptional();
50
63
  const beatDragging = usePlayerStore((s) => s.beatDragging);
51
64
  // Scroll a clip into view when the sidebar (asset card) requests a reveal.
52
65
  useTimelineRevealClip(scrollRef);
@@ -63,8 +76,6 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
63
76
  // The drag ghost follows the cursor freely (both axes) — CapCut-style. The
64
77
  // "magnetic" affordance is a highlight on the destination lane (draggedRowIndex),
65
78
  // which flips at the MAGNETIC_TRACK_THRESHOLD point; the clip drops into it.
66
- const draggedRowIndex =
67
- draggedClip?.started === true ? displayTrackOrder.indexOf(draggedClip.previewTrack) : -1;
68
79
  // Live multi-selection drag: while a selected clip is dragged, ALL selected
69
80
  // clips move together as one rigid formation. The GRABBED clip is the free
70
81
  // ghost below; its co-selected "passengers" slide by the SAME group-clamped
@@ -101,7 +112,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
101
112
  return (
102
113
  <div
103
114
  className="relative"
104
- style={{ height: props.totalH, width: GUTTER + TRACKS_LEFT_PAD + props.trackContentWidth }}
115
+ style={{ height: props.totalH, width: props.contentOrigin + props.trackContentWidth }}
105
116
  >
106
117
  <TimelineRuler
107
118
  major={props.major}
@@ -113,6 +124,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
113
124
  majorTickInterval={props.majorTickInterval}
114
125
  theme={props.theme}
115
126
  beatAnalysis={props.beatAnalysis}
127
+ contentOrigin={props.contentOrigin}
116
128
  />
117
129
 
118
130
  {/* Breathing room between the sticky ruler and the first track lane — the
@@ -124,6 +136,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
124
136
  draggedElement={draggedElement}
125
137
  multiDragPreview={multiDragPreview}
126
138
  onToggleTrackHidden={onToggleTrackHidden}
139
+ onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
127
140
  onResizeElement={onResizeElement}
128
141
  onMoveElement={onMoveElement}
129
142
  onRazorSplit={onRazorSplit}
@@ -148,8 +161,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
148
161
  key={`gap-${strip.kind}-${strip.track}-${gap.start}`}
149
162
  className="pointer-events-none absolute"
150
163
  style={{
151
- top: getTimelineRowTop(rowIndex) + CLIP_Y,
152
- left: GUTTER + TRACKS_LEFT_PAD + gap.start * props.pps,
164
+ top: getTimelineRowTop(rowIndex, props.rowHeights) + CLIP_Y,
165
+ left: props.contentOrigin + gap.start * props.pps,
153
166
  width: Math.max((gap.end - gap.start) * props.pps, 2),
154
167
  height: TRACK_H - CLIP_Y * 2,
155
168
  background: loud ? "rgba(60,230,172,0.18)" : "rgba(60,230,172,0.055)",
@@ -166,10 +179,10 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
166
179
  <div
167
180
  className="absolute pointer-events-none"
168
181
  style={{
169
- top: getTimelineRowTop(draggedRowIndex) + CLIP_Y,
170
- left: GUTTER + TRACKS_LEFT_PAD + draggedClip.previewStart * props.pps,
182
+ top: getTimelineRowTop(draggedRowIndex, props.rowHeights) + CLIP_Y,
183
+ left: props.contentOrigin + draggedClip.previewStart * props.pps,
171
184
  width: Math.max(draggedClip.element.duration * props.pps, 4),
172
- height: TRACK_H - CLIP_Y * 2,
185
+ height: draggedClipHeight,
173
186
  border: "1px solid rgba(60,230,172,0.55)",
174
187
  background: "rgba(60,230,172,0.12)",
175
188
  borderRadius: 4,
@@ -184,8 +197,8 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
184
197
  <div
185
198
  className="absolute pointer-events-none"
186
199
  style={{
187
- top: getTimelineRowTop(draggedClip.insertRow) - 0.5,
188
- left: GUTTER + TRACKS_LEFT_PAD,
200
+ top: getTimelineRowTop(draggedClip.insertRow, props.rowHeights) - 0.5,
201
+ left: props.contentOrigin,
189
202
  width: props.trackContentWidth,
190
203
  height: 1,
191
204
  background: "#3CE6AC",
@@ -200,7 +213,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
200
213
  <div
201
214
  className="absolute pointer-events-none"
202
215
  style={{
203
- left: GUTTER + TRACKS_LEFT_PAD + draggedClip.snapTime * props.pps,
216
+ left: props.contentOrigin + draggedClip.snapTime * props.pps,
204
217
  top: RULER_H,
205
218
  bottom: 0,
206
219
  width: 1,
@@ -222,7 +235,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
222
235
  top: activeDraggedPosition.top,
223
236
  left: activeDraggedPosition.left,
224
237
  width: Math.max(activeDraggedElement.duration * props.pps, 4),
225
- height: TRACK_H - CLIP_Y * 2,
238
+ height: draggedClipHeight,
226
239
  zIndex: 40,
227
240
  }}
228
241
  >
@@ -280,8 +293,7 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
280
293
  className="absolute pointer-events-none"
281
294
  style={{
282
295
  left:
283
- GUTTER +
284
- TRACKS_LEFT_PAD +
296
+ props.contentOrigin +
285
297
  Math.min(props.rangeSelection.start, props.rangeSelection.end) * props.pps,
286
298
  width: Math.abs(props.rangeSelection.end - props.rangeSelection.start) * props.pps,
287
299
  top: RULER_H,
@@ -297,13 +309,13 @@ export const TimelineCanvas = memo(function TimelineCanvas(props: TimelineCanvas
297
309
  {/* Playhead — hidden while dragging a beat so its guideline doesn't
298
310
  track the scrub and clutter the beat being moved. Explicit width +
299
311
  the half-head offset baked into getTimelinePlayheadLeft keep the
300
- inner 1px line's CENTER exactly on GUTTER + t * pps (the ruler
312
+ inner 1px line's CENTER exactly on contentOrigin + t * pps (the ruler
301
313
  ticks' center), instead of relying on shrink-wrap sizing. */}
302
314
  <div
303
315
  ref={props.playheadRef}
304
316
  className="absolute top-0 bottom-0 pointer-events-none"
305
317
  style={{
306
- left: `${getTimelinePlayheadLeft(0, 0)}px`,
318
+ left: `${getTimelinePlayheadLeft(0, 0, props.contentOrigin)}px`,
307
319
  width: PLAYHEAD_HEAD_W,
308
320
  zIndex: 100,
309
321
  display: beatDragging ? "none" : undefined,
@@ -8,6 +8,7 @@ interface TimelineClipProps {
8
8
  el: TimelineElement;
9
9
  pps: number;
10
10
  clipY: number;
11
+ clipHeight?: number;
11
12
  isSelected: boolean;
12
13
  isHovered: boolean;
13
14
  isDragging?: boolean;
@@ -30,6 +31,7 @@ export const TimelineClip = memo(function TimelineClip({
30
31
  el,
31
32
  pps,
32
33
  clipY,
34
+ clipHeight,
33
35
  isSelected,
34
36
  isHovered,
35
37
  isDragging = false,
@@ -71,7 +73,7 @@ export const TimelineClip = memo(function TimelineClip({
71
73
  left: leftPx,
72
74
  width: widthPx,
73
75
  top: clipY,
74
- bottom: clipY,
76
+ ...(clipHeight === undefined ? { bottom: clipY } : { height: clipHeight }),
75
77
  borderRadius: theme.clipRadius,
76
78
  zIndex: isDragging ? 20 : isSelected ? 10 : isHovered ? 5 : 1,
77
79
  // Regular cursor over clips (CapCut-style, user preference) — no grab hand.