@hyperframes/studio 0.6.73 → 0.6.75

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 (63) hide show
  1. package/dist/assets/index-DcyZuBcU.css +1 -0
  2. package/dist/assets/index-uB_W2GDl.js +140 -0
  3. package/dist/index.html +2 -2
  4. package/package.json +4 -4
  5. package/src/App.tsx +30 -24
  6. package/src/components/StudioPreviewArea.tsx +101 -26
  7. package/src/components/StudioRightPanel.tsx +3 -0
  8. package/src/components/StudioToast.tsx +18 -0
  9. package/src/components/TimelineToolbar.tsx +230 -4
  10. package/src/components/editor/AnimationCard.tsx +68 -4
  11. package/src/components/editor/DomEditOverlay.tsx +70 -1
  12. package/src/components/editor/GridOverlay.tsx +50 -0
  13. package/src/components/editor/KeyframeDiamond.tsx +49 -0
  14. package/src/components/editor/KeyframeNavigation.tsx +139 -0
  15. package/src/components/editor/LayersPanel.test.ts +135 -0
  16. package/src/components/editor/LayersPanel.tsx +151 -15
  17. package/src/components/editor/PropertyPanel.tsx +293 -140
  18. package/src/components/editor/SnapGuideOverlay.tsx +166 -0
  19. package/src/components/editor/SnapToolbar.tsx +163 -0
  20. package/src/components/editor/SpringEaseEditor.tsx +256 -0
  21. package/src/components/editor/domEditOverlayGestures.ts +7 -0
  22. package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
  23. package/src/components/editor/gsapAnimationConstants.ts +42 -0
  24. package/src/components/editor/gsapAnimationHelpers.ts +2 -1
  25. package/src/components/editor/manualEditingAvailability.ts +6 -0
  26. package/src/components/editor/manualEditsDom.ts +56 -2
  27. package/src/components/editor/manualOffsetDrag.ts +19 -3
  28. package/src/components/editor/propertyPanelHelpers.ts +90 -0
  29. package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
  30. package/src/components/editor/snapEngine.test.ts +657 -0
  31. package/src/components/editor/snapEngine.ts +575 -0
  32. package/src/components/editor/snapTargetCollection.ts +147 -0
  33. package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
  34. package/src/components/editor/useLayerDrag.ts +213 -0
  35. package/src/components/nle/NLELayout.tsx +18 -0
  36. package/src/contexts/DomEditContext.tsx +27 -0
  37. package/src/hooks/gsapRuntimeBridge.ts +585 -0
  38. package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
  39. package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
  40. package/src/hooks/useAppHotkeys.ts +63 -1
  41. package/src/hooks/useDomEditCommits.ts +88 -4
  42. package/src/hooks/useDomEditSession.ts +179 -65
  43. package/src/hooks/useGsapScriptCommits.ts +144 -7
  44. package/src/hooks/useGsapSelectionHandlers.ts +202 -0
  45. package/src/hooks/useGsapTweenCache.ts +174 -3
  46. package/src/hooks/useTimelineEditing.ts +93 -0
  47. package/src/icons/SystemIcons.tsx +2 -0
  48. package/src/player/components/ClipContextMenu.tsx +99 -0
  49. package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
  50. package/src/player/components/Timeline.test.ts +2 -1
  51. package/src/player/components/Timeline.tsx +108 -68
  52. package/src/player/components/TimelineCanvas.tsx +47 -1
  53. package/src/player/components/TimelineClip.tsx +8 -3
  54. package/src/player/components/TimelineClipDiamonds.tsx +174 -0
  55. package/src/player/components/timelineDragDrop.ts +103 -0
  56. package/src/player/components/timelineLayout.ts +1 -1
  57. package/src/player/store/playerStore.ts +42 -0
  58. package/src/utils/editHistory.ts +1 -1
  59. package/src/utils/optimisticUpdate.test.ts +53 -0
  60. package/src/utils/optimisticUpdate.ts +18 -0
  61. package/src/utils/studioUiPreferences.ts +17 -0
  62. package/dist/assets/index-CrxThtSJ.css +0 -1
  63. package/dist/assets/index-Dc2HfqON.js +0 -140
