@hyperframes/studio 0.8.114 → 0.8.116

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 (266) hide show
  1. package/dist/assets/{hyperframes-player-aXNJKiIg.js → hyperframes-player-mGYvPVpw.js} +1 -1
  2. package/dist/assets/{index-Bn2RJNTJ.js → index-BWHt0-ir.js} +1 -1
  3. package/dist/assets/{index-Dz7doz9l.js → index-DYNaXPVf.js} +203 -203
  4. package/dist/assets/{index-C9N-DGjC.js → index-Dr3hNpvS.js} +1 -1
  5. package/dist/assets/{index-BYnsJnJV.css → index-t1LH6H5H.css} +1 -1
  6. package/dist/{chunk-BIUXP3PG.js → chunk-3F47EFCT.js} +142 -277
  7. package/dist/chunk-3F47EFCT.js.map +1 -0
  8. package/dist/{chunk-K47E2ZJA.js → chunk-5EKWJZLM.js} +504 -17
  9. package/dist/chunk-5EKWJZLM.js.map +1 -0
  10. package/dist/{chunk-VWT7STY3.js → chunk-J6Q572FA.js} +2 -2
  11. package/dist/{domEditingLayers-V67MS4WP.js → domEditingLayers-OL7GLSS4.js} +3 -5
  12. package/dist/index.d.ts +146 -40
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +4021 -3591
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles.css +1 -1
  18. package/package.json +9 -8
  19. package/src/App.tsx +2 -2
  20. package/src/captions/hooks/useCaptionSync.ts +10 -6
  21. package/src/components/LinkedSelectionToggle.tsx +3 -2
  22. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  23. package/src/components/dock/Dock.tsx +5 -7
  24. package/src/components/editor/CanvasContextMenu.tsx +3 -2
  25. package/src/components/editor/DomEditCropHandles.test.tsx +27 -0
  26. package/src/components/editor/DomEditCropHandles.tsx +7 -4
  27. package/src/components/editor/DomEditOverlay.tsx +5 -1
  28. package/src/components/editor/FileTreeNodes.tsx +13 -10
  29. package/src/components/editor/MotionPathOverlay.tsx +7 -2
  30. package/src/components/editor/PropertyPanelEmptyState.tsx +12 -2
  31. package/src/components/editor/SnapToolbar.test.tsx +100 -1
  32. package/src/components/editor/SnapToolbar.tsx +43 -5
  33. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +48 -1
  34. package/src/components/editor/audioFxTelemetry.ts +2 -3
  35. package/src/components/editor/commitPreflightHandles.test.tsx +2 -2
  36. package/src/components/editor/cropResize.ts +4 -3
  37. package/src/components/editor/domEditingDom.ts +4 -0
  38. package/src/components/editor/domEditingLayers.ts +1 -5
  39. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  40. package/src/components/editor/domGroupMoveBy.ts +95 -0
  41. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  42. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  43. package/src/components/editor/manualEditingAvailability.ts +8 -0
  44. package/src/components/editor/manualEdits.ts +1 -0
  45. package/src/components/editor/manualEditsParsing.ts +7 -0
  46. package/src/components/editor/manualOffsetDrag.ts +5 -6
  47. package/src/components/editor/marqueeCommit.test.tsx +106 -37
  48. package/src/components/editor/marqueeCommit.ts +24 -8
  49. package/src/components/editor/motionPathCommit.test.ts +29 -1
  50. package/src/components/editor/motionPathCommit.ts +21 -9
  51. package/src/components/editor/motionPathSelection.test.ts +6 -31
  52. package/src/components/editor/probeSourceElement.test.ts +45 -0
  53. package/src/components/editor/probeSourceElement.ts +41 -16
  54. package/src/components/editor/propertyPanelFlatTextSection.tsx +10 -7
  55. package/src/components/editor/propertyPanelFont.tsx +3 -2
  56. package/src/components/editor/propertyPanelFxAddMenu.tsx +11 -2
  57. package/src/components/editor/propertyPanelFxCarveModule.tsx +15 -5
  58. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +3 -0
  59. package/src/components/editor/propertyPanelFxNodeParams.tsx +15 -2
  60. package/src/components/editor/propertyPanelFxNodeRow.tsx +3 -0
  61. package/src/components/editor/propertyPanelFxPresetRun.tsx +10 -2
  62. package/src/components/editor/propertyPanelFxSection.test.tsx +56 -0
  63. package/src/components/editor/propertyPanelFxSection.tsx +30 -28
  64. package/src/components/editor/propertyPanelTypes.ts +6 -3
  65. package/src/components/editor/useColorGradingController.ts +2 -1
  66. package/src/components/editor/useDomEditNudge.test.tsx +37 -0
  67. package/src/components/editor/useDomEditNudge.ts +2 -0
  68. package/src/components/editor/useDomEditOverlayGestures.ts +5 -0
  69. package/src/components/editor/useDomEditZOrder.ts +12 -2
  70. package/src/components/editor/useFxCarveNodes.ts +2 -1
  71. package/src/components/editor/useFxLevelling.ts +2 -1
  72. package/src/components/feedback/projectProvenance.ts +2 -1
  73. package/src/components/nle/NLEContext.test.ts +1 -0
  74. package/src/components/nle/NLEContext.tsx +2 -1
  75. package/src/components/nle/NLEPreview.tsx +12 -10
  76. package/src/components/nle/PreviewPoster.tsx +2 -1
  77. package/src/components/nle/useTimelineEditCallbacks.test.tsx +1 -0
  78. package/src/components/nle/useTimelineEditCallbacks.ts +7 -1
  79. package/src/components/renders/useFfmpegStatus.ts +2 -1
  80. package/src/components/renders/useRenderQueue.ts +5 -4
  81. package/src/components/sidebar/AssetContextMenu.tsx +4 -4
  82. package/src/components/sidebar/AssetsTab.tsx +2 -1
  83. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  84. package/src/components/ui/menuStyle.test.ts +61 -0
  85. package/src/components/ui/menuStyle.ts +10 -0
  86. package/src/contexts/DomEditContext.tsx +5 -1
  87. package/src/contexts/TimelineEditContext.tsx +1 -0
  88. package/src/domEditExports.test.tsx +9 -1
  89. package/src/hooks/domEditCommitRunner.ts +1 -1
  90. package/src/hooks/domSelectionTestHarness.ts +23 -0
  91. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  92. package/src/hooks/domStyleCommit.ts +8 -1
  93. package/src/hooks/draggedGsapPosition.ts +10 -0
  94. package/src/hooks/elementOffsetStager.ts +5 -4
  95. package/src/hooks/gsapDragCommit.test.ts +75 -164
  96. package/src/hooks/gsapDragCommit.ts +4 -125
  97. package/src/hooks/gsapDragPositionCommit.ts +36 -384
  98. package/src/hooks/gsapEditOutcome.ts +22 -2
  99. package/src/hooks/gsapGestureOutcome.test.ts +161 -0
  100. package/src/hooks/gsapGestureOutcome.ts +64 -0
  101. package/src/hooks/gsapGroupPreflight.ts +34 -0
  102. package/src/hooks/gsapKeyframeCommit.ts +3 -12
  103. package/src/hooks/gsapMutationClient.ts +4 -3
  104. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  105. package/src/hooks/gsapParsedTween.test.ts +62 -0
  106. package/src/hooks/gsapParsedTween.ts +243 -0
  107. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  108. package/src/hooks/gsapPositionDetection.ts +52 -33
  109. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  110. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  111. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  112. package/src/hooks/gsapResizeIntercept.test.ts +96 -11
  113. package/src/hooks/gsapResizeIntercept.ts +79 -168
  114. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  115. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  116. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  117. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  118. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  119. package/src/hooks/gsapScriptCommitTypes.ts +13 -0
  120. package/src/hooks/gsapShared.ts +7 -0
  121. package/src/hooks/gsapShared.writeTarget.test.ts +45 -57
  122. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  123. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  124. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  125. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  126. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  127. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  128. package/src/hooks/keyframeCacheAstLoad.ts +8 -6
  129. package/src/hooks/newTweenTarget.test.ts +41 -76
  130. package/src/hooks/plainRotation.ts +3 -2
  131. package/src/hooks/previewPromotionSelection.test.tsx +1 -1
  132. package/src/hooks/refreshTimelineRowText.test.ts +65 -0
  133. package/src/hooks/refreshTimelineRowText.ts +29 -0
  134. package/src/hooks/timelineTimingSync.test.ts +1 -0
  135. package/src/hooks/timelineTimingSync.ts +2 -1
  136. package/src/hooks/useAnimatedPropertyCommit.test.tsx +46 -5
  137. package/src/hooks/useAnimatedPropertyCommit.ts +2 -2
  138. package/src/hooks/useAskAgentModal.ts +2 -1
  139. package/src/hooks/useBlockCatalog.ts +2 -1
  140. package/src/hooks/useBlockedTimelineEditToast.ts +12 -3
  141. package/src/hooks/useCaptionDetection.ts +2 -1
  142. package/src/hooks/useCompositionContentLoader.ts +2 -1
  143. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  144. package/src/hooks/useDomEditPositionPatchCommit.ts +18 -23
  145. package/src/hooks/useDomEditSession.test.tsx +30 -1
  146. package/src/hooks/useDomEditSession.ts +10 -0
  147. package/src/hooks/useDomEditTextCommits.test.tsx +41 -0
  148. package/src/hooks/useDomEditTextCommits.ts +13 -3
  149. package/src/hooks/useDomEditWiring.ts +4 -2
  150. package/src/hooks/useDomGeometryCommit.test.tsx +25 -6
  151. package/src/hooks/useDomGeometryCommit.ts +8 -4
  152. package/src/hooks/useDomGeometryCommits.test.tsx +1 -0
  153. package/src/hooks/useDomGeometryCommits.ts +3 -2
  154. package/src/hooks/useDomSelection.test.ts +26 -0
  155. package/src/hooks/useDomSelection.ts +10 -3
  156. package/src/hooks/useDomSelectionTypes.ts +6 -1
  157. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  158. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  159. package/src/hooks/useEditHistoryActions.ts +16 -5
  160. package/src/hooks/useElementLifecycleOps.ts +2 -1
  161. package/src/hooks/useEnableKeyframes.test.ts +34 -3
  162. package/src/hooks/useEnableKeyframes.ts +7 -4
  163. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +121 -0
  164. package/src/hooks/useExternalFileChangeCoordinator.ts +35 -8
  165. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  166. package/src/hooks/useFileManager.ts +14 -10
  167. package/src/hooks/useFileTree.ts +3 -2
  168. package/src/hooks/useFrameCapture.ts +5 -1
  169. package/src/hooks/useFreezeFrame.ts +3 -2
  170. package/src/hooks/useGestureCommit.test.tsx +225 -55
  171. package/src/hooks/useGestureCommit.ts +86 -55
  172. package/src/hooks/useGroupCommits.ts +2 -1
  173. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  174. package/src/hooks/useGsapAwareEditing.test.tsx +28 -35
  175. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  176. package/src/hooks/useGsapAwareEditing.ts +101 -195
  177. package/src/hooks/useGsapAwareGroupMove.ts +179 -0
  178. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  179. package/src/hooks/useGsapKeyframeOps.sdkUsage.test.tsx +114 -0
  180. package/src/hooks/useGsapKeyframeOps.ts +36 -10
  181. package/src/hooks/useGsapScriptCommits.test.tsx +60 -1
  182. package/src/hooks/useGsapScriptCommits.ts +15 -10
  183. package/src/hooks/useGsapSelectionHandlers.test.tsx +1 -1
  184. package/src/hooks/useGsapSelectionHandlers.ts +24 -18
  185. package/src/hooks/useGsapTweenCache.ts +18 -12
  186. package/src/hooks/useLintModal.ts +2 -1
  187. package/src/hooks/usePersistentEditHistory.test.ts +277 -1
  188. package/src/hooks/usePersistentEditHistory.ts +47 -8
  189. package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +76 -2
  190. package/src/hooks/usePreviewInteraction.test.ts +21 -0
  191. package/src/hooks/usePreviewInteraction.ts +2 -0
  192. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  193. package/src/hooks/usePreviewPersistence.ts +13 -11
  194. package/src/hooks/useProjectFileWriter.ts +20 -19
  195. package/src/hooks/useRemoveBackground.ts +2 -1
  196. package/src/hooks/useRenderClipContent.test.ts +18 -0
  197. package/src/hooks/useRenderClipContent.ts +4 -5
  198. package/src/hooks/useSdkSession.ts +2 -1
  199. package/src/hooks/useServerConnection.ts +4 -3
  200. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  201. package/src/hooks/useThumbnailStill.test.ts +100 -0
  202. package/src/hooks/useThumbnailStill.ts +37 -0
  203. package/src/hooks/useTimelineDeleteOps.test.tsx +30 -22
  204. package/src/hooks/useTimelineDeleteOps.ts +22 -27
  205. package/src/hooks/useTimelineEditing.test.tsx +1 -1
  206. package/src/index.ts +4 -0
  207. package/src/player/components/AudioWaveform.tsx +3 -2
  208. package/src/player/components/AutomationSelectionMenu.tsx +3 -2
  209. package/src/player/components/ClipContextMenu.test.tsx +73 -0
  210. package/src/player/components/ClipContextMenu.tsx +33 -31
  211. package/src/player/components/CompositionThumbnail.tsx +2 -1
  212. package/src/player/components/KeyframeDiamondContextMenu.tsx +8 -9
  213. package/src/player/components/OutOfSyncBadge.tsx +5 -5
  214. package/src/player/components/TextClipContent.test.ts +19 -0
  215. package/src/player/components/TextClipContent.tsx +27 -0
  216. package/src/player/components/TrackGapContextMenu.tsx +3 -6
  217. package/src/player/components/clipMenuNormalize.ts +2 -1
  218. package/src/player/components/clipMenuToolItems.test.tsx +23 -0
  219. package/src/player/components/clipMenuToolItems.tsx +13 -9
  220. package/src/player/components/clipPeakMap.ts +2 -1
  221. package/src/player/components/timelineCallbacks.ts +1 -0
  222. package/src/player/components/timelineClipDragGestureLifecycle.ts +1 -1
  223. package/src/player/components/timelineClipGestureHandlers.test.ts +94 -0
  224. package/src/player/components/timelineClipGestureHandlers.ts +20 -2
  225. package/src/player/components/timelineEditing.ts +3 -1
  226. package/src/player/components/timelineLinkSelection.ts +35 -1
  227. package/src/player/components/useTimelineClipDrag.keyboard.test.tsx +14 -0
  228. package/src/player/components/useTimelineClipDrag.ts +6 -7
  229. package/src/player/hooks/timelinePlayerSync.test.ts +6 -0
  230. package/src/player/hooks/timelinePlayerSync.ts +6 -1
  231. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  232. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  233. package/src/player/lib/thumbnailScheduler.ts +10 -2
  234. package/src/player/lib/timelineElementHelpers.test.ts +87 -1
  235. package/src/player/lib/timelineElementHelpers.ts +6 -15
  236. package/src/player/lib/timelineText.ts +40 -0
  237. package/src/player/sceneSwap.ts +2 -1
  238. package/src/player/store/timelineElement.ts +12 -0
  239. package/src/styles/theme.css +3 -0
  240. package/src/telemetry/client.ts +2 -1
  241. package/src/utils/blockInstaller.ts +2 -1
  242. package/src/utils/elementGsap.test.ts +9 -0
  243. package/src/utils/elementGsap.ts +7 -0
  244. package/src/utils/gsapSoftReload.ts +2 -3
  245. package/src/utils/gsapUndoRestore.browser.test.ts +51 -45
  246. package/src/utils/gsapUndoRestore.ts +3 -6
  247. package/src/utils/keyframeUsage.ts +79 -0
  248. package/src/utils/previewFeatureUsage.test.ts +32 -0
  249. package/src/utils/previewFeatureUsage.ts +46 -0
  250. package/src/utils/previewPrefetch.ts +2 -1
  251. package/src/utils/razorSplitTransaction.ts +12 -8
  252. package/src/utils/softReloadTargets.ts +1 -3
  253. package/src/utils/studioApiFetch.test.ts +75 -0
  254. package/src/utils/studioApiFetch.ts +13 -0
  255. package/src/utils/studioFileHistory.ts +4 -1
  256. package/src/utils/studioHelpers.ts +7 -1
  257. package/src/utils/studioPendingEdits.test.ts +23 -6
  258. package/src/utils/studioPendingEdits.ts +32 -8
  259. package/src/utils/studioTelemetry.test.ts +107 -0
  260. package/src/utils/studioTelemetry.ts +51 -5
  261. package/src/utils/timelineCompositionInsert.ts +3 -2
  262. package/src/webmcp/StudioAgentTools.tsx +4 -3
  263. package/dist/chunk-BIUXP3PG.js.map +0 -1
  264. package/dist/chunk-K47E2ZJA.js.map +0 -1
  265. /package/dist/{chunk-VWT7STY3.js.map → chunk-J6Q572FA.js.map} +0 -0
  266. /package/dist/{domEditingLayers-V67MS4WP.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -1,3 +1,6 @@
1
+ import { useGsapAwareGroupMove } from "./useGsapAwareGroupMove";
2
+ import { observeGsapGesture } from "./gsapGestureOutcome";
3
+ import type { DomEditPersistOutcome } from "./domEditCommitTypes";
1
4
  /**
2
5
  * GSAP-aware move/resize/rotation wrappers that intercept geometry commits
3
6
  * for animated elements and route them through script mutation instead of
@@ -21,14 +24,9 @@ import {
21
24
  useGsapSaveFailureTelemetry,
22
25
  useSafeGsapCommitMutation,
23
26
  } from "./useSafeGsapCommitMutation";
24
- import type {
25
- CommitMutation,
26
- CommitMutationCall,
27
- CommitMutationOptions,
28
- } from "./gsapScriptCommitTypes";
27
+ import type { CommitMutation, CommitMutationOptions } from "./gsapScriptCommitTypes";
29
28
  import { setElementGsapPosition } from "../utils/elementGsap";
30
29
  import { logResize, logResizeSettle } from "../utils/resizeDebug";
31
- import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
32
30
  import type { MoveCommitOptions } from "../components/editor/domEditOverlayGestures";
33
31
  import { runGestureTransaction } from "./gestureTransaction";
34
32
  import {
@@ -40,33 +38,18 @@ import {
40
38
  } from "./gsapRuntimeKeyframes";
41
39
  import { assertGsapEditPersisted, saveMove } from "./gsapEditOutcome";
42
40
  import type { GsapAnimationFetchOptions } from "./useGsapAnimationFetchFallback";
43
- import { refuseGsapTakeover, type ElementOffsetStagerDeps } from "./elementOffsetStager";
41
+ import { type ElementOffsetStagerDeps } from "./elementOffsetStager";
44
42
  import {
45
43
  prepareCropResize,
46
44
  saveCropResize,
47
45
  writeSizeWithCrop,
48
46
  } from "../components/editor/cropResize";
49
47
 
50
- // Distinct coalesceKey per group drag so consecutive group drags don't fold
51
- // into one another's undo entry (module-local counter, not Date.now()).
52
- let groupDragCommitCounter = 0;
53
-
54
- function firstPreflightFailure(
55
- results: PromiseSettledResult<void>[],
56
- updates: DomEditGroupPathOffsetCommit[],
57
- ): { error: unknown; selection: DomEditSelection } | null {
58
- for (const [index, result] of results.entries()) {
59
- if (result.status !== "rejected") continue;
60
- const selection = updates[index]?.selection;
61
- if (selection) return { error: result.reason, selection };
62
- }
63
- return null;
64
- }
65
-
66
48
  export interface UseGsapAwareEditingParams {
67
49
  domEditSelection: DomEditSelection | null;
68
50
  selectedGsapAnimations: GsapAnimation[];
69
51
  gsapCommitMutation: CommitMutation | null;
52
+ activeCompPath?: string | null;
70
53
  previewIframeRef: React.RefObject<HTMLIFrameElement | null>;
71
54
  showToast: (message: string, tone?: "error" | "info") => void;
72
55
  bumpGsapCache: () => void;
@@ -79,6 +62,7 @@ export interface UseGsapAwareEditingParams {
79
62
  selection: DomEditSelection | null,
80
63
  mutationType: string,
81
64
  label: string,
65
+ toast?: boolean,
82
66
  ) => void;
83
67
  // DOM fallbacks (from useDomEditCommits)
84
68
  stageElementPositionOffset: (
@@ -86,15 +70,18 @@ export interface UseGsapAwareEditingParams {
86
70
  next: { x: number; y: number },
87
71
  plainTranslate: boolean,
88
72
  coalesceKey?: string,
89
- ) => { save: () => Promise<void>; rollback: () => void };
90
- handleDomRotationCommit: (selection: DomEditSelection, next: RotationCommit) => Promise<void>;
73
+ ) => { save: () => Promise<DomEditPersistOutcome | undefined>; rollback: () => void };
74
+ handleDomRotationCommit: (
75
+ selection: DomEditSelection,
76
+ next: RotationCommit,
77
+ ) => Promise<DomEditPersistOutcome | undefined>;
91
78
  handleDomBoxSizeCommit: (
92
79
  selection: DomEditSelection,
93
80
  next: { width: number; height: number },
94
81
  offset?: { x: number; y: number },
95
82
  restore?: () => void,
96
83
  undoKey?: string,
97
- ) => Promise<void>;
84
+ ) => Promise<DomEditPersistOutcome | undefined>;
98
85
  commitPositionPatchToHtml: ElementOffsetStagerDeps["commitPositionPatchToHtml"];
99
86
  // GSAP script commit ops (from useGsapScriptCommits)
100
87
  addGsapAnimation: (
@@ -132,6 +119,7 @@ export function useGsapAwareEditing({
132
119
  domEditSelection,
133
120
  selectedGsapAnimations,
134
121
  gsapCommitMutation,
122
+ activeCompPath,
135
123
  previewIframeRef,
136
124
  showToast,
137
125
  bumpGsapCache,
@@ -162,10 +150,12 @@ export function useGsapAwareEditing({
162
150
  next: { x: number; y: number },
163
151
  modifiers?: MoveCommitOptions,
164
152
  ) => {
165
- // A gesture and the panel carry their route; webmcp's moveTo decides here.
166
- if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element))
167
- return stageElementPositionOffset(selection, next, true).save();
168
- if (gsapCommitMutation) {
153
+ const writes = observeGsapGesture(gsapCommitMutation);
154
+ if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element)) {
155
+ const result = await stageElementPositionOffset(selection, next, true).save();
156
+ return writes.finish(result?.changed === true);
157
+ }
158
+ if (writes.commit) {
169
159
  try {
170
160
  const ownedAnimations = getGsapAnimationsForSelection(selection);
171
161
  const targetAnimations = Array.isArray(ownedAnimations)
@@ -176,11 +166,15 @@ export function useGsapAwareEditing({
176
166
  next,
177
167
  targetAnimations,
178
168
  previewIframeRef.current,
179
- gsapCommitMutation,
169
+ writes.commit,
180
170
  makeFetchFallback(selection),
181
171
  modifiers,
182
172
  );
183
- await saveMove(outcome, () => stageElementPositionOffset(selection, next, false).save());
173
+ await saveMove(outcome, async () => {
174
+ const result = await stageElementPositionOffset(selection, next, false).save();
175
+ writes.recordDomResult(result);
176
+ });
177
+ return writes.finish();
184
178
  } catch (error) {
185
179
  trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
186
180
  throw error;
@@ -197,137 +191,15 @@ export function useGsapAwareEditing({
197
191
  ],
198
192
  );
199
193
 
200
- // Multi-select (group) drag: each member takes the single drag's writer, so a member GSAP
201
- // does not position is saved on itself and the rest go through the GSAP intercept.
202
- const handleGsapAwareGroupPathOffsetCommit = useCallback(
203
- async (updates: DomEditGroupPathOffsetCommit[]) => {
204
- if (!gsapCommitMutation || updates.length === 0) return;
205
- // A group drag is ONE user action: fold every member's position write into
206
- // a single undo entry by forcing a shared coalesceKey (infinite window, so
207
- // it survives the N sequential server round-trips) onto each commit —
208
- // otherwise each member records its own entry and it takes N presses to undo.
209
- const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
210
- // Members are written one at a time, and a re-render re-runs the script with the OLD
211
- // position of every member not yet written, so they snap back until their own write
212
- // lands. The drafts are already on screen: hold the render until the last member.
213
- let renderOnCommit = false;
214
- const previewFallbackLatch = { pending: false };
215
- const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
216
- ...options,
217
- coalesceKey,
218
- coalesceMs: Number.POSITIVE_INFINITY,
219
- deferPreviewSync: !renderOnCommit,
220
- previewFallbackLatch,
221
- });
222
- // Every member writes the same file. Queue their mutations and send them as
223
- // ONE request instead of one round trip per member: the server reads, parses
224
- // and writes the composition once, and the preview patches once.
225
- const queued: CommitMutationCall[] = [];
226
- const flushQueued = async () => {
227
- if (queued.length === 0) return;
228
- const calls = queued.splice(0, queued.length);
229
- if (!gsapCommitMutation.batch) {
230
- for (const call of calls) {
231
- await gsapCommitMutation(call.selection, call.mutation, call.options);
232
- }
233
- return;
234
- }
235
- await gsapCommitMutation.batch(calls, {
236
- ...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
237
- label: "Move animated layer (group)",
238
- });
239
- };
240
- const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) => {
241
- queued.push({ selection, mutation, options: withGroupOptions(options) });
242
- return Promise.resolve();
243
- };
244
- const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
245
- // Members saved on themselves, each with its route: true for its CSS translate.
246
- const offsetMembers = new Map<DomEditSelection, boolean>();
247
- // Editability is user-atomic: prove every member can be written before the first source
248
- // mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
249
- // write nothing and share one in-flight parse per file, so they run together.
250
- const preflightResults = await Promise.allSettled(
251
- updates.map(async ({ selection, plainTranslate }) => {
252
- if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
253
- refuseGsapTakeover(selection.element, showToast);
254
- return void offsetMembers.set(selection, true);
255
- }
256
- const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
257
- preflightAnimations.set(selection, animations);
258
- const outcome = await tryGsapDragIntercept(
259
- selection,
260
- { x: 0, y: 0 },
261
- animations,
262
- previewIframeRef.current,
263
- coalescedCommit,
264
- undefined,
265
- { preflightOnly: true, group: true },
266
- );
267
- if (outcome.status === "element-offset") offsetMembers.set(selection, false);
268
- assertGsapEditPersisted(outcome);
269
- }),
270
- );
271
- const preflightFailure = firstPreflightFailure(preflightResults, updates);
272
- if (preflightFailure) {
273
- trackGsapInteractionFailure(
274
- preflightFailure.error,
275
- preflightFailure.selection,
276
- "drag",
277
- "Move animated layer (group)",
278
- );
279
- throw preflightFailure.error;
280
- }
281
- const lastScriptWrite = updates.findLastIndex(
282
- ({ selection }) => !offsetMembers.has(selection),
283
- );
284
- for (const [index, { selection, next }] of updates.entries()) {
285
- renderOnCommit = index === lastScriptWrite;
286
- const plain = offsetMembers.get(selection);
287
- if (plain !== undefined) {
288
- await stageElementPositionOffset(selection, next, plain, coalesceKey).save();
289
- continue;
290
- }
291
- try {
292
- const outcome = await tryGsapDragIntercept(
293
- selection,
294
- next,
295
- preflightAnimations.get(selection) ?? [],
296
- previewIframeRef.current,
297
- coalescedCommit,
298
- // The intercept re-reads the file to resolve a stale or shared tween.
299
- // Anything already queued has to be on disk before that read, or it
300
- // resolves against a file missing writes it is about to build on.
301
- async () => {
302
- await flushQueued();
303
- return makeFetchFallback(selection, { fresh: true })();
304
- },
305
- { preflightPassed: true },
306
- );
307
- assertGsapEditPersisted(outcome);
308
- } catch (error) {
309
- trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
310
- throw error;
311
- }
312
- }
313
- try {
314
- await flushQueued();
315
- } catch (error) {
316
- // The aggregate write has no uniquely failing member; do not misattribute
317
- // its telemetry to whichever member happened to be last in the array.
318
- trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
319
- throw error;
320
- }
321
- },
322
- [
323
- gsapCommitMutation,
324
- previewIframeRef,
325
- makeFetchFallback,
326
- trackGsapInteractionFailure,
327
- stageElementPositionOffset,
328
- showToast,
329
- ],
330
- );
194
+ const handleGsapAwareGroupPathOffsetCommit = useGsapAwareGroupMove({
195
+ gsapCommitMutation,
196
+ activeCompPath,
197
+ previewIframeRef,
198
+ makeFetchFallback,
199
+ trackGsapInteractionFailure,
200
+ stageElementPositionOffset,
201
+ showToast,
202
+ });
331
203
 
332
204
  const handleGsapAwareBoxSizeCommit = useCallback(
333
205
  async (
@@ -336,8 +208,11 @@ export function useGsapAwareEditing({
336
208
  offset?: { x: number; y: number },
337
209
  restore: () => void = () => undefined,
338
210
  ) => {
339
- if (!gsapWritesBox(selection.element))
340
- return handleDomBoxSizeCommit(selection, next, offset, restore);
211
+ const writes = observeGsapGesture(gsapCommitMutation);
212
+ if (!gsapWritesBox(selection.element)) {
213
+ const result = await handleDomBoxSizeCommit(selection, next, offset, restore);
214
+ return writes.finish(result?.changed === true);
215
+ }
341
216
  let targetAnimations: GsapAnimation[];
342
217
  try {
343
218
  const ownedAnimations = getGsapAnimationsForSelection(selection);
@@ -367,7 +242,28 @@ export function useGsapAwareEditing({
367
242
  let anchorMove: ReturnType<typeof stageElementPositionOffset> | null = null;
368
243
  const stageCrop = prepareCropResize(selection.element);
369
244
  let cropUndoKey: string | null = null;
370
- return runGestureTransaction({
245
+ const saveResizeAnchor = async (
246
+ ownsDragOffset: boolean,
247
+ commitMutation: CommitMutation,
248
+ coalesceKey: string,
249
+ ) => {
250
+ if (offset && !ownsDragOffset) {
251
+ const dragOutcome = await tryGsapDragIntercept(
252
+ selection,
253
+ offset,
254
+ targetAnimations,
255
+ previewIframeRef.current,
256
+ commitMutation,
257
+ makeFetchFallback(selection),
258
+ );
259
+ // Saved after the size, under its undo key, so the two are one step.
260
+ await saveMove(dragOutcome, async () => {
261
+ const plain = !gsapWritesPosition(selection.element);
262
+ anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
263
+ });
264
+ }
265
+ };
266
+ await runGestureTransaction({
371
267
  element: selection.element,
372
268
  label: "Resize layer",
373
269
  settle: () => {
@@ -383,8 +279,8 @@ export function useGsapAwareEditing({
383
279
  setElementGsapPosition(selection.element, newX, newY);
384
280
  },
385
281
  persist: async (commit, coalesceKey) => {
386
- if (gsapCommitMutation) {
387
- const commitMutation = commit(gsapCommitMutation);
282
+ if (writes.commit) {
283
+ const commitMutation = commit(writes.commit);
388
284
  try {
389
285
  const outcome = await tryGsapResizeIntercept(
390
286
  selection,
@@ -393,12 +289,20 @@ export function useGsapAwareEditing({
393
289
  previewIframeRef.current,
394
290
  commitMutation,
395
291
  makeFetchFallback(selection),
292
+ offset,
396
293
  );
397
294
  assertGsapEditPersisted(outcome);
398
295
  // Saved before the buffered GSAP writes, so their reload stays the gesture's last render.
399
- if (outcome.status === "element-size")
400
- await handleDomBoxSizeCommit(selection, next, undefined, undefined, coalesceKey);
401
- else cropUndoKey = coalesceKey;
296
+ if (outcome.status === "element-size") {
297
+ const result = await handleDomBoxSizeCommit(
298
+ selection,
299
+ next,
300
+ undefined,
301
+ undefined,
302
+ coalesceKey,
303
+ );
304
+ writes.recordDomResult(result);
305
+ } else cropUndoKey = coalesceKey;
402
306
  // What the resize did, not what its tweens suggest: a scale hold still commits a size.
403
307
  const ownsDragOffset =
404
308
  outcome.status === "persisted" && outcome.ownsDragOffset === true;
@@ -409,21 +313,7 @@ export function useGsapAwareEditing({
409
313
  });
410
314
  // A resize that moved the element itself has already written
411
315
  // where it landed. Everything else leaves the anchor to the drag.
412
- if (offset && !ownsDragOffset) {
413
- const dragOutcome = await tryGsapDragIntercept(
414
- selection,
415
- offset,
416
- targetAnimations,
417
- previewIframeRef.current,
418
- commitMutation,
419
- makeFetchFallback(selection),
420
- );
421
- // Saved after the size, under its undo key, so the two are one step.
422
- await saveMove(dragOutcome, async () => {
423
- const plain = !gsapWritesPosition(selection.element);
424
- anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
425
- });
426
- }
316
+ await saveResizeAnchor(ownsDragOffset, commitMutation, coalesceKey);
427
317
  logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
428
318
  return;
429
319
  } catch (error) {
@@ -434,10 +324,17 @@ export function useGsapAwareEditing({
434
324
  throw new Error("Resize of a GSAP-owned box has no GSAP writer");
435
325
  },
436
326
  afterBufferedCommitsSaved: async () => {
437
- await anchorMove?.save();
327
+ const anchorResult = await anchorMove?.save();
328
+ writes.recordDomResult(anchorResult);
438
329
  // Only now is the size live for every caller, drag or not.
439
330
  if (cropUndoKey) {
440
- await saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey);
331
+ const cropResult = await saveCropResize(
332
+ stageCrop,
333
+ selection,
334
+ commitPositionPatchToHtml,
335
+ cropUndoKey,
336
+ );
337
+ writes.recordDomResult(cropResult);
441
338
  }
442
339
  },
443
340
  restore: () => {
@@ -446,6 +343,7 @@ export function useGsapAwareEditing({
446
343
  },
447
344
  skipPixelAssert: hasLivePositionTween,
448
345
  });
346
+ return writes.finish();
449
347
  },
450
348
  [
451
349
  handleDomBoxSizeCommit,
@@ -461,9 +359,12 @@ export function useGsapAwareEditing({
461
359
 
462
360
  const handleGsapAwareRotationCommit = useCallback(
463
361
  async (selection: DomEditSelection, next: RotationCommit) => {
464
- if (next.plain || !gsapWritesRotation(selection.element))
465
- return handleDomRotationCommit(selection, next);
466
- if (gsapCommitMutation) {
362
+ const writes = observeGsapGesture(gsapCommitMutation);
363
+ if (next.plain || !gsapWritesRotation(selection.element)) {
364
+ const result = await handleDomRotationCommit(selection, next);
365
+ return writes.finish(result?.changed === true);
366
+ }
367
+ if (writes.commit) {
467
368
  try {
468
369
  const targetAnimations = await getGsapAnimationsForSelection(selection);
469
370
  // A keyframe or a tl.set; a computed source rejects, so the gesture restores its draft.
@@ -472,10 +373,11 @@ export function useGsapAwareEditing({
472
373
  next.angle,
473
374
  targetAnimations,
474
375
  previewIframeRef.current,
475
- gsapCommitMutation,
376
+ writes.commit,
476
377
  makeFetchFallback(selection),
477
378
  );
478
379
  assertGsapEditPersisted(outcome);
380
+ return writes.finish();
479
381
  } catch (error) {
480
382
  trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
481
383
  throw error;
@@ -564,12 +466,16 @@ export function useGsapAwareEditing({
564
466
  );
565
467
 
566
468
  const commitMutation = useCallback(
567
- async (mutation: Record<string, unknown>, options: { label: string; softReload?: boolean }) => {
568
- if (!domEditSelection) return;
469
+ async (
470
+ mutation: Record<string, unknown>,
471
+ options: CommitMutationOptions,
472
+ selection = domEditSelection,
473
+ ) => {
474
+ if (!selection) return;
569
475
  // Return (await) the safe-commit chain so consumers that `await
570
476
  // session.commitMutation(...)` (gesture recording, enable-keyframes) run
571
477
  // their post-actions only after the server save has settled.
572
- await safeGsapCommit(domEditSelection, mutation, options);
478
+ await safeGsapCommit(selection, mutation, options);
573
479
  },
574
480
  [domEditSelection, safeGsapCommit],
575
481
  );
@@ -0,0 +1,179 @@
1
+ import { useCallback } from "react";
2
+ import type { DomEditSelection } from "../components/editor/domEditingTypes";
3
+ import type { DomEditGroupPathOffsetCommit } from "../components/editor/DomEditOverlay";
4
+ import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
5
+ import type { CommitMutationCall, CommitMutationOptions } from "./gsapScriptCommitTypes";
6
+ import type { UseGsapAwareEditingParams } from "./useGsapAwareEditing";
7
+ import { observeGsapGesture } from "./gsapGestureOutcome";
8
+ import { gsapWritesPosition } from "./gsapRuntimeKeyframes";
9
+ import { refuseGsapTakeover } from "./elementOffsetStager";
10
+ import { tryGsapDragIntercept } from "./gsapRuntimeBridge";
11
+ import { assertGsapEditPersisted } from "./gsapEditOutcome";
12
+ import { firstPreflightFailure } from "./gsapGroupPreflight";
13
+
14
+ // A distinct key keeps consecutive group drags in separate undo entries.
15
+ let groupDragCommitCounter = 0;
16
+
17
+ export function useGsapAwareGroupMove({
18
+ gsapCommitMutation,
19
+ activeCompPath,
20
+ previewIframeRef,
21
+ makeFetchFallback,
22
+ trackGsapInteractionFailure,
23
+ stageElementPositionOffset,
24
+ showToast,
25
+ }: Pick<
26
+ UseGsapAwareEditingParams,
27
+ | "gsapCommitMutation"
28
+ | "activeCompPath"
29
+ | "previewIframeRef"
30
+ | "makeFetchFallback"
31
+ | "trackGsapInteractionFailure"
32
+ | "stageElementPositionOffset"
33
+ | "showToast"
34
+ >) {
35
+ const handleGsapAwareGroupPathOffsetCommit = useCallback(
36
+ async (
37
+ updates: DomEditGroupPathOffsetCommit[],
38
+ options: { refusalToast?: boolean } = {},
39
+ ): Promise<import("../utils/previewFeatureUsage").GeometryCommitResult> => {
40
+ const writes = observeGsapGesture(gsapCommitMutation);
41
+ const writer = writes.commit;
42
+ if (!writer) return { ok: true, changed: false };
43
+ const toastRefusal = options.refusalToast !== false;
44
+ // One coalesce key across slow writes keeps the group drag in one undo entry.
45
+ const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
46
+ // Hold the render until every member is saved, or unwritten members snap back.
47
+ let renderOnCommit = false;
48
+ const previewFallbackLatch = { pending: false };
49
+ const withGroupOptions = (options: CommitMutationOptions): CommitMutationOptions => ({
50
+ ...options,
51
+ coalesceKey,
52
+ coalesceMs: Number.POSITIVE_INFINITY,
53
+ deferPreviewSync: !renderOnCommit,
54
+ previewFallbackLatch,
55
+ });
56
+ // Batch members in the same file to avoid repeating its read, parse, write and preview patch.
57
+ const queued: CommitMutationCall[] = [];
58
+ const flushQueued = async () => {
59
+ if (queued.length === 0) return;
60
+ const calls = queued.splice(0, queued.length);
61
+ if (!writer.batch) {
62
+ for (const call of calls) {
63
+ await writer(call.selection, call.mutation, call.options);
64
+ }
65
+ return;
66
+ }
67
+ await writer.batch(calls, {
68
+ ...(calls.at(-1)?.options ?? { label: "Move animated layer (group)" }),
69
+ label: "Move animated layer (group)",
70
+ });
71
+ };
72
+ const coalescedCommit: typeof gsapCommitMutation = (selection, mutation, options) => {
73
+ queued.push({ selection, mutation, options: withGroupOptions(options) });
74
+ return Promise.resolve();
75
+ };
76
+ const preflightAnimations = new Map<DomEditSelection, GsapAnimation[]>();
77
+ // Members saved on themselves, each with its route: true for its CSS translate.
78
+ const offsetMembers = new Map<DomEditSelection, boolean>();
79
+ // Editability is user-atomic: prove every member can be written before the first source
80
+ // mutation, so a blocked member never leaves earlier siblings partially moved. Preflights
81
+ // write nothing and share one in-flight parse per file, so they run together.
82
+ const preflightResults = await Promise.allSettled(
83
+ updates.map(async ({ selection, next, plainTranslate }) => {
84
+ if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
85
+ refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {});
86
+ return void offsetMembers.set(selection, true);
87
+ }
88
+ const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
89
+ preflightAnimations.set(selection, animations);
90
+ const outcome = await tryGsapDragIntercept(
91
+ selection,
92
+ next,
93
+ animations,
94
+ previewIframeRef.current,
95
+ coalescedCommit,
96
+ undefined,
97
+ { preflightOnly: true, group: true },
98
+ );
99
+ if (outcome.status === "element-offset") offsetMembers.set(selection, false);
100
+ assertGsapEditPersisted(outcome);
101
+ }),
102
+ );
103
+ const preflightFailure = firstPreflightFailure(
104
+ preflightResults,
105
+ updates,
106
+ offsetMembers,
107
+ activeCompPath,
108
+ );
109
+ if (preflightFailure) {
110
+ trackGsapInteractionFailure(
111
+ preflightFailure.error,
112
+ preflightFailure.selection,
113
+ "drag",
114
+ "Move animated layer (group)",
115
+ toastRefusal,
116
+ );
117
+ throw preflightFailure.error;
118
+ }
119
+ const lastScriptWrite = updates.findLastIndex(
120
+ ({ selection }) => !offsetMembers.has(selection),
121
+ );
122
+ for (const [index, { selection, next }] of updates.entries()) {
123
+ renderOnCommit = index === lastScriptWrite;
124
+ const plain = offsetMembers.get(selection);
125
+ if (plain !== undefined) {
126
+ const result = await stageElementPositionOffset(
127
+ selection,
128
+ next,
129
+ plain,
130
+ coalesceKey,
131
+ ).save();
132
+ writes.recordDomResult(result);
133
+ continue;
134
+ }
135
+ try {
136
+ const outcome = await tryGsapDragIntercept(
137
+ selection,
138
+ next,
139
+ preflightAnimations.get(selection) ?? [],
140
+ previewIframeRef.current,
141
+ coalescedCommit,
142
+ // The intercept re-reads the file to resolve a stale or shared tween.
143
+ // Anything already queued has to be on disk before that read, or it
144
+ // resolves against a file missing writes it is about to build on.
145
+ async () => {
146
+ await flushQueued();
147
+ return makeFetchFallback(selection, { fresh: true })();
148
+ },
149
+ { preflightPassed: true },
150
+ );
151
+ assertGsapEditPersisted(outcome);
152
+ } catch (error) {
153
+ trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
154
+ throw error;
155
+ }
156
+ }
157
+ try {
158
+ await flushQueued();
159
+ return writes.finish();
160
+ } catch (error) {
161
+ // The aggregate write has no uniquely failing member; do not misattribute
162
+ // its telemetry to whichever member happened to be last in the array.
163
+ trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
164
+ throw error;
165
+ }
166
+ },
167
+ [
168
+ gsapCommitMutation,
169
+ activeCompPath,
170
+ previewIframeRef,
171
+ makeFetchFallback,
172
+ trackGsapInteractionFailure,
173
+ stageElementPositionOffset,
174
+ showToast,
175
+ ],
176
+ );
177
+
178
+ return handleGsapAwareGroupPathOffsetCommit;
179
+ }
@@ -4,8 +4,8 @@ import { trackStudioEditBlocked, trackStudioSaveFailure } from "../utils/studioS
4
4
  import { isGsapEditBlockedError } from "./gsapEditOutcome";
5
5
  import { wasAlreadyToasted } from "./domEditPersistFailure";
6
6
 
7
- function failureToast(error: unknown): string | null {
8
- if (wasAlreadyToasted(error)) return null;
7
+ function failureToast(error: unknown, toast: boolean): string | null {
8
+ if (!toast || wasAlreadyToasted(error)) return null;
9
9
  return isGsapEditBlockedError(error) ? error.message : "Failed to save animated edit.";
10
10
  }
11
11
 
@@ -14,7 +14,13 @@ export function useGsapInteractionFailureTelemetry(
14
14
  showToast: (message: string, tone?: "error" | "info") => void,
15
15
  ) {
16
16
  return useCallback(
17
- (error: unknown, selection: DomEditSelection | null, mutationType: string, label: string) => {
17
+ (
18
+ error: unknown,
19
+ selection: DomEditSelection | null,
20
+ mutationType: string,
21
+ label: string,
22
+ toast?: boolean,
23
+ ) => {
18
24
  const report = isGsapEditBlockedError(error)
19
25
  ? trackStudioEditBlocked
20
26
  : trackStudioSaveFailure;
@@ -28,7 +34,7 @@ export function useGsapInteractionFailureTelemetry(
28
34
  targetSelector: selection?.selector,
29
35
  targetSourceFile: selection?.sourceFile,
30
36
  });
31
- const message = failureToast(error);
37
+ const message = failureToast(error, toast !== false);
32
38
  if (message) showToast(message, "error");
33
39
  },
34
40
  [activeCompPath, showToast],