@hyperframes/studio 0.6.96 → 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-BWFaypdT.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-0esDKGRk.js +0 -418
  108. package/dist/assets/index-B0twsRu0.css +0 -1
  109. package/dist/assets/index-BA979yF1.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
@@ -0,0 +1,242 @@
1
+ /**
2
+ * GSAP-aware move/resize/rotation wrappers that intercept geometry commits
3
+ * for animated elements and route them through script mutation instead of
4
+ * CSS patching. Also exposes the animated-property commit, arc-path ops,
5
+ * and the thin `commitMutation` facade.
6
+ *
7
+ * Extracted from useDomEditSession to isolate the GSAP intercept routing
8
+ * from the rest of the editing orchestration.
9
+ */
10
+ import { useCallback } from "react";
11
+ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
12
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
13
+ import { STUDIO_GSAP_DRAG_INTERCEPT_ENABLED } from "../components/editor/manualEditingAvailability";
14
+ import { GSAP_CSS_FALLBACK_BLOCKED_MESSAGE } from "./useDomGeometryCommits";
15
+ import {
16
+ tryGsapDragIntercept,
17
+ tryGsapResizeIntercept,
18
+ tryGsapRotationIntercept,
19
+ } from "./gsapRuntimeBridge";
20
+ import { useAnimatedPropertyCommit } from "./useAnimatedPropertyCommit";
21
+ import type { CommitMutation } from "./gsapScriptCommitTypes";
22
+
23
+ export interface UseGsapAwareEditingParams {
24
+ domEditSelection: DomEditSelection | null;
25
+ selectedGsapAnimations: GsapAnimation[];
26
+ gsapCommitMutation: CommitMutation | null;
27
+ previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
28
+ showToast: (message: string, tone?: "error" | "info") => void;
29
+ bumpGsapCache: () => void;
30
+ makeFetchFallback: (selection: DomEditSelection) => () => Promise<GsapAnimation[]>;
31
+ trackGsapInteractionFailure: (
32
+ error: unknown,
33
+ selection: DomEditSelection,
34
+ mutationType: string,
35
+ label: string,
36
+ ) => void;
37
+ // DOM fallbacks (from useDomEditCommits)
38
+ handleDomPathOffsetCommit: (
39
+ selection: DomEditSelection,
40
+ next: { x: number; y: number },
41
+ ) => Promise<void>;
42
+ handleDomBoxSizeCommit: (
43
+ selection: DomEditSelection,
44
+ next: { width: number; height: number },
45
+ ) => Promise<void>;
46
+ handleDomRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void>;
47
+ // GSAP script commit ops (from useGsapScriptCommits)
48
+ addGsapAnimation: (
49
+ sel: DomEditSelection,
50
+ method: "to" | "from" | "set" | "fromTo",
51
+ time?: number,
52
+ ) => Promise<void>;
53
+ convertToKeyframes: (sel: DomEditSelection, animId: string) => void;
54
+ setArcPath: (
55
+ sel: DomEditSelection,
56
+ animId: string,
57
+ config: {
58
+ enabled: boolean;
59
+ autoRotate?: boolean | number;
60
+ segments?: Array<{
61
+ curviness: number;
62
+ cp1?: { x: number; y: number };
63
+ cp2?: { x: number; y: number };
64
+ }>;
65
+ },
66
+ ) => void;
67
+ updateArcSegment: (
68
+ sel: DomEditSelection,
69
+ animId: string,
70
+ segmentIndex: number,
71
+ update: {
72
+ curviness?: number;
73
+ cp1?: { x: number; y: number };
74
+ cp2?: { x: number; y: number };
75
+ },
76
+ ) => void;
77
+ }
78
+
79
+ export function useGsapAwareEditing({
80
+ domEditSelection,
81
+ selectedGsapAnimations,
82
+ gsapCommitMutation,
83
+ previewIframeRef,
84
+ showToast,
85
+ bumpGsapCache,
86
+ makeFetchFallback,
87
+ trackGsapInteractionFailure,
88
+ handleDomPathOffsetCommit,
89
+ handleDomBoxSizeCommit,
90
+ handleDomRotationCommit,
91
+ addGsapAnimation,
92
+ convertToKeyframes,
93
+ setArcPath,
94
+ updateArcSegment,
95
+ }: UseGsapAwareEditingParams) {
96
+ // ── GSAP-aware geometry commits ──
97
+
98
+ const handleGsapAwarePathOffsetCommit = useCallback(
99
+ async (selection: DomEditSelection, next: { x: number; y: number }) => {
100
+ const hasGsapAnims = selectedGsapAnimations.length > 0;
101
+ if (hasGsapAnims && !STUDIO_GSAP_DRAG_INTERCEPT_ENABLED) {
102
+ showToast(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE, "error");
103
+ throw new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
104
+ }
105
+ if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
106
+ try {
107
+ const handled = await tryGsapDragIntercept(
108
+ selection,
109
+ next,
110
+ selectedGsapAnimations,
111
+ previewIframeRef.current,
112
+ gsapCommitMutation,
113
+ makeFetchFallback(selection),
114
+ );
115
+ if (handled) return;
116
+ } catch (error) {
117
+ trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
118
+ throw error;
119
+ }
120
+ }
121
+ return handleDomPathOffsetCommit(selection, next);
122
+ },
123
+ [
124
+ handleDomPathOffsetCommit,
125
+ selectedGsapAnimations,
126
+ gsapCommitMutation,
127
+ previewIframeRef,
128
+ makeFetchFallback,
129
+ trackGsapInteractionFailure,
130
+ showToast,
131
+ ],
132
+ );
133
+
134
+ const handleGsapAwareBoxSizeCommit = useCallback(
135
+ async (selection: DomEditSelection, next: { width: number; height: number }) => {
136
+ if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
137
+ try {
138
+ const handled = await tryGsapResizeIntercept(
139
+ selection,
140
+ next,
141
+ selectedGsapAnimations,
142
+ previewIframeRef.current,
143
+ gsapCommitMutation,
144
+ makeFetchFallback(selection),
145
+ );
146
+ if (handled) return;
147
+ } catch (error) {
148
+ trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
149
+ throw error;
150
+ }
151
+ }
152
+ return handleDomBoxSizeCommit(selection, next);
153
+ },
154
+ [
155
+ handleDomBoxSizeCommit,
156
+ selectedGsapAnimations,
157
+ gsapCommitMutation,
158
+ previewIframeRef,
159
+ makeFetchFallback,
160
+ trackGsapInteractionFailure,
161
+ ],
162
+ );
163
+
164
+ const handleGsapAwareRotationCommit = useCallback(
165
+ async (selection: DomEditSelection, next: { angle: number }) => {
166
+ if (STUDIO_GSAP_DRAG_INTERCEPT_ENABLED && gsapCommitMutation) {
167
+ try {
168
+ const handled = await tryGsapRotationIntercept(
169
+ selection,
170
+ next.angle,
171
+ selectedGsapAnimations,
172
+ previewIframeRef.current,
173
+ gsapCommitMutation,
174
+ makeFetchFallback(selection),
175
+ );
176
+ if (handled) return;
177
+ } catch (error) {
178
+ trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
179
+ throw error;
180
+ }
181
+ }
182
+ return handleDomRotationCommit(selection, next);
183
+ },
184
+ [
185
+ handleDomRotationCommit,
186
+ selectedGsapAnimations,
187
+ gsapCommitMutation,
188
+ previewIframeRef,
189
+ makeFetchFallback,
190
+ trackGsapInteractionFailure,
191
+ ],
192
+ );
193
+
194
+ // ── Animated property commit ──
195
+
196
+ const commitAnimatedProperty = useAnimatedPropertyCommit({
197
+ selectedGsapAnimations,
198
+ gsapCommitMutation,
199
+ addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
200
+ convertToKeyframes: (sel, animId) => convertToKeyframes(sel, animId),
201
+ previewIframeRef,
202
+ bumpGsapCache,
203
+ });
204
+
205
+ // ── Arc path wrappers ──
206
+
207
+ const handleSetArcPath = useCallback(
208
+ (animId: string, config: Parameters<typeof setArcPath>[2]) => {
209
+ if (!domEditSelection) return;
210
+ setArcPath(domEditSelection, animId, config);
211
+ },
212
+ [domEditSelection, setArcPath],
213
+ );
214
+
215
+ const handleUpdateArcSegment = useCallback(
216
+ (animId: string, segmentIndex: number, update: Parameters<typeof updateArcSegment>[3]) => {
217
+ if (!domEditSelection) return;
218
+ updateArcSegment(domEditSelection, animId, segmentIndex, update);
219
+ },
220
+ [domEditSelection, updateArcSegment],
221
+ );
222
+
223
+ // ── Thin commitMutation facade ──
224
+
225
+ const commitMutation = useCallback(
226
+ async (mutation: Record<string, unknown>, options: { label: string; softReload?: boolean }) => {
227
+ if (!domEditSelection) return;
228
+ await gsapCommitMutation?.(domEditSelection, mutation, options);
229
+ },
230
+ [domEditSelection, gsapCommitMutation],
231
+ );
232
+
233
+ return {
234
+ handleGsapAwarePathOffsetCommit,
235
+ handleGsapAwareBoxSizeCommit,
236
+ handleGsapAwareRotationCommit,
237
+ commitAnimatedProperty,
238
+ handleSetArcPath,
239
+ handleUpdateArcSegment,
240
+ commitMutation,
241
+ };
242
+ }
@@ -0,0 +1,167 @@
1
+ import { useCallback } from "react";
2
+ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
3
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
4
+ import { executeOptimistic } from "../utils/optimisticUpdate";
5
+ import type { KeyframeCacheEntry } from "../player/store/playerStore";
6
+ import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit";
7
+ import { readKeyframeSnapshot, writeKeyframeCache } from "./gsapKeyframeCacheHelpers";
8
+ import type {
9
+ CommitMutation,
10
+ SafeGsapCommitMutation,
11
+ TrackGsapSaveFailure,
12
+ } from "./gsapScriptCommitTypes";
13
+
14
+ function executeOptimisticKeyframeCacheUpdate(options: {
15
+ sourceFile: string;
16
+ elementId: string | null | undefined;
17
+ apply: (entry: KeyframeCacheEntry) => KeyframeCacheEntry;
18
+ persist: () => Promise<void>;
19
+ }): Promise<void> {
20
+ return executeOptimistic<KeyframeCacheEntry | undefined>({
21
+ apply: () => {
22
+ const prev = readKeyframeSnapshot(options.sourceFile, options.elementId);
23
+ if (prev) writeKeyframeCache(options.sourceFile, options.elementId, options.apply(prev));
24
+ return prev;
25
+ },
26
+ persist: options.persist,
27
+ rollback: (prev) => {
28
+ writeKeyframeCache(options.sourceFile, options.elementId, prev);
29
+ },
30
+ });
31
+ }
32
+
33
+ interface GsapKeyframeOpsParams {
34
+ activeCompPath: string | null;
35
+ commitMutation: CommitMutation;
36
+ commitMutationSafely: SafeGsapCommitMutation;
37
+ trackGsapSaveFailure: TrackGsapSaveFailure;
38
+ }
39
+
40
+ export function useGsapKeyframeOps({
41
+ activeCompPath,
42
+ commitMutation,
43
+ commitMutationSafely,
44
+ trackGsapSaveFailure,
45
+ }: GsapKeyframeOpsParams) {
46
+ const addKeyframe = useCallback(
47
+ (
48
+ selection: DomEditSelection,
49
+ animationId: string,
50
+ percentage: number,
51
+ property: string,
52
+ value: number | string,
53
+ ) => {
54
+ const sourceFile = selection.sourceFile || activeCompPath || "index.html";
55
+ const mutation = {
56
+ type: "add-keyframe",
57
+ animationId,
58
+ percentage,
59
+ properties: { [property]: value },
60
+ };
61
+ void executeOptimisticKeyframeCacheUpdate({
62
+ sourceFile,
63
+ elementId: selection.id,
64
+ apply: (prev) => ({
65
+ ...prev,
66
+ keyframes: [...prev.keyframes, { percentage, properties: { [property]: value } }].sort(
67
+ (a, b) => a.percentage - b.percentage,
68
+ ),
69
+ }),
70
+ persist: () =>
71
+ commitMutation(selection, mutation, {
72
+ label: `Add keyframe at ${percentage}%`,
73
+ softReload: true,
74
+ }),
75
+ }).catch((error) => {
76
+ trackGsapSaveFailure(error, selection, mutation, `Add keyframe at ${percentage}%`);
77
+ });
78
+ },
79
+ [activeCompPath, commitMutation, trackGsapSaveFailure],
80
+ );
81
+
82
+ const addKeyframeBatch = useCallback(
83
+ (
84
+ selection: DomEditSelection,
85
+ animationId: string,
86
+ percentage: number,
87
+ properties: Record<string, number | string>,
88
+ ) => {
89
+ return commitMutation(
90
+ selection,
91
+ { type: "add-keyframe", animationId, percentage, properties },
92
+ { label: `Add keyframe at ${percentage}%`, softReload: true },
93
+ );
94
+ },
95
+ [commitMutation],
96
+ );
97
+
98
+ const removeKeyframe = useCallback(
99
+ (selection: DomEditSelection, animationId: string, percentage: number) => {
100
+ const sourceFile = selection.sourceFile || activeCompPath || "index.html";
101
+ const mutation = { type: "remove-keyframe", animationId, percentage };
102
+ void executeOptimisticKeyframeCacheUpdate({
103
+ sourceFile,
104
+ elementId: selection.id,
105
+ apply: (prev) => ({
106
+ ...prev,
107
+ keyframes: prev.keyframes.filter(
108
+ (kf) => Math.abs((kf.tweenPercentage ?? kf.percentage) - percentage) > 0.2,
109
+ ),
110
+ }),
111
+ persist: () =>
112
+ commitMutation(selection, mutation, {
113
+ label: `Remove keyframe at ${percentage}%`,
114
+ softReload: true,
115
+ }),
116
+ }).catch((error) => {
117
+ trackGsapSaveFailure(error, selection, mutation, `Remove keyframe at ${percentage}%`);
118
+ });
119
+ },
120
+ [activeCompPath, commitMutation, trackGsapSaveFailure],
121
+ );
122
+
123
+ const convertToKeyframes = useCallback(
124
+ (
125
+ selection: DomEditSelection,
126
+ animationId: string,
127
+ resolvedFromValues?: Record<string, number | string>,
128
+ ) => {
129
+ return commitMutation(
130
+ selection,
131
+ { type: "convert-to-keyframes", animationId, resolvedFromValues },
132
+ { label: "Convert to keyframes" },
133
+ );
134
+ },
135
+ [commitMutation],
136
+ );
137
+
138
+ const removeAllKeyframes = useCallback(
139
+ (selection: DomEditSelection, animationId: string) => {
140
+ commitMutationSafely(
141
+ selection,
142
+ { type: "remove-all-keyframes", animationId },
143
+ { label: "Remove all keyframes", softReload: true },
144
+ );
145
+ },
146
+ [commitMutationSafely],
147
+ );
148
+
149
+ const commitKeyframeAtTime = useCallback(
150
+ (
151
+ selection: DomEditSelection,
152
+ absoluteTime: number,
153
+ animations: GsapAnimation[],
154
+ properties: Record<string, number | string>,
155
+ ) => commitKeyframeAtTimeImpl(selection, absoluteTime, animations, properties, commitMutation),
156
+ [commitMutation],
157
+ );
158
+
159
+ return {
160
+ addKeyframe,
161
+ addKeyframeBatch,
162
+ removeKeyframe,
163
+ convertToKeyframes,
164
+ removeAllKeyframes,
165
+ commitKeyframeAtTime,
166
+ };
167
+ }
@@ -0,0 +1,135 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
3
+ import { PROPERTY_DEFAULTS } from "./gsapScriptCommitHelpers";
4
+ import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";
5
+
6
+ const DEBOUNCE_MS = 150;
7
+
8
+ export function useGsapPropertyDebounce(commitMutationSafely: SafeGsapCommitMutation) {
9
+ const pendingPropertyEditRef = useRef<{
10
+ selection: DomEditSelection;
11
+ animationId: string;
12
+ property: string;
13
+ value: number | string;
14
+ } | null>(null);
15
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
16
+
17
+ const flushPendingPropertyEdit = useCallback(() => {
18
+ const pending = pendingPropertyEditRef.current;
19
+ if (!pending) return;
20
+ pendingPropertyEditRef.current = null;
21
+ const { selection, animationId, property, value } = pending;
22
+ commitMutationSafely(
23
+ selection,
24
+ { type: "update-property", animationId, property, value },
25
+ {
26
+ label: `Edit GSAP ${property}`,
27
+ coalesceKey: `gsap:${animationId}:${property}`,
28
+ softReload: true,
29
+ },
30
+ );
31
+ }, [commitMutationSafely]);
32
+
33
+ const updateGsapProperty = useCallback(
34
+ (
35
+ selection: DomEditSelection,
36
+ animationId: string,
37
+ property: string,
38
+ value: number | string,
39
+ ) => {
40
+ pendingPropertyEditRef.current = { selection, animationId, property, value };
41
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
42
+ debounceTimerRef.current = setTimeout(flushPendingPropertyEdit, DEBOUNCE_MS);
43
+ },
44
+ [flushPendingPropertyEdit],
45
+ );
46
+
47
+ useEffect(() => {
48
+ return () => {
49
+ if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
50
+ flushPendingPropertyEdit();
51
+ };
52
+ }, [flushPendingPropertyEdit]);
53
+
54
+ const addGsapProperty = useCallback(
55
+ (selection: DomEditSelection, animationId: string, property: string) => {
56
+ let defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
57
+ const el = selection.element;
58
+ if (property === "width" || property === "height") {
59
+ const rect = el.getBoundingClientRect();
60
+ defaultValue = Math.round(property === "width" ? rect.width : rect.height);
61
+ } else if (property === "opacity" || property === "autoAlpha") {
62
+ const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
63
+ defaultValue = cs ? Number.parseFloat(cs.opacity) || 1 : 1;
64
+ }
65
+ commitMutationSafely(
66
+ selection,
67
+ { type: "add-property", animationId, property, defaultValue },
68
+ { label: `Add GSAP ${property}` },
69
+ );
70
+ },
71
+ [commitMutationSafely],
72
+ );
73
+
74
+ const removeGsapProperty = useCallback(
75
+ (selection: DomEditSelection, animationId: string, property: string) => {
76
+ commitMutationSafely(
77
+ selection,
78
+ { type: "remove-property", animationId, property },
79
+ { label: `Remove GSAP ${property}` },
80
+ );
81
+ },
82
+ [commitMutationSafely],
83
+ );
84
+
85
+ const updateGsapFromProperty = useCallback(
86
+ (
87
+ selection: DomEditSelection,
88
+ animationId: string,
89
+ property: string,
90
+ value: number | string,
91
+ ) => {
92
+ commitMutationSafely(
93
+ selection,
94
+ { type: "update-from-property", animationId, property, value },
95
+ {
96
+ label: `Edit GSAP from-${property}`,
97
+ coalesceKey: `gsap:${animationId}:from:${property}`,
98
+ },
99
+ );
100
+ },
101
+ [commitMutationSafely],
102
+ );
103
+
104
+ const addGsapFromProperty = useCallback(
105
+ (selection: DomEditSelection, animationId: string, property: string) => {
106
+ const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
107
+ commitMutationSafely(
108
+ selection,
109
+ { type: "add-from-property", animationId, property, defaultValue },
110
+ { label: `Add GSAP from-${property}` },
111
+ );
112
+ },
113
+ [commitMutationSafely],
114
+ );
115
+
116
+ const removeGsapFromProperty = useCallback(
117
+ (selection: DomEditSelection, animationId: string, property: string) => {
118
+ commitMutationSafely(
119
+ selection,
120
+ { type: "remove-from-property", animationId, property },
121
+ { label: `Remove GSAP from-${property}` },
122
+ );
123
+ },
124
+ [commitMutationSafely],
125
+ );
126
+
127
+ return {
128
+ updateGsapProperty,
129
+ addGsapProperty,
130
+ removeGsapProperty,
131
+ updateGsapFromProperty,
132
+ addGsapFromProperty,
133
+ removeGsapFromProperty,
134
+ };
135
+ }