@@ -1,3 +1,4 @@
1
+ // fallow-ignore-file code-duplication
1
2
  /**
2
3
  * Gesture handling for DomEditOverlay.
3
4
  * Owns: onPointerMove, onPointerUp, clearPointerState.
@@ -23,7 +24,12 @@ import {
23
24
  restoreStudioPathOffset,
24
25
  restoreStudioRotation,
25
26
  } from "./manualEdits";
26
- import { type GroupOverlayItem, type OverlayRect, toOverlayRect } from "./domEditOverlayGeometry";
27
+ import {
28
+ type GroupOverlayItem,
29
+ type OverlayRect,
30
+ resolveDomEditGroupOverlayRect,
31
+ toOverlayRect,
32
+ } from "./domEditOverlayGeometry";
27
33
  import {
28
34
  BLOCKED_MOVE_THRESHOLD_PX,
29
35
  type BlockedMoveState,
@@ -39,6 +45,13 @@ import {
39
45
  startGesture as _startGesture,
40
46
  startGroupDrag as _startGroupDrag,
41
47
  } from "./domEditOverlayStartGesture";
48
+ import {
49
+ resolveSnapAdjustment,
50
+ resolveResizeSnapAdjustment,
51
+ resolveEquidistanceGuides,
52
+ SNAP_THRESHOLD_PX,
53
+ } from "./snapEngine";
54
+ import type { SnapGuidesState } from "./SnapGuideOverlay";
42
55
 
43
56
  // Refs are stable across renders; values are read via .current.
44
57
  export type UseDomEditOverlayGesturesOptions = {
@@ -79,6 +92,7 @@ export type UseDomEditOverlayGesturesOptions = {
79
92
  e: React.MouseEvent<HTMLDivElement>,
80
93
  o?: { preferClipAncestor?: boolean },
81
94
  ) => void;
95
+ snapGuidesRef: RefObject<SnapGuidesState | null>;
82
96
  };
83
97
 
84
98
  export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) {
@@ -111,6 +125,7 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
111
125
  options?: { selection?: DomEditSelection; rect?: OverlayRect | null },
112
126
  ) => _startGesture(kind, e, opts, options);
113
127
 
128
+ // fallow-ignore-next-line complexity
114
129
  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
115
130
  const g = opts.gestureRef.current;
116
131
  const groupG = opts.groupGestureRef.current;
@@ -133,8 +148,48 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
133
148
  }
134
149
 
135
150
  if (groupG) {
136
- const dx = e.clientX - groupG.startX;
137
- const dy = e.clientY - groupG.startY;
151
+ let dx = e.clientX - groupG.startX;
152
+ let dy = e.clientY - groupG.startY;
153
+
154
+ const sc = groupG.snapContext;
155
+ if (sc?.snapEnabled && sc.targets.length > 0) {
156
+ const groupBounds = resolveDomEditGroupOverlayRect(
157
+ groupG.originItems.map((item) => item.rect),
158
+ );
159
+ if (groupBounds) {
160
+ const allTargets = sc.compositionTarget
161
+ ? [...sc.targets, sc.compositionTarget]
162
+ : sc.targets;
163
+ const snap = resolveSnapAdjustment({
164
+ movingRect: groupBounds,
165
+ proposedDx: dx,
166
+ proposedDy: dy,
167
+ targets: allTargets,
168
+ gridEdges: sc.gridEdges ?? undefined,
169
+ threshold: SNAP_THRESHOLD_PX,
170
+ disabled: e.altKey,
171
+ });
172
+ dx = snap.dx;
173
+ dy = snap.dy;
174
+ const movedRect = {
175
+ left: groupBounds.left + dx,
176
+ top: groupBounds.top + dy,
177
+ width: groupBounds.width,
178
+ height: groupBounds.height,
179
+ };
180
+ const spacingGuides = e.altKey
181
+ ? []
182
+ : resolveEquidistanceGuides({
183
+ movingRect: movedRect,
184
+ targets: allTargets,
185
+ threshold: SNAP_THRESHOLD_PX,
186
+ });
187
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
188
+ }
189
+ }
190
+ groupG.lastSnappedDx = dx;
191
+ groupG.lastSnappedDy = dy;
192
+
138
193
  setDraftGroupOverlayItems(
139
194
  groupG.originItems.map((item) => ({
140
195
  ...item,
@@ -146,8 +201,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
146
201
  }
147
202
 
148
203
  if (!g || !sel) return;
149
- const dx = e.clientX - g.startX;
150
- const dy = e.clientY - g.startY;
204
+ let dx = e.clientX - g.startX;
205
+ let dy = e.clientY - g.startY;
151
206
 
152
207
  if (g.kind === "rotate") {
153
208
  applyStudioRotationDraft(
@@ -167,6 +222,46 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
167
222
  }
168
223
 
169
224
  if (g.kind === "drag") {
225
+ const sc = g.snapContext;
226
+ if (sc?.snapEnabled && sc.targets.length > 0) {
227
+ const movingRect = {
228
+ left: g.originLeft,
229
+ top: g.originTop,
230
+ width: g.originWidth,
231
+ height: g.originHeight,
232
+ };
233
+ const allTargets = sc.compositionTarget
234
+ ? [...sc.targets, sc.compositionTarget]
235
+ : sc.targets;
236
+ const snap = resolveSnapAdjustment({
237
+ movingRect,
238
+ proposedDx: dx,
239
+ proposedDy: dy,
240
+ targets: allTargets,
241
+ gridEdges: sc.gridEdges ?? undefined,
242
+ threshold: SNAP_THRESHOLD_PX,
243
+ disabled: e.altKey,
244
+ });
245
+ dx = snap.dx;
246
+ dy = snap.dy;
247
+ const movedRect = {
248
+ left: movingRect.left + dx,
249
+ top: movingRect.top + dy,
250
+ width: movingRect.width,
251
+ height: movingRect.height,
252
+ };
253
+ const spacingGuides = e.altKey
254
+ ? []
255
+ : resolveEquidistanceGuides({
256
+ movingRect: movedRect,
257
+ targets: allTargets,
258
+ threshold: SNAP_THRESHOLD_PX,
259
+ });
260
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
261
+ }
262
+ g.lastSnappedDx = dx;
263
+ g.lastSnappedDy = dy;
264
+
170
265
  const nextBoxLeft = g.originLeft + dx;
171
266
  const nextBoxTop = g.originTop + dy;
172
267
  setDraftOverlayRect({
@@ -184,6 +279,32 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
184
279
  if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
185
280
  } else {
186
281
  if (!box) return;
282
+
283
+ const sc = g.snapContext;
284
+ if (sc?.snapEnabled && sc.targets.length > 0) {
285
+ const movingRect = {
286
+ left: g.originLeft,
287
+ top: g.originTop,
288
+ width: g.originWidth,
289
+ height: g.originHeight,
290
+ };
291
+ const allTargets = sc.compositionTarget
292
+ ? [...sc.targets, sc.compositionTarget]
293
+ : sc.targets;
294
+ const snap = resolveResizeSnapAdjustment({
295
+ movingRect,
296
+ proposedDx: dx,
297
+ proposedDy: dy,
298
+ targets: allTargets,
299
+ gridEdges: sc.gridEdges ?? undefined,
300
+ threshold: SNAP_THRESHOLD_PX,
301
+ disabled: e.altKey,
302
+ });
303
+ dx = snap.dx;
304
+ dy = snap.dy;
305
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides: [] };
306
+ }
307
+
187
308
  const nextSize = resolveDomEditResizeGesture({
188
309
  originWidth: g.originWidth,
189
310
  originHeight: g.originHeight,
@@ -223,7 +344,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
223
344
  }
224
345
  };
225
346
 
347
+ // fallow-ignore-next-line complexity
226
348
  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
349
+ opts.snapGuidesRef.current = null;
227
350
  const g = opts.gestureRef.current;
228
351
  const groupG = opts.groupGestureRef.current;
229
352
  const sel = g?.selection ?? opts.selectionRef.current;
@@ -233,13 +356,15 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
233
356
  if (groupG) {
234
357
  opts.groupGestureRef.current = null;
235
358
  opts.rafPausedRef.current = false;
236
- const dx = e.clientX - groupG.startX;
237
- const dy = e.clientY - groupG.startY;
238
- if (Math.hypot(dx, dy) < BLOCKED_MOVE_THRESHOLD_PX) {
359
+ const rawDx = e.clientX - groupG.startX;
360
+ const rawDy = e.clientY - groupG.startY;
361
+ if (Math.hypot(rawDx, rawDy) < BLOCKED_MOVE_THRESHOLD_PX) {
239
362
  restoreGroupPathOffsets(groupG);
240
363
  opts.suppressNextBoxClickRef.current = true;
241
364
  return;
242
365
  }
366
+ const dx = groupG.lastSnappedDx ?? rawDx;
367
+ const dy = groupG.lastSnappedDy ?? rawDy;
243
368
  setDraftGroupOverlayItems(
244
369
  groupG.originItems.map((item) => ({
245
370
  ...item,
@@ -327,8 +452,8 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
327
452
  })
328
453
  .finally(() => endStudioManualEditGesture(sel.element, g.manualEditDragToken));
329
454
  } else if (g.kind === "drag") {
330
- const dx = e.clientX - g.startX;
331
- const dy = e.clientY - g.startY;
455
+ const dx = g.lastSnappedDx ?? e.clientX - g.startX;
456
+ const dy = g.lastSnappedDy ?? e.clientY - g.startY;
332
457
  if (!g.pathOffsetMember) return;
333
458
  const finalOffset = applyManualOffsetDragCommit(g.pathOffsetMember, dx, dy);
334
459
  const nextBoxLeft = g.originLeft + dx;
@@ -372,7 +497,9 @@ export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGestu
372
497
  }
373
498
  };
374
499
 
500
+ // fallow-ignore-next-line complexity
375
501
  const clearPointerState = (selectionRef: RefObject<DomEditSelection | null>) => {
502
+ opts.snapGuidesRef.current = null;
376
503
  const groupG = opts.groupGestureRef.current;
377
504
  if (groupG) restoreGroupPathOffsets(groupG);
378
505
  const g = opts.gestureRef.current;
@@ -0,0 +1,213 @@
1
+ import { useRef, useState, useCallback } from "react";
2
+ import type { DomEditLayerItem } from "./domEditingTypes";
3
+
4
+ const DRAG_THRESHOLD_PX = 4;
5
+
6
+ interface DragState {
7
+ pointerId: number;
8
+ startY: number;
9
+ dragLayerIndex: number;
10
+ siblingIndices: number[];
11
+ fromSiblingPos: number;
12
+ insertSiblingPos: number;
13
+ siblingRects: DOMRect[];
14
+ activated: boolean;
15
+ }
16
+
17
+ export interface LayerReorderEvent {
18
+ siblingLayers: DomEditLayerItem[];
19
+ fromIndex: number;
20
+ toIndex: number;
21
+ }
22
+
23
+ export interface UseLayerDragOptions {
24
+ visibleLayers: DomEditLayerItem[];
25
+ scrollContainerRef: React.RefObject<HTMLDivElement | null>;
26
+ onReorder: (event: LayerReorderEvent) => void;
27
+ onSingleSibling?: () => void;
28
+ }
29
+
30
+ export interface UseLayerDragReturn {
31
+ dragKey: string | null;
32
+ insertionLineY: number | null;
33
+ handleRowPointerDown: (layerIndex: number, e: React.PointerEvent) => void;
34
+ handleContainerPointerMove: (e: React.PointerEvent) => void;
35
+ handleContainerPointerUp: () => void;
36
+ }
37
+
38
+ export function isLayerDraggable(layer: DomEditLayerItem): boolean {
39
+ if (!(layer.selector || layer.id)) return false;
40
+ let el: HTMLElement | null = layer.element;
41
+ while (el) {
42
+ if (el.hasAttribute("data-timeline-locked")) return false;
43
+ el = el.parentElement;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function findSiblingIndices(visibleLayers: DomEditLayerItem[], layerIndex: number): number[] {
49
+ const depth = visibleLayers[layerIndex].depth;
50
+ const indices: number[] = [];
51
+
52
+ let start = layerIndex;
53
+ while (start > 0) {
54
+ start--;
55
+ if (visibleLayers[start].depth < depth) {
56
+ start++;
57
+ break;
58
+ }
59
+ }
60
+
61
+ for (let i = start; i < visibleLayers.length; i++) {
62
+ const d = visibleLayers[i].depth;
63
+ if (d < depth) break;
64
+ if (d === depth) indices.push(i);
65
+ }
66
+
67
+ return indices;
68
+ }
69
+
70
+ function measureSiblingRects(container: HTMLDivElement, siblingIndices: number[]): DOMRect[] {
71
+ const rows = container.querySelectorAll<HTMLElement>("[data-layer-index]");
72
+ const rects: DOMRect[] = [];
73
+ for (const idx of siblingIndices) {
74
+ for (const row of rows) {
75
+ if (row.dataset.layerIndex === String(idx)) {
76
+ rects.push(row.getBoundingClientRect());
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ return rects;
82
+ }
83
+
84
+ function computeInsertionPos(clientY: number, siblingRects: DOMRect[]): number {
85
+ if (siblingRects.length === 0) return 0;
86
+
87
+ if (clientY <= siblingRects[0].top + siblingRects[0].height / 2) return 0;
88
+
89
+ for (let i = 0; i < siblingRects.length - 1; i++) {
90
+ const midpoint = (siblingRects[i].bottom + siblingRects[i + 1].top) / 2;
91
+ if (clientY <= midpoint) return i + 1;
92
+ }
93
+
94
+ const last = siblingRects[siblingRects.length - 1];
95
+ if (clientY <= last.top + last.height / 2) return siblingRects.length - 1;
96
+
97
+ return siblingRects.length;
98
+ }
99
+
100
+ function computeInsertionLineY(
101
+ insertPos: number,
102
+ siblingRects: DOMRect[],
103
+ containerRect: DOMRect,
104
+ ): number | null {
105
+ if (siblingRects.length === 0) return null;
106
+ if (insertPos <= 0) return siblingRects[0].top - containerRect.top;
107
+ if (insertPos >= siblingRects.length) {
108
+ return siblingRects[siblingRects.length - 1].bottom - containerRect.top;
109
+ }
110
+ return siblingRects[insertPos].top - containerRect.top;
111
+ }
112
+
113
+ export function useLayerDrag({
114
+ visibleLayers,
115
+ scrollContainerRef,
116
+ onReorder,
117
+ onSingleSibling,
118
+ }: UseLayerDragOptions): UseLayerDragReturn {
119
+ const dragRef = useRef<DragState | null>(null);
120
+ const [dragKey, setDragKey] = useState<string | null>(null);
121
+ const [insertionLineY, setInsertionLineY] = useState<number | null>(null);
122
+
123
+ const handleRowPointerDown = useCallback(
124
+ (layerIndex: number, e: React.PointerEvent) => {
125
+ if (e.button !== 0) return;
126
+ const layer = visibleLayers[layerIndex];
127
+ if (!layer || !isLayerDraggable(layer)) return;
128
+
129
+ const siblingIndices = findSiblingIndices(visibleLayers, layerIndex);
130
+ if (siblingIndices.length <= 1) {
131
+ onSingleSibling?.();
132
+ return;
133
+ }
134
+
135
+ const fromSiblingPos = siblingIndices.indexOf(layerIndex);
136
+ if (fromSiblingPos === -1) return;
137
+
138
+ const container = scrollContainerRef.current;
139
+ if (!container) return;
140
+
141
+ e.preventDefault();
142
+ container.setPointerCapture(e.pointerId);
143
+
144
+ dragRef.current = {
145
+ pointerId: e.pointerId,
146
+ startY: e.clientY,
147
+ dragLayerIndex: layerIndex,
148
+ siblingIndices,
149
+ fromSiblingPos,
150
+ insertSiblingPos: fromSiblingPos,
151
+ siblingRects: measureSiblingRects(container, siblingIndices),
152
+ activated: false,
153
+ };
154
+ },
155
+ [visibleLayers, scrollContainerRef, onSingleSibling],
156
+ );
157
+
158
+ const handleContainerPointerMove = useCallback(
159
+ (e: React.PointerEvent) => {
160
+ const drag = dragRef.current;
161
+ if (!drag) return;
162
+
163
+ if (!drag.activated) {
164
+ if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
165
+ drag.activated = true;
166
+ setDragKey(visibleLayers[drag.dragLayerIndex]?.key ?? null);
167
+ }
168
+
169
+ const insertPos = computeInsertionPos(e.clientY, drag.siblingRects);
170
+ drag.insertSiblingPos = insertPos;
171
+
172
+ const container = scrollContainerRef.current;
173
+ if (container) {
174
+ const containerRect = container.getBoundingClientRect();
175
+ setInsertionLineY(computeInsertionLineY(insertPos, drag.siblingRects, containerRect));
176
+ }
177
+ },
178
+ [visibleLayers, scrollContainerRef],
179
+ );
180
+
181
+ const handleContainerPointerUp = useCallback(() => {
182
+ const drag = dragRef.current;
183
+ dragRef.current = null;
184
+ setDragKey(null);
185
+ setInsertionLineY(null);
186
+
187
+ if (!drag || !drag.activated) return;
188
+
189
+ const container = scrollContainerRef.current;
190
+ if (container) {
191
+ try {
192
+ container.releasePointerCapture(drag.pointerId);
193
+ } catch {
194
+ // already released
195
+ }
196
+ }
197
+
198
+ let toPos = drag.insertSiblingPos;
199
+ if (toPos > drag.fromSiblingPos) toPos--;
200
+ if (toPos === drag.fromSiblingPos) return;
201
+
202
+ const siblingLayers = drag.siblingIndices.map((i) => visibleLayers[i]);
203
+ onReorder({ siblingLayers, fromIndex: drag.fromSiblingPos, toIndex: toPos });
204
+ }, [visibleLayers, scrollContainerRef, onReorder]);
205
+
206
+ return {
207
+ dragKey,
208
+ insertionLineY,
209
+ handleRowPointerDown,
210
+ handleContainerPointerMove,
211
+ handleContainerPointerUp,
212
+ };
213
+ }
@@ -69,7 +69,13 @@ interface NLELayoutProps {
69
69
  updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
70
70
  ) => Promise<void> | void;
71
71
  onBlockedEditAttempt?: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
72
+ onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
72
73
  onSelectTimelineElement?: (element: TimelineElement | null) => void;
74
+ onDeleteKeyframe?: (elementId: string, percentage: number) => void;
75
+ onDeleteAllKeyframes?: (elementId: string) => void;
76
+ onChangeKeyframeEase?: (elementId: string, percentage: number, ease: string) => void;
77
+ onMoveKeyframe?: (element: TimelineElement, oldPct: number, newPct: number) => void;
78
+ onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
73
79
  /** Exposes the compIdToSrc map for parent components (e.g., useRenderClipContent) */
