@hyperframes/studio 0.6.97 → 0.6.98

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 (120) hide show
  1. package/dist/assets/hyperframes-player-DgsMQSvV.js +418 -0
  2. package/dist/assets/index-B62bDCQv.css +1 -0
  3. package/dist/assets/index-Ce3pBm_I.js +252 -0
  4. package/dist/assets/{index-HveJ0MuV.js → index-D-ET9M0b.js} +1 -1
  5. package/dist/assets/index-D-bS9Dxx.js +1 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +7 -5
  8. package/src/App.tsx +182 -177
  9. package/src/captions/store.ts +11 -11
  10. package/src/components/StudioHeader.tsx +4 -4
  11. package/src/components/StudioLeftSidebar.tsx +2 -2
  12. package/src/components/StudioPreviewArea.tsx +225 -183
  13. package/src/components/StudioRightPanel.tsx +3 -3
  14. package/src/components/TimelineToolbar.tsx +25 -0
  15. package/src/components/editor/DomEditOverlay.tsx +2 -5
  16. package/src/components/editor/EaseCurveSection.tsx +2 -3
  17. package/src/components/editor/GestureTrailOverlay.tsx +4 -3
  18. package/src/components/editor/LayersPanel.tsx +3 -9
  19. package/src/components/editor/PropertyPanel.tsx +20 -61
  20. package/src/components/editor/colorValue.ts +3 -1
  21. package/src/components/editor/domEditOverlayGestures.ts +54 -1
  22. package/src/components/editor/domEditOverlayStartGesture.ts +5 -2
  23. package/src/components/editor/gradientValue.ts +3 -3
  24. package/src/components/editor/keyframeMove.test.ts +101 -0
  25. package/src/components/editor/keyframeMove.ts +151 -0
  26. package/src/components/editor/manualEditsDom.ts +0 -12
  27. package/src/components/editor/propertyPanelHelpers.ts +10 -38
  28. package/src/components/editor/propertyPanelMediaSection.tsx +1 -5
  29. package/src/components/editor/propertyPanelTimingSection.tsx +1 -6
  30. package/src/components/editor/propertyPanelTransformCommit.ts +129 -0
  31. package/src/components/editor/studioMotionOps.test.ts +1 -1
  32. package/src/components/editor/studioMotionOps.ts +2 -1
  33. package/src/components/editor/useDomEditOverlayGestures.ts +1 -46
  34. package/src/components/nle/NLELayout.tsx +1 -24
  35. package/src/components/sidebar/BlocksTab.tsx +2 -2
  36. package/src/contexts/DomEditContext.tsx +134 -31
  37. package/src/contexts/StudioContext.tsx +90 -40
  38. package/src/contexts/TimelineEditContext.tsx +47 -0
  39. package/src/hooks/domEditCommitTypes.ts +14 -0
  40. package/src/hooks/gsapDragCommit.ts +9 -24
  41. package/src/hooks/gsapKeyframeCacheHelpers.ts +2 -1
  42. package/src/hooks/gsapKeyframeCommit.ts +5 -15
  43. package/src/hooks/gsapRuntimeBridge.ts +18 -52
  44. package/src/hooks/gsapRuntimeKeyframes.ts +8 -57
  45. package/src/hooks/gsapRuntimeReaders.ts +19 -26
  46. package/src/hooks/gsapScriptCommitHelpers.ts +1 -11
  47. package/src/hooks/gsapScriptCommitTypes.ts +58 -0
  48. package/src/hooks/gsapShared.ts +157 -0
  49. package/src/hooks/timelineEditingHelpers.ts +63 -2
  50. package/src/hooks/useAnimatedPropertyCommit.ts +3 -25
  51. package/src/hooks/useAppHotkeys.ts +299 -377
  52. package/src/hooks/useConsoleErrorCapture.ts +33 -5
  53. package/src/hooks/useDomEditCommits.ts +35 -293
  54. package/src/hooks/useDomEditPositionPatchCommit.ts +1 -1
  55. package/src/hooks/useDomEditSession.ts +78 -249
  56. package/src/hooks/useDomEditTextCommits.ts +1 -1
  57. package/src/hooks/useDomEditWiring.ts +255 -0
  58. package/src/hooks/useDomGeometryCommits.ts +181 -0
  59. package/src/hooks/useDomSelection.ts +10 -27
  60. package/src/hooks/useEditorSave.ts +82 -0
  61. package/src/hooks/useElementLifecycleOps.ts +177 -0
  62. package/src/hooks/useEnableKeyframes.ts +10 -15
  63. package/src/hooks/useFileManager.ts +32 -114
  64. package/src/hooks/useFileTree.ts +80 -0
  65. package/src/hooks/useGestureCommit.ts +7 -5
  66. package/src/hooks/useGestureRecording.ts +1 -1
  67. package/src/hooks/useGsapAnimationOps.ts +122 -0
  68. package/src/hooks/useGsapArcPathOps.ts +61 -0
  69. package/src/hooks/useGsapAwareEditing.ts +242 -0
  70. package/src/hooks/useGsapKeyframeOps.ts +167 -0
  71. package/src/hooks/useGsapPropertyDebounce.ts +135 -0
  72. package/src/hooks/useGsapScriptCommits.ts +58 -570
  73. package/src/hooks/useGsapSelectionHandlers.ts +22 -9
  74. package/src/hooks/useGsapTweenCache.ts +35 -29
  75. package/src/hooks/useLintModal.ts +7 -0
  76. package/src/hooks/useMusicBeatAnalysis.ts +152 -0
  77. package/src/hooks/useRazorSplit.ts +1 -1
  78. package/src/hooks/useRenderClipContent.ts +46 -21
  79. package/src/hooks/useTimelineEditing.ts +48 -4
  80. package/src/player/components/AudioWaveform.tsx +29 -4
  81. package/src/player/components/BeatStrip.tsx +166 -0
  82. package/src/player/components/Timeline.tsx +39 -18
  83. package/src/player/components/TimelineCanvas.tsx +52 -12
  84. package/src/player/components/TimelineClipDiamonds.tsx +130 -20
  85. package/src/player/components/TimelinePropertyRows.tsx +8 -2
  86. package/src/player/components/TimelineRuler.tsx +36 -2
  87. package/src/player/components/timelineEditing.ts +30 -5
  88. package/src/player/components/useTimelineClipDrag.ts +155 -4
  89. package/src/player/components/useTimelinePlayhead.ts +30 -1
  90. package/src/player/hooks/useTimelinePlayer.ts +47 -45
  91. package/src/player/lib/mediaProbe.ts +46 -3
  92. package/src/player/lib/playbackScrub.ts +16 -0
  93. package/src/player/lib/timelineDOM.ts +10 -2
  94. package/src/player/lib/timelineIframeHelpers.ts +89 -0
  95. package/src/player/store/playerStore.ts +92 -33
  96. package/src/utils/beatEditActions.ts +109 -0
  97. package/src/utils/beatEditing.ts +136 -0
  98. package/src/utils/clipboardPayload.ts +3 -2
  99. package/src/utils/compositionPatterns.ts +2 -0
  100. package/src/utils/keyframeSelection.test.ts +45 -0
  101. package/src/utils/keyframeSelection.ts +29 -0
  102. package/src/utils/rounding.ts +9 -0
  103. package/src/utils/studioHelpers.ts +5 -2
  104. package/src/utils/studioUrlState.ts +2 -1
  105. package/src/utils/timelineAssetDrop.ts +6 -5
  106. package/src/utils/timelineInspector.ts +15 -100
  107. package/dist/assets/hyperframes-player-Daj5djxa.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-Cfye9xzo.js +0 -251
  110. package/src/components/editor/DopesheetStrip.tsx +0 -141
  111. package/src/components/editor/StaggerControls.tsx +0 -61
  112. package/src/components/editor/TimelineLayerPanel.test.ts +0 -42
  113. package/src/components/editor/TimelineLayerPanel.tsx +0 -15
  114. package/src/components/nle/TimelineEditorNotice.tsx +0 -133
  115. package/src/hooks/gsapRuntimePreview.ts +0 -19
  116. package/src/player/components/timelineUtils.ts +0 -211
  117. package/src/utils/audioBeatDetection.ts +0 -58
  118. package/src/utils/keyframeSnapping.test.ts +0 -74
  119. package/src/utils/keyframeSnapping.ts +0 -63
  120. package/src/utils/timelineInspector.test.ts +0 -79
