@hyperframes/studio 0.6.73 → 0.6.74

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 (60) hide show
  1. package/dist/assets/index-BcJO6Ej5.js +140 -0
  2. package/dist/assets/index-C2gBZ2km.css +1 -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/PropertyPanel.tsx +293 -140
  16. package/src/components/editor/SnapGuideOverlay.tsx +166 -0
  17. package/src/components/editor/SnapToolbar.tsx +163 -0
  18. package/src/components/editor/SpringEaseEditor.tsx +256 -0
  19. package/src/components/editor/domEditOverlayGestures.ts +7 -0
  20. package/src/components/editor/domEditOverlayStartGesture.ts +28 -0
  21. package/src/components/editor/gsapAnimationConstants.ts +42 -0
  22. package/src/components/editor/gsapAnimationHelpers.ts +2 -1
  23. package/src/components/editor/manualEditingAvailability.ts +6 -0
  24. package/src/components/editor/manualEditsDom.ts +56 -2
  25. package/src/components/editor/manualOffsetDrag.ts +19 -3
  26. package/src/components/editor/propertyPanelHelpers.ts +90 -0
  27. package/src/components/editor/propertyPanelTimingSection.tsx +64 -0
  28. package/src/components/editor/snapEngine.test.ts +657 -0
  29. package/src/components/editor/snapEngine.ts +575 -0
  30. package/src/components/editor/snapTargetCollection.ts +147 -0
  31. package/src/components/editor/useDomEditOverlayGestures.ts +137 -10
  32. package/src/components/nle/NLELayout.tsx +18 -0
  33. package/src/contexts/DomEditContext.tsx +24 -0
  34. package/src/hooks/gsapRuntimeBridge.ts +585 -0
  35. package/src/hooks/gsapRuntimeKeyframes.ts +170 -0
  36. package/src/hooks/useAnimatedPropertyCommit.ts +131 -0
  37. package/src/hooks/useAppHotkeys.ts +63 -1
  38. package/src/hooks/useDomEditCommits.ts +39 -4
  39. package/src/hooks/useDomEditSession.ts +177 -63
  40. package/src/hooks/useGsapScriptCommits.ts +144 -7
  41. package/src/hooks/useGsapSelectionHandlers.ts +202 -0
  42. package/src/hooks/useGsapTweenCache.ts +174 -3
  43. package/src/hooks/useTimelineEditing.ts +93 -0
  44. package/src/icons/SystemIcons.tsx +2 -0
  45. package/src/player/components/ClipContextMenu.tsx +99 -0
  46. package/src/player/components/KeyframeDiamondContextMenu.tsx +164 -0
  47. package/src/player/components/Timeline.test.ts +2 -1
  48. package/src/player/components/Timeline.tsx +108 -68
  49. package/src/player/components/TimelineCanvas.tsx +47 -1
  50. package/src/player/components/TimelineClip.tsx +8 -3
  51. package/src/player/components/TimelineClipDiamonds.tsx +174 -0
  52. package/src/player/components/timelineDragDrop.ts +103 -0
  53. package/src/player/components/timelineLayout.ts +1 -1
  54. package/src/player/store/playerStore.ts +42 -0
  55. package/src/utils/editHistory.ts +1 -1
  56. package/src/utils/optimisticUpdate.test.ts +53 -0
  57. package/src/utils/optimisticUpdate.ts +18 -0
  58. package/src/utils/studioUiPreferences.ts +17 -0
  59. package/dist/assets/index-CrxThtSJ.css +0 -1
  60. package/dist/assets/index-Dc2HfqON.js +0 -140
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
2
  import type { TimelineElement } from "../player";