74
80
  onCompIdToSrcChange?: (map: Map<string, string>) => void;
75
81
  /** Whether the timeline panel is visible (default: true) */
@@ -117,7 +123,13 @@ export const NLELayout = memo(function NLELayout({
117
123
  onMoveElement,
118
124
  onResizeElement,
119
125
  onBlockedEditAttempt,
126
+ onSplitElement,
120
127
  onSelectTimelineElement,
128
+ onDeleteKeyframe,
129
+ onDeleteAllKeyframes,
130
+ onChangeKeyframeEase,
131
+ onMoveKeyframe,
132
+ onToggleKeyframeAtPlayhead,
121
133
  onCompIdToSrcChange,
122
134
  timelineVisible,
123
135
  onToggleTimeline,
@@ -447,7 +459,13 @@ export const NLELayout = memo(function NLELayout({
447
459
  onMoveElement={onMoveElement}
448
460
  onResizeElement={onResizeElement}
449
461
  onBlockedEditAttempt={onBlockedEditAttempt}
462
+ onSplitElement={onSplitElement}
450
463
  onSelectElement={onSelectTimelineElement}
464
+ onDeleteKeyframe={onDeleteKeyframe}
465
+ onDeleteAllKeyframes={onDeleteAllKeyframes}
466
+ onChangeKeyframeEase={onChangeKeyframeEase}
467
+ onMoveKeyframe={onMoveKeyframe}
468
+ onToggleKeyframeAtPlayhead={onToggleKeyframeAtPlayhead}
451
469
  />
452
470
  </div>
453
471
  {timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
@@ -32,6 +32,7 @@ export function DomEditProvider({
32
32
  handleDomHtmlAttributeCommit,
33
33
  handleDomPathOffsetCommit,
34
34
  handleDomGroupPathOffsetCommit,
35
+ handleDomZIndexReorderCommit,
35
36
  handleDomBoxSizeCommit,
36
37
  handleDomRotationCommit,
37
38
  handleDomManualEditsReset,
@@ -65,6 +66,14 @@ export function DomEditProvider({
65
66
  handleGsapUpdateFromProperty,
66
67
  handleGsapAddFromProperty,
67
68
  handleGsapRemoveFromProperty,
69
+ handleGsapAddKeyframe,
70
+ handleGsapRemoveKeyframe,
71
+ handleGsapConvertToKeyframes,
72
+ handleGsapRemoveAllKeyframes,
73
+ handleResetSelectedElementKeyframes,
74
+ commitAnimatedProperty,
75
+ invalidateGsapCache,
76
+ previewIframeRef,
68
77
  },
69
78
  children,
70
79
  }: {
@@ -92,6 +101,7 @@ export function DomEditProvider({
92
101
  handleDomHtmlAttributeCommit,
93
102
  handleDomPathOffsetCommit,
94
103
  handleDomGroupPathOffsetCommit,
104
+ handleDomZIndexReorderCommit,
95
105
  handleDomBoxSizeCommit,
96
106
  handleDomRotationCommit,
97
107
  handleDomManualEditsReset,
@@ -125,6 +135,14 @@ export function DomEditProvider({
125
135
  handleGsapUpdateFromProperty,
126
136
  handleGsapAddFromProperty,
127
137
  handleGsapRemoveFromProperty,
138
+ handleGsapAddKeyframe,
139
+ handleGsapRemoveKeyframe,
140
+ handleGsapConvertToKeyframes,
141
+ handleGsapRemoveAllKeyframes,
142
+ handleResetSelectedElementKeyframes,
143
+ commitAnimatedProperty,
144
+ invalidateGsapCache,
145
+ previewIframeRef,
128
146
  }),
129
147
  [
130
148
  domEditSelection,
@@ -146,6 +164,7 @@ export function DomEditProvider({
146
164
  handleDomHtmlAttributeCommit,
147
165
  handleDomPathOffsetCommit,
148
166
  handleDomGroupPathOffsetCommit,
167
+ handleDomZIndexReorderCommit,
149
168
  handleDomBoxSizeCommit,
150
169
  handleDomRotationCommit,
151
170
  handleDomManualEditsReset,
@@ -179,6 +198,14 @@ export function DomEditProvider({
179
198
  handleGsapUpdateFromProperty,
180
199
  handleGsapAddFromProperty,
181
200
  handleGsapRemoveFromProperty,
201
+ handleGsapAddKeyframe,
202
+ handleGsapRemoveKeyframe,
203
+ handleGsapConvertToKeyframes,
204
+ handleGsapRemoveAllKeyframes,
205
+ handleResetSelectedElementKeyframes,
206
+ commitAnimatedProperty,
207
+ invalidateGsapCache,
208
+ previewIframeRef,
182
209
  ],
183
210
  );
184
211
  return <DomEditContext value={stable}>{children}</DomEditContext>;