@@ -1,16 +1,16 @@
1
- import { useState, useCallback, useRef, useEffect, useMemo } from "react";
1
+ import { useState, useCallback, useRef } from "react";
2
2
  import type { EditingFile } from "../utils/studioHelpers";
3
3
  import { FONT_EXT, isMediaFile } from "../utils/mediaTypes";
4
4
  import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
5
- import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
6
5
  import type { EditHistoryKind } from "../utils/editHistory";
7
6
  import { findTagByTarget, type PatchTarget } from "../utils/sourcePatcher";
8
- import { trackStudioEvent } from "../utils/studioTelemetry";
9
7
  import {
10
8
  createStudioSaveHttpError,
11
9
  retryStudioSave,
12
10
  StudioSaveNetworkError,
13
11
  } from "../utils/studioSaveDiagnostics";
12
+ import { useFileTree } from "./useFileTree";
13
+ import { useEditorSave } from "./useEditorSave";
14
14
 
15
15
  // ── Types ──
16
16
 
@@ -38,54 +38,31 @@ export function useFileManager({
38
38
  domEditSaveTimestampRef,
39
39
  setRefreshKey,
40
40
  }: UseFileManagerOptions) {
41
- // ── State ──
41
+ // ── Shared refs ──
42
42
 
43
43
  const [editingFile, setEditingFile] = useState<EditingFile | null>(null);
44
- const [projectDir, setProjectDir] = useState<string | null>(null);
45
- const [fileTree, setFileTree] = useState<string[]>([]);
46
- const [compositionPaths, setCompositionPaths] = useState<string[]>([]);
47
- const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
48
44
  const [revealSourceOffset, setRevealSourceOffset] = useState<number | null>(null);
49
45
 
50
- // ── Refs ──
51
-
52
46
  const editingPathRef = useRef(editingFile?.path);
53
47
  editingPathRef.current = editingFile?.path;
54
48
 
55
49
  const projectIdRef = useRef(projectId);
56
50
  projectIdRef.current = projectId;
57
51
 
58
- const saveRafRef = useRef<number | null>(null);
59
- const refreshRafRef = useRef<number | null>(null);
60
52
  const importedFontAssetsRef = useRef<ImportedFontAsset[]>([]);
61
53
 
62
- // ── Load file tree when projectId changes ──
54
+ // ── File tree ──
63
55
 
64
- // eslint-disable-next-line no-restricted-syntax
65
- useEffect(() => {
66
- if (!projectId) {
67
- setFileTreeLoaded(false);
68
- return;
69
- }
70
- let cancelled = false;
71
- setFileTreeLoaded(false);
72
- fetch(`/api/projects/${projectId}`)
73
- .then((r) => r.json())
74
- .then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
75
- if (!cancelled && data.files) setFileTree(data.files);
76
- if (!cancelled && data.compositions) setCompositionPaths(data.compositions);
77
- if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
78
- })
79
- .catch(() => {
80
- if (!cancelled) setProjectDir(null);
81
- })
82
- .finally(() => {
83
- if (!cancelled) setFileTreeLoaded(true);
84
- });
85
- return () => {
86
- cancelled = true;
87
- };
88
- }, [projectId]);
56
+ const {
57
+ projectDir,
58
+ fileTree,
59
+ setFileTree,
60
+ fileTreeLoaded,
61
+ refreshFileTree,
62
+ compositions,
63
+ assets,
64
+ fontAssets,
65
+ } = useFileTree({ projectId, projectIdRef });
89
66
 