3
+ import { usePlayerStore } from "../player";
3
4
  import {
4
5
  STUDIO_INSPECTOR_PANELS_ENABLED,
5
6
  STUDIO_GSAP_PANEL_ENABLED,
@@ -16,7 +17,20 @@ import { useDomSelection } from "./useDomSelection";
16
17
  import { usePreviewInteraction } from "./usePreviewInteraction";
17
18
  import { useDomEditCommits } from "./useDomEditCommits";
18
19
  import { useGsapScriptCommits } from "./useGsapScriptCommits";
19
- import { useGsapAnimationsForElement, useGsapCacheVersion } from "./useGsapTweenCache";
20
+ import {
21
+ useGsapAnimationsForElement,
22
+ useGsapCacheVersion,
23
+ usePopulateKeyframeCacheForFile,
24
+ fetchParsedAnimations,
25
+ getAnimationsForElement,
26
+ } from "./useGsapTweenCache";
27
+ import {
28
+ tryGsapDragIntercept,
29
+ tryGsapResizeIntercept,
30
+ tryGsapRotationIntercept,
31
+ } from "./gsapRuntimeBridge";
32
+ import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
33
+ import { useGsapSelectionHandlers } from "./useGsapSelectionHandlers";
20
34
 
21
35
  // ── Types ──
22
36
 
@@ -194,17 +208,37 @@ export function useDomEditSession({
194
208
  onClickToSource,
195
209
  });
196
210
 
211
+ // Sync DOM selection → timeline selectedElementId so that clip selection
212
+ // highlights and diamond playhead fills work on cold-load URL restore.
213
+ useEffect(() => {
214
+ if (!domEditSelection?.id) return;
215
+ const { selectedElementId, elements, setSelectedElementId } = usePlayerStore.getState();
216
+ const matchKey = elements.find(
217
+ (el) => el.domId === domEditSelection.id || el.id === domEditSelection.id,
218
+ );
219
+ const key = matchKey ? (matchKey.key ?? matchKey.id) : null;
220
+ if (key && key !== selectedElementId) setSelectedElementId(key);
221
+ }, [domEditSelection?.id]);
222
+
197
223
  // ── GSAP script editing ──
198
224
 
199
225
  const { version: gsapCacheVersion, bump: bumpGsapCache } = useGsapCacheVersion();
200
226
 
227
+ const gsapSourceFile = domEditSelection?.sourceFile || activeCompPath || "index.html";
228
+
229
+ usePopulateKeyframeCacheForFile(
230
+ STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
231
+ gsapSourceFile,
232
+ gsapCacheVersion,
233
+ );
234
+
201
235
  const {
202
236
  animations: selectedGsapAnimations,
203
237
  multipleTimelines: gsapMultipleTimelines,
204
238
  unsupportedTimelinePattern: gsapUnsupportedTimelinePattern,
205
239
  } = useGsapAnimationsForElement(
206
240
  STUDIO_GSAP_PANEL_ENABLED ? (projectId ?? null) : null,
207
- domEditSelection?.sourceFile || activeCompPath || "index.html",
241
+ gsapSourceFile,
208
242
  domEditSelection
209
243
  ? { id: domEditSelection.id ?? null, selector: domEditSelection.selector ?? null }
210
244
  : null,
@@ -212,6 +246,7 @@ export function useDomEditSession({
212
246
  );
213
247
 
214
248
  const {
249
+ commitMutation: gsapCommitMutation,
215
250
  updateGsapProperty,
216
251
  updateGsapMeta,
217
252
  deleteGsapAnimation,
@@ -221,6 +256,10 @@ export function useDomEditSession({
221
256
  updateGsapFromProperty,
222
257
  addGsapFromProperty,
223
258
  removeGsapFromProperty,
259
+ addKeyframe,
260
+ removeKeyframe,
261
+ convertToKeyframes,
262
+ removeAllKeyframes,
224
263
  } = useGsapScriptCommits({
225
264
  projectIdRef,
226
265
  activeCompPath,
@@ -270,77 +309,144 @@ export function useDomEditSession({
270
309
  buildDomSelectionFromTarget,
271
310
  });
272
311
 
273
- const handleGsapUpdateProperty = useCallback(
274
- (animId: string, prop: string, value: number | string) => {
275
- if (!domEditSelection) return;
276
- updateGsapProperty(domEditSelection, animId, prop, value);
277
- },
278
- [domEditSelection, updateGsapProperty],
279
- );
280
-
281
- const handleGsapUpdateMeta = useCallback(
282
- (animId: string, updates: { duration?: number; ease?: string; position?: number }) => {
283
- if (!domEditSelection) return;
284
- updateGsapMeta(domEditSelection, animId, updates);
285
- },
286
- [domEditSelection, updateGsapMeta],
287
- );
288
-
289
- const handleGsapDeleteAnimation = useCallback(
290
- (animId: string) => {
291
- if (!domEditSelection) return;
292
- deleteGsapAnimation(domEditSelection, animId);
293
- },
294
- [domEditSelection, deleteGsapAnimation],
295
- );
296
-
297
- const handleGsapAddAnimation = useCallback(
298
- (method: "to" | "from" | "set" | "fromTo") => {
299
- if (!domEditSelection) return;
300
- addGsapAnimation(domEditSelection, method, currentTime);
312
+ // GSAP-aware: intercept offset/resize/rotation to commit via script mutation when animated.
313
+ const handleGsapAwarePathOffsetCommit = useCallback(
314
+ async (selection: DomEditSelection, next: { x: number; y: number }) => {
315
+ if (gsapCommitMutation) {
316
+ const handled = await tryGsapDragIntercept(
317
+ selection,
318
+ next,
319
+ selectedGsapAnimations,
320
+ previewIframeRef.current,
321
+ gsapCommitMutation,
322
+ async () => {
323
+ const pid = projectId;
324
+ if (!pid) return [];
325
+ const parsed = await fetchParsedAnimations(pid, gsapSourceFile);
326
+ if (!parsed) return [];
327
+ const target = { id: selection.id ?? null, selector: selection.selector ?? null };
328
+ return getAnimationsForElement(parsed.animations, target);
329
+ },
330
+ );
331
+ if (handled) return;
332
+ }
333
+ handleDomPathOffsetCommit(selection, next);
301
334
  },
302
- [domEditSelection, addGsapAnimation, currentTime],
335
+ [
336
+ handleDomPathOffsetCommit,
337
+ selectedGsapAnimations,
338
+ gsapCommitMutation,
339
+ previewIframeRef,
340
+ projectId,
341
+ gsapSourceFile,
342
+ ],
303
343
  );
304
344
 
305
- const handleGsapAddProperty = useCallback(
306
- (animId: string, prop: string) => {
307
- if (!domEditSelection) return;
308
- addGsapProperty(domEditSelection, animId, prop);
345
+ const makeFetchFallback = useCallback(
346
+ (selection: DomEditSelection) => async () => {
347
+ const pid = projectId;
348
+ if (!pid) return [];
349
+ const parsed = await fetchParsedAnimations(pid, gsapSourceFile);
350
+ if (!parsed) return [];
351
+ return getAnimationsForElement(parsed.animations, {
352
+ id: selection.id ?? null,
353
+ selector: selection.selector ?? null,
354
+ });
309
355
  },
310
- [domEditSelection, addGsapProperty],
356
+ [projectId, gsapSourceFile],
311
357
  );
312
358
 
313
- const handleGsapRemoveProperty = useCallback(
314
- (animId: string, prop: string) => {
315
- if (!domEditSelection) return;
316
- removeGsapProperty(domEditSelection, animId, prop);
359
+ const handleGsapAwareBoxSizeCommit = useCallback(
360
+ async (selection: DomEditSelection, next: { width: number; height: number }) => {
361
+ if (gsapCommitMutation) {
362
+ const handled = await tryGsapResizeIntercept(
363
+ selection,
364
+ next,
365
+ selectedGsapAnimations,
366
+ previewIframeRef.current,
367
+ gsapCommitMutation,
368
+ makeFetchFallback(selection),
369
+ );
370
+ if (handled) return;
371
+ }
372
+ handleDomBoxSizeCommit(selection, next);
317
373
  },
318
- [domEditSelection, removeGsapProperty],
374
+ [
375
+ handleDomBoxSizeCommit,
376
+ selectedGsapAnimations,
377
+ gsapCommitMutation,
378
+ previewIframeRef,
379
+ makeFetchFallback,
380
+ ],
319
381
  );
320
382
 
321
- const handleGsapUpdateFromProperty = useCallback(
322
- (animId: string, prop: string, value: number | string) => {
323
- if (!domEditSelection) return;
324
- updateGsapFromProperty(domEditSelection, animId, prop, value);
383
+ const handleGsapAwareRotationCommit = useCallback(
384
+ async (selection: DomEditSelection, next: { angle: number }) => {
385
+ if (gsapCommitMutation) {
386
+ const handled = await tryGsapRotationIntercept(
387
+ selection,
388
+ next.angle,
389
+ selectedGsapAnimations,
390
+ previewIframeRef.current,
391
+ gsapCommitMutation,
392
+ makeFetchFallback(selection),
393
+ );
394
+ if (handled) return;
395
+ }
396
+ handleDomRotationCommit(selection, next);
325
397
  },
326
- [domEditSelection, updateGsapFromProperty],
398
+ [
399
+ handleDomRotationCommit,
400
+ selectedGsapAnimations,
401
+ gsapCommitMutation,
402
+ previewIframeRef,
403
+ makeFetchFallback,
404
+ ],
327
405
  );
328
406
 
329
- const handleGsapAddFromProperty = useCallback(
330
- (animId: string, prop: string) => {
331
- if (!domEditSelection) return;
332
- addGsapFromProperty(domEditSelection, animId, prop);
333
- },
334
- [domEditSelection, addGsapFromProperty],
335
- );
407
+ const {
408
+ handleGsapUpdateProperty,
409
+ handleGsapUpdateMeta,
410
+ handleGsapDeleteAnimation,
411
+ handleGsapAddAnimation,
412
+ handleGsapAddProperty,
413
+ handleGsapRemoveProperty,
414
+ handleGsapUpdateFromProperty,
415
+ handleGsapAddFromProperty,
416
+ handleGsapRemoveFromProperty,
417
+ handleGsapAddKeyframe,
418
+ handleGsapRemoveKeyframe,
419
+ handleGsapConvertToKeyframes,
420
+ handleGsapRemoveAllKeyframes,
421
+ handleResetSelectedElementKeyframes,
422
+ } = useGsapSelectionHandlers({
423
+ domEditSelection,
424
+ updateGsapProperty,
425
+ updateGsapMeta,
426
+ deleteGsapAnimation,
427
+ addGsapAnimation,
428
+ addGsapProperty,
429
+ removeGsapProperty,
430
+ updateGsapFromProperty,
431
+ addGsapFromProperty,
432
+ removeGsapFromProperty,
433
+ addKeyframe,
434
+ removeKeyframe,
435
+ convertToKeyframes,
436
+ removeAllKeyframes,
437
+ currentTime,
438
+ handleDomManualEditsReset,
439
+ selectedGsapAnimations,
440
+ });
336
441
 
337
- const handleGsapRemoveFromProperty = useCallback(
338
- (animId: string, prop: string) => {
339
- if (!domEditSelection) return;
340
- removeGsapFromProperty(domEditSelection, animId, prop);
341
- },
342
- [domEditSelection, removeGsapFromProperty],
343
- );
442
+ const commitAnimatedProperty = useAnimatedPropertyCommit({
443
+ selectedGsapAnimations,
444
+ gsapCommitMutation,
445
+ addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
446
+ convertToKeyframes: (sel, animId) => convertToKeyframes(sel, animId),
447
+ previewIframeRef,
448
+ bumpGsapCache,
449
+ });
344
450
 
345
451
  // Sync selection from preview document on load / refresh
346
452
  // eslint-disable-next-line no-restricted-syntax
@@ -445,10 +551,10 @@ export function useDomEditSession({
445
551
  handleDomStyleCommit,
446
552
  handleDomAttributeCommit,
447
553
  handleDomHtmlAttributeCommit,
448
- handleDomPathOffsetCommit,
554
+ handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
449
555
  handleDomGroupPathOffsetCommit,
450
- handleDomBoxSizeCommit,
451
- handleDomRotationCommit,
556
+ handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
557
+ handleDomRotationCommit: handleGsapAwareRotationCommit,
452
558
  handleDomManualEditsReset,
453
559
  handleDomMotionCommit,
454
560
  handleDomMotionClear,
@@ -482,5 +588,13 @@ export function useDomEditSession({
482
588
  handleGsapUpdateFromProperty,
483
589
  handleGsapAddFromProperty,
484
590
  handleGsapRemoveFromProperty,
591
+ handleGsapAddKeyframe,
592
+ handleGsapRemoveKeyframe,
593
+ handleGsapConvertToKeyframes,
594
+ handleGsapRemoveAllKeyframes,
595
+ handleResetSelectedElementKeyframes,
596
+ commitAnimatedProperty,
597
+ invalidateGsapCache: bumpGsapCache,
598
+ previewIframeRef,
485
599
  };
486
600
  }
@@ -3,6 +3,8 @@ import type { ParsedGsap } from "@hyperframes/core/gsap-parser";
3
3
  import type { DomEditSelection } from "../components/editor/domEditingTypes";
4
4
  import type { EditHistoryKind } from "../utils/editHistory";
5
5
  import { applySoftReload } from "../utils/gsapSoftReload";
6
+ import { executeOptimistic } from "../utils/optimisticUpdate";
7
+ import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore";
6
8
 
7
9
  const PROPERTY_DEFAULTS: Record<string, number> = {
8
10
  opacity: 1,
@@ -70,6 +72,27 @@ async function mutateGsapScript(
70
72
  }
71
73
  }
72
74
 
75
+ function buildCacheKey(sourceFile: string, elementId: string): string {
76
+ return `${sourceFile}#${elementId}`;
77
+ }
78
+
79
+ function readKeyframeSnapshot(
80
+ sourceFile: string,
81
+ elementId: string | null | undefined,
82
+ ): KeyframeCacheEntry | undefined {
83
+ if (!elementId) return undefined;
84
+ return usePlayerStore.getState().keyframeCache.get(buildCacheKey(sourceFile, elementId));
85
+ }
86
+
87
+ function writeKeyframeCache(
88
+ sourceFile: string,
89
+ elementId: string | null | undefined,
90
+ data: KeyframeCacheEntry | undefined,
91
+ ): void {
92
+ if (!elementId) return;
93
+ usePlayerStore.getState().setKeyframeCache(buildCacheKey(sourceFile, elementId), data);
94
+ }
95
+
73
96
  interface GsapScriptCommitsParams {
74
97
  projectIdRef: React.MutableRefObject<string | null>;
75
98
  activeCompPath: string | null;
@@ -113,7 +136,13 @@ export function useGsapScriptCommits({
113
136
  async (
114
137
  selection: DomEditSelection,
115
138
  mutation: Record<string, unknown>,
116
- options: { label: string; coalesceKey?: string; softReload?: boolean },
139
+ options: {
140
+ label: string;
141
+ coalesceKey?: string;
142
+ softReload?: boolean;
143
+ skipReload?: boolean;
144
+ beforeReload?: () => void;
145
+ },
117
146
  ) => {
118
147
  const pid = projectIdRef.current;
119
148
  if (!pid) return;
@@ -135,6 +164,21 @@ export function useGsapScriptCommits({
135
164
 
136
165
  onCacheInvalidate();
137
166
 
167
+ if (result.parsed?.animations) {
168
+ const { setKeyframeCache } = usePlayerStore.getState();
169
+ for (const anim of result.parsed.animations) {
170
+ if (!anim.keyframes) continue;
171
+ const id = anim.targetSelector.match(/^#([\w-]+)/)?.[1];
172
+ if (!id) continue;
173
+ setKeyframeCache(`${targetPath}#${id}`, anim.keyframes);
174
+ if (targetPath !== "index.html") setKeyframeCache(`index.html#${id}`, anim.keyframes);
175
+ }
176
+ }
177
+
178
+ if (options.skipReload) return;
179
+
180
+ options.beforeReload?.();
181
+
138
182
  if (options.softReload && result.scriptText) {
139
183
  if (!applySoftReload(previewIframeRef.current, result.scriptText)) {
140
184
  reloadPreview();
@@ -225,7 +269,7 @@ export function useGsapScriptCommits({
225
269
  async (
226
270
  selection: DomEditSelection,
227
271
  method: "to" | "from" | "set" | "fromTo",
228
- currentTime?: number,
272
+ _currentTime?: number,
229
273
  ) => {
230
274
  const { selector, autoId } = ensureElementAddressable(selection);
231
275
 
@@ -253,12 +297,15 @@ export function useGsapScriptCommits({
253
297
  if (!data.changed) return;
254
298
  }
255
299
 
256
- const start = currentTime ?? (Number.parseFloat(selection.dataAttributes.start ?? "0") || 0);
300
+ const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
301
+ const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
302
+ const position = Math.round(elStart * 1000) / 1000;
303
+ const duration = Math.round(elDuration * 1000) / 1000;
257
304
  const toDefaults: Record<string, Record<string, number>> = {
258
305
  from: { opacity: 0 },
259
- to: { opacity: 1 },
306
+ to: { x: 0, y: 0, opacity: 1 },
260
307
  set: { opacity: 1 },
261
- fromTo: { opacity: 1 },
308
+ fromTo: { x: 0, y: 0, opacity: 1 },
262
309
  };
263
310
 
264
311
  await commitMutation(
@@ -267,8 +314,8 @@ export function useGsapScriptCommits({
267
314
  type: "add",
268
315
  targetSelector: selector,
269
316
  method,
270
- position: start,
271
- duration: method === "set" ? undefined : 0.5,
317
+ position,
318
+ duration: method === "set" ? undefined : duration,
272
319
  ease: method === "set" ? undefined : "power2.out",
273
320
  properties: toDefaults[method] ?? { opacity: 1 },
274
321
  fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
@@ -353,7 +400,93 @@ export function useGsapScriptCommits({
353
400
  [commitMutation],
354
401
  );
355
402
 
403
+ const addKeyframe = useCallback(
404
+ (
405
+ selection: DomEditSelection,
406
+ animationId: string,
407
+ percentage: number,
408
+ property: string,
409
+ value: number | string,
410
+ ) => {
411
+ const sf = selection.sourceFile || activeCompPath || "index.html";
412
+ const elementId = selection.id;
413
+ void executeOptimistic<KeyframeCacheEntry | undefined>({
414
+ apply: () => {
415
+ const prev = readKeyframeSnapshot(sf, elementId);
416
+ if (prev) {
417
+ const newKeyframes = [
418
+ ...prev.keyframes,
419
+ { percentage, properties: { [property]: value } },
420
+ ].sort((a, b) => a.percentage - b.percentage);
421
+ writeKeyframeCache(sf, elementId, { ...prev, keyframes: newKeyframes });
422
+ }
423
+ return prev;
424
+ },
425
+ persist: () =>
426
+ commitMutation(
427
+ selection,
428
+ { type: "add-keyframe", animationId, percentage, properties: { [property]: value } },
429
+ { label: `Add keyframe at ${percentage}%`, softReload: true },
430
+ ),
431
+ rollback: (prev) => {
432
+ writeKeyframeCache(sf, elementId, prev);
433
+ },
434
+ });
435
+ },
436
+ [commitMutation, activeCompPath],
437
+ );
438
+
439
+ const removeKeyframe = useCallback(
440
+ (selection: DomEditSelection, animationId: string, percentage: number) => {
441
+ const sf = selection.sourceFile || activeCompPath || "index.html";
442
+ const elementId = selection.id;
443
+ void executeOptimistic<KeyframeCacheEntry | undefined>({
444
+ apply: () => {
445
+ const prev = readKeyframeSnapshot(sf, elementId);
446
+ if (prev) {
447
+ const newKeyframes = prev.keyframes.filter((kf) => kf.percentage !== percentage);
448
+ writeKeyframeCache(sf, elementId, { ...prev, keyframes: newKeyframes });
449
+ }
450
+ return prev;
451
+ },
452
+ persist: () =>
453
+ commitMutation(
454
+ selection,
455
+ { type: "remove-keyframe", animationId, percentage },
456
+ { label: `Remove keyframe at ${percentage}%`, softReload: true },
457
+ ),
458
+ rollback: (prev) => {
459
+ writeKeyframeCache(sf, elementId, prev);
460
+ },
461
+ });
462
+ },
463
+ [commitMutation, activeCompPath],
464
+ );
465
+
466
+ const convertToKeyframes = useCallback(
467
+ (selection: DomEditSelection, animationId: string) => {
468
+ void commitMutation(
469
+ selection,
470
+ { type: "convert-to-keyframes", animationId },
471
+ { label: "Convert to keyframes" },
472
+ );
473
+ },
474
+ [commitMutation],
475
+ );
476
+
477
+ const removeAllKeyframes = useCallback(
478
+ (selection: DomEditSelection, animationId: string) => {
479
+ void commitMutation(
480
+ selection,
481
+ { type: "remove-all-keyframes", animationId },
482
+ { label: "Remove all keyframes", softReload: true },
483
+ );
484
+ },
485
+ [commitMutation],
486
+ );
487
+
356
488
  return {
489
+ commitMutation,
357
490
  updateGsapProperty,
358
491
  updateGsapMeta,
359
492
  deleteGsapAnimation,
@@ -363,5 +496,9 @@ export function useGsapScriptCommits({
363
496
  updateGsapFromProperty,
364
497
  addGsapFromProperty,
365
498
  removeGsapFromProperty,
499
+ addKeyframe,
500
+ removeKeyframe,
501
+ convertToKeyframes,
502
+ removeAllKeyframes,
366
503
  };
367
504
  }