90
67
  // ── Core file I/O ──
91
68
 
@@ -139,8 +116,23 @@ export function useFileManager({
139
116
  return typeof data.content === "string" ? data.content : "";
140
117
  }, []);
141
118
 
119
+ // ── Editor save (debounced content change) ──
120
+
121
+ const { saveRafRef, handleContentChange } = useEditorSave({
122
+ editingPathRef,
123
+ projectIdRef,
124
+ readProjectFile,
125
+ writeProjectFile,
126
+ recordEdit,
127
+ domEditSaveTimestampRef,
128
+ setRefreshKey,
129
+ });
130
+
142
131
  // ── File select ──
143
132
 
133
+ const revealRequestIdRef = useRef(0);
134
+ const revealAbortRef = useRef<AbortController | null>(null);
135
+
144
136
  const handleFileSelect = useCallback((path: string) => {
145
137
  const pid = projectIdRef.current;
146
138
  if (!pid) return;
@@ -162,47 +154,7 @@ export function useFileManager({
162
154
  .catch(() => {});
163
155
  }, []);
164
156
 
165
- // ── Content change (debounced save) ──
166
-
167
- const handleContentChange = useCallback(
168
- (content: string) => {
169
- const pid = projectIdRef.current;
170
- if (!pid) return;
171
- const path = editingPathRef.current;
172
- if (!path) return;
173
-
174
- if (saveRafRef.current != null) cancelAnimationFrame(saveRafRef.current);
175
- saveRafRef.current = requestAnimationFrame(() => {
176
- domEditSaveTimestampRef.current = Date.now();
177
- saveProjectFilesWithHistory({
178
- projectId: pid,
179
- label: "Edit source",
180
- kind: "source",
181
- coalesceKey: `source:${path}`,
182
- files: { [path]: content },
183
- readFile: readProjectFile,
184
- writeFile: writeProjectFile,
185
- recordEdit,
186
- })
187
- .then(() => {
188
- if (refreshRafRef.current != null) cancelAnimationFrame(refreshRafRef.current);
189
- refreshRafRef.current = requestAnimationFrame(() => setRefreshKey((k) => k + 1));
190
- })
191
- .catch((error) => {
192
- trackStudioEvent("save_failure", {
193
- source: "code_editor",
194
- error_message: error instanceof Error ? error.message : "unknown",
195
- });
196
- });
197
- });
198
- },
199
- [domEditSaveTimestampRef, readProjectFile, recordEdit, setRefreshKey, writeProjectFile],
200
- );
201
-
202
- // ── Open source for selection (click-to-source) ──
203
-
204
- const revealRequestIdRef = useRef(0);
205
- const revealAbortRef = useRef<AbortController | null>(null);
157
+ // ── Click-to-source ──
206
158
 
207
159
  const openSourceForSelection = useCallback(
208
160
  (sourceFile: string, target: PatchTarget) => {
@@ -235,16 +187,6 @@ export function useFileManager({
235
187
  [editingFile?.content],
236
188
  );
237
189
 
238
- // ── File tree refresh ──
239
-
240
- const refreshFileTree = useCallback(async () => {
241
- const pid = projectIdRef.current;
242
- if (!pid) return;
243
- const res = await fetch(`/api/projects/${pid}`);
244
- const data = await res.json();
245
- if (data.files) setFileTree(data.files);
246
- }, []);
247
-
248
190
  // ── Upload ──
249
191
 
250
192
  const uploadProjectFiles = useCallback(
@@ -289,7 +231,7 @@ export function useFileManager({
289
231
  [refreshFileTree, setRefreshKey, showToast],
290
232
  );
291
233
 
292
- // ── File management handlers ──
234
+ // ── File CRUD ──
293
235
 
294
236
  const handleCreateFile = useCallback(
295
237
  async (path: string) => {
@@ -320,7 +262,6 @@ export function useFileManager({
320
262
  async (path: string) => {
321
263
  const pid = projectIdRef.current;
322
264
  if (!pid) return;
323
- // Create a .gitkeep inside the folder so it appears in the tree
324
265
  const res = await fetch(
325
266
  `/api/projects/${pid}/files/${encodeURIComponent(path + "/.gitkeep")}`,
326
267
  {
@@ -371,7 +312,6 @@ export function useFileManager({
371
312
  handleFileSelect(newPath);
372
313
  }
373
314
  await refreshFileTree();
374
- // Refresh preview — references in compositions may have been updated
375
315
  setRefreshKey((k) => k + 1);
376
316
  } else {
377
317
  const err = await res.json().catch(() => ({ error: "unknown" }));
@@ -437,28 +377,6 @@ export function useFileManager({
437
377
  [uploadProjectFiles],
438
378
  );
439
379
 
440
- // ── Derived state ──
441
-
442
- const compositions = compositionPaths;
443
-
444
- const assets = useMemo(
445
- () =>
446
- fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
447
- [fileTree],
448
- );
449
-
450
- const fontAssets = useMemo<ImportedFontAsset[]>(
451
- () =>
452
- assets
453
- .filter((asset) => FONT_EXT.test(asset))
454
- .map((asset) => ({
455
- family: fontFamilyFromAssetPath(asset),
456
- path: asset,
457
- url: `/api/projects/${projectId}/preview/${asset}`,
458
- })),
459
- [assets, projectId],
460
- );
461
-
462
380
  // ── Return ──
463
381
 
464
382
  return {
@@ -0,0 +1,80 @@
1
+ import { useState, useCallback, useEffect, useMemo } from "react";
2
+ import { FONT_EXT } from "../utils/mediaTypes";
3
+ import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
4
+
5
+ interface UseFileTreeOptions {
6
+ projectId: string | null;
7
+ projectIdRef: React.RefObject<string | null>;
8
+ }
9
+
10
+ export function useFileTree({ projectId, projectIdRef }: UseFileTreeOptions) {
11
+ const [projectDir, setProjectDir] = useState<string | null>(null);
12
+ const [fileTree, setFileTree] = useState<string[]>([]);
13
+ const [compositionPaths, setCompositionPaths] = useState<string[]>([]);
14
+ const [fileTreeLoaded, setFileTreeLoaded] = useState(false);
15
+
16
+ // eslint-disable-next-line no-restricted-syntax
17
+ useEffect(() => {
18
+ if (!projectId) {
19
+ setFileTreeLoaded(false);
20
+ return;
21
+ }
22
+ let cancelled = false;
23
+ setFileTreeLoaded(false);
24
+ fetch(`/api/projects/${projectId}`)
25
+ .then((r) => r.json())
26
+ .then((data: { files?: string[]; dir?: string; compositions?: string[] }) => {
27
+ if (!cancelled && data.files) setFileTree(data.files);
28
+ if (!cancelled && data.compositions) setCompositionPaths(data.compositions);
29
+ if (!cancelled) setProjectDir(typeof data.dir === "string" ? data.dir : null);
30
+ })
31
+ .catch(() => {
32
+ if (!cancelled) setProjectDir(null);
33
+ })
34
+ .finally(() => {
35
+ if (!cancelled) setFileTreeLoaded(true);
36
+ });
37
+ return () => {
38
+ cancelled = true;
39
+ };
40
+ }, [projectId]);
41
+
42
+ const refreshFileTree = useCallback(async () => {
43
+ const pid = projectIdRef.current;
44
+ if (!pid) return;
45
+ const res = await fetch(`/api/projects/${pid}`);
46
+ const data = await res.json();
47
+ if (data.files) setFileTree(data.files);
48
+ }, [projectIdRef]);
49
+
50
+ const compositions = compositionPaths;
51
+
52
+ const assets = useMemo(
53
+ () =>
54
+ fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
55
+ [fileTree],
56
+ );
57
+
58
+ const fontAssets = useMemo<ImportedFontAsset[]>(
59
+ () =>
60
+ assets
61
+ .filter((asset) => FONT_EXT.test(asset))
62
+ .map((asset) => ({
63
+ family: fontFamilyFromAssetPath(asset),
64
+ path: asset,
65
+ url: `/api/projects/${projectId}/preview/${asset}`,
66
+ })),
67
+ [assets, projectId],
68
+ );
69
+
70
+ return {
71
+ projectDir,
72
+ fileTree,
73
+ setFileTree,
74
+ fileTreeLoaded,
75
+ refreshFileTree,
76
+ compositions,
77
+ assets,
78
+ fontAssets,
79
+ };
80
+ }
@@ -8,6 +8,7 @@ import { simplifyGestureSamples } from "../utils/rdpSimplify";
8
8
  import { usePlayerStore } from "../player";
9
9
  import type { DomEditSelection } from "../components/editor/domEditing";
10
10
  import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
11
+ import { roundTo3 } from "../utils/rounding";
11
12
  import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
12
13
 
13
14
  // Minimal subset of the session used by gesture commit
@@ -74,7 +75,8 @@ export function useGestureCommit({
74
75
  }
75
76
  return;
76
77
  }
77
- const duration = frozenSamples.length > 0 ? frozenSamples[frozenSamples.length - 1]!.time : 0;
78
+ const duration =
79
+ frozenSamples.length > 0 ? (frozenSamples[frozenSamples.length - 1]?.time ?? 0) : 0;
78
80
 
79
81
  if (frozenSamples.length <= 2) {
80
82
  showToast("No gesture detected — move the pointer while recording", "error");
@@ -171,8 +173,8 @@ export function useGestureCommit({
171
173
  {
172
174
  type: "add-with-keyframes",
173
175
  targetSelector: selector,
174
- position: Math.round(recStart * 1000) / 1000,
175
- duration: Math.round(duration * 1000) / 1000,
176
+ position: roundTo3(recStart),
177
+ duration: roundTo3(duration),
176
178
  keyframes,
177
179
  },
178
180
  { label: "Gesture recording (new range)", softReload: true },
@@ -183,8 +185,8 @@ export function useGestureCommit({
183
185
  {
184
186
  type: "add-with-keyframes",
185
187
  targetSelector: selector,
186
- position: Math.round(recStart * 1000) / 1000,
187
- duration: Math.round(duration * 1000) / 1000,
188
+ position: roundTo3(recStart),
189
+ duration: roundTo3(duration),
188
190
  keyframes,
189
191
  },
190
192
  { label: "Gesture recording", softReload: true },
@@ -430,7 +430,7 @@ export function useGestureRecording() {
430
430
  r.cleanup?.();
431
431
  r.cleanup = null;
432
432
  const frozen = r.samples.slice();
433
- setRecordingDuration(frozen.length > 0 ? frozen[frozen.length - 1]!.time : 0);
433
+ setRecordingDuration(frozen.length > 0 ? (frozen[frozen.length - 1]?.time ?? 0) : 0);
434
434
  setIsRecording(false);
435
435
  return frozen;
436
436
  }, []); // No deps — uses refs only
@@ -0,0 +1,122 @@
1
+ import { useCallback } from "react";
2
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
3
+ import { roundTo3 } from "../utils/rounding";
4
+ import {
5
+ assignGsapTargetAutoIdIfNeeded,
6
+ ensureElementAddressable,
7
+ } from "./gsapScriptCommitHelpers";
8
+ import type { CommitMutation, SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
9
+
10
+ interface GsapAnimationOpsParams {
11
+ projectIdRef: React.MutableRefObject<string | null>;
12
+ activeCompPath: string | null;
13
+ commitMutation: CommitMutation;
14
+ commitMutationSafely: SafeGsapCommitMutation;
15
+ showToast: (message: string, tone?: "error" | "info") => void;
16
+ }
17
+
18
+ export function useGsapAnimationOps({
19
+ projectIdRef,
20
+ activeCompPath,
21
+ commitMutation,
22
+ commitMutationSafely,
23
+ showToast,
24
+ }: GsapAnimationOpsParams) {
25
+ const updateGsapMeta = useCallback(
26
+ (
27
+ selection: DomEditSelection,
28
+ animationId: string,
29
+ updates: { duration?: number; ease?: string; position?: number },
30
+ ) => {
31
+ commitMutationSafely(
32
+ selection,
33
+ { type: "update-meta", animationId, updates },
34
+ {
35
+ label: "Edit GSAP animation",
36
+ coalesceKey: `gsap:${animationId}:meta`,
37
+ },
38
+ );
39
+ },
40
+ [commitMutationSafely],
41
+ );
42
+
43
+ const deleteGsapAnimation = useCallback(
44
+ (selection: DomEditSelection, animationId: string) => {
45
+ commitMutationSafely(
46
+ selection,
47
+ { type: "delete", animationId, stripStudioEdits: true },
48
+ { label: "Delete GSAP animation" },
49
+ );
50
+ },
51
+ [commitMutationSafely],
52
+ );
53
+
54
+ const deleteAllForSelector = useCallback(
55
+ (selection: DomEditSelection, targetSelector: string) => {
56
+ void commitMutation(
57
+ selection,
58
+ { type: "delete-all-for-selector", targetSelector },
59
+ { label: "Delete all animations for element" },
60
+ );
61
+ },
62
+ [commitMutation],
63
+ );
64
+
65
+ const addGsapAnimation = useCallback(
66
+ async (
67
+ selection: DomEditSelection,
68
+ method: "to" | "from" | "set" | "fromTo",
69
+ _currentTime?: number,
70
+ ) => {
71
+ const { selector, autoId } = ensureElementAddressable(selection);
72
+
73
+ if (autoId) {
74
+ const pid = projectIdRef.current;
75
+ const targetPath = selection.sourceFile || activeCompPath || "index.html";
76
+ if (!pid) return;
77
+ const assigned = await assignGsapTargetAutoIdIfNeeded({
78
+ projectId: pid,
79
+ targetPath,
80
+ selection,
81
+ autoId,
82
+ showToast,
83
+ });
84
+ if (!assigned) return;
85
+ }
86
+
87
+ const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
88
+ const elDuration = Number.parseFloat(selection.dataAttributes?.duration ?? "1") || 1;
89
+ const position = roundTo3(elStart);
90
+ const duration = roundTo3(elDuration);
91
+ const toDefaults: Record<string, Record<string, number>> = {
92
+ from: { opacity: 0 },
93
+ to: { x: 0, y: 0, opacity: 1 },
94
+ set: { opacity: 1 },
95
+ fromTo: { x: 0, y: 0, opacity: 1 },
96
+ };
97
+
98
+ await commitMutation(
99
+ selection,
100
+ {
101
+ type: "add",
102
+ targetSelector: selector,
103
+ method,
104
+ position,
105
+ duration: method === "set" ? undefined : duration,
106
+ ease: method === "set" ? undefined : "power2.out",
107
+ properties: toDefaults[method] ?? { opacity: 1 },
108
+ fromProperties: method === "fromTo" ? { opacity: 0 } : undefined,
109
+ },
110
+ { label: `Add GSAP ${method} animation` },
111
+ );
112
+ },
113
+ [activeCompPath, commitMutation, projectIdRef, showToast],
114
+ );
115
+
116
+ return {
117
+ updateGsapMeta,
118
+ deleteGsapAnimation,
119
+ deleteAllForSelector,
120
+ addGsapAnimation,
121
+ };
122
+ }
@@ -0,0 +1,61 @@
1
+ import { useCallback } from "react";
2
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
3
+ import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
4
+
5
+ export function useGsapArcPathOps(commitMutationSafely: SafeGsapCommitMutation) {
6
+ const setArcPath = useCallback(
7
+ (
8
+ selection: DomEditSelection,
9
+ animationId: string,
10
+ config: {
11
+ enabled: boolean;
12
+ autoRotate?: boolean | number;
13
+ segments?: Array<{
14
+ curviness: number;
15
+ cp1?: { x: number; y: number };
16
+ cp2?: { x: number; y: number };
17
+ }>;
18
+ },
19
+ ) => {
20
+ commitMutationSafely(
21
+ selection,
22
+ { type: "set-arc-path" as const, animationId, ...config },
23
+ { label: config.enabled ? "Enable arc path" : "Disable arc path", softReload: true },
24
+ );
25
+ },
26
+ [commitMutationSafely],
27
+ );
28
+
29
+ const updateArcSegment = useCallback(
30
+ (
31
+ selection: DomEditSelection,
32
+ animationId: string,
33
+ segmentIndex: number,
34
+ update: {
35
+ curviness?: number;
36
+ cp1?: { x: number; y: number };
37
+ cp2?: { x: number; y: number };
38
+ },
39
+ ) => {
40
+ commitMutationSafely(
41
+ selection,
42
+ { type: "update-arc-segment" as const, animationId, segmentIndex, ...update },
43
+ { label: "Update arc segment", softReload: true },
44
+ );
45
+ },
46
+ [commitMutationSafely],
47
+ );
48
+
49
+ const removeArcPath = useCallback(
50
+ (selection: DomEditSelection, animationId: string) => {
51
+ commitMutationSafely(
52
+ selection,
53
+ { type: "remove-arc-path" as const, animationId },
54
+ { label: "Remove arc path", softReload: true },
55
+ );
56
+ },
57
+ [commitMutationSafely],
58
+ );
59
+
60
+ return { setArcPath, updateArcSegment, removeArcPath };
61
+ }