@hyperframes/studio 0.8.115 → 0.8.117

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 (163) hide show
  1. package/dist/assets/{hyperframes-player-C-9N8nk0.js → hyperframes-player-5fcgSstJ.js} +1 -1
  2. package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
  3. package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
  4. package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.js} +1 -1
  5. package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
  6. package/dist/chunk-3F47EFCT.js.map +1 -0
  7. package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
  8. package/dist/index.d.ts +132 -41
  9. package/dist/index.html +1 -1
  10. package/dist/index.js +1184 -1084
  11. package/dist/index.js.map +1 -1
  12. package/package.json +8 -8
  13. package/src/captions/hooks/useCaptionSync.ts +10 -6
  14. package/src/components/ProjectUnreachableBanner.tsx +3 -3
  15. package/src/components/editor/DomEditOverlay.tsx +1 -0
  16. package/src/components/editor/domEditOverlayGestures.ts +1 -0
  17. package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
  18. package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
  19. package/src/components/editor/domGroupMoveBy.ts +95 -0
  20. package/src/components/editor/dragUndoPaint.test.ts +5 -5
  21. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
  22. package/src/components/editor/manualEditingAvailability.ts +8 -0
  23. package/src/components/editor/manualOffsetDrag.ts +8 -9
  24. package/src/components/editor/motionPathSelection.test.ts +6 -31
  25. package/src/components/editor/plainTranslate.ts +5 -3
  26. package/src/components/editor/probeSourceElement.ts +2 -1
  27. package/src/components/editor/propertyPanelFont.tsx +3 -2
  28. package/src/components/editor/propertyPanelHelpers.ts +2 -2
  29. package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
  30. package/src/components/editor/rotationDraft.ts +2 -1
  31. package/src/components/editor/useColorGradingController.ts +2 -1
  32. package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
  33. package/src/components/editor/useFxCarveNodes.ts +2 -1
  34. package/src/components/editor/useFxLevelling.ts +2 -1
  35. package/src/components/feedback/projectProvenance.ts +2 -1
  36. package/src/components/nle/NLEContext.test.ts +1 -0
  37. package/src/components/nle/NLEContext.tsx +2 -1
  38. package/src/components/nle/NLEPreview.tsx +12 -10
  39. package/src/components/nle/PreviewPoster.tsx +2 -1
  40. package/src/components/renders/useFfmpegStatus.ts +2 -1
  41. package/src/components/renders/useRenderQueue.ts +5 -4
  42. package/src/components/sidebar/AssetsTab.tsx +2 -1
  43. package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
  44. package/src/contexts/DomEditContext.tsx +4 -0
  45. package/src/hooks/domSelectionTestHarness.ts +23 -0
  46. package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
  47. package/src/hooks/domStyleCommit.ts +8 -1
  48. package/src/hooks/draggedGsapPosition.ts +10 -0
  49. package/src/hooks/gsapDragCommit.test.ts +75 -164
  50. package/src/hooks/gsapDragCommit.ts +2 -105
  51. package/src/hooks/gsapDragPositionCommit.ts +30 -393
  52. package/src/hooks/gsapEditOutcome.ts +22 -2
  53. package/src/hooks/gsapGroupPreflight.ts +34 -0
  54. package/src/hooks/gsapMutationClient.ts +4 -3
  55. package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
  56. package/src/hooks/gsapParsedTween.test.ts +62 -0
  57. package/src/hooks/gsapParsedTween.ts +243 -0
  58. package/src/hooks/gsapPositionDetection.test.ts +78 -0
  59. package/src/hooks/gsapPositionDetection.ts +52 -33
  60. package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
  61. package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
  62. package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
  63. package/src/hooks/gsapResizeIntercept.test.ts +63 -11
  64. package/src/hooks/gsapResizeIntercept.ts +79 -169
  65. package/src/hooks/gsapResizeSweep.test.ts +16 -33
  66. package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
  67. package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
  68. package/src/hooks/gsapRuntimeBridge.ts +105 -131
  69. package/src/hooks/gsapRuntimeKeyframes.ts +16 -3
  70. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  71. package/src/hooks/gsapScriptCommitTypes.ts +9 -1
  72. package/src/hooks/gsapShared.ts +7 -0
  73. package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
  74. package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
  75. package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
  76. package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
  77. package/src/hooks/gsapValueAtPlayhead.ts +292 -0
  78. package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
  79. package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
  80. package/src/hooks/keyframeCacheAstLoad.ts +2 -1
  81. package/src/hooks/newTweenTarget.test.ts +41 -76
  82. package/src/hooks/plainRotation.ts +5 -1
  83. package/src/hooks/timelineTimingSync.test.ts +1 -0
  84. package/src/hooks/timelineTimingSync.ts +2 -1
  85. package/src/hooks/useAskAgentModal.ts +2 -1
  86. package/src/hooks/useBlockCatalog.ts +2 -1
  87. package/src/hooks/useCaptionDetection.ts +2 -1
  88. package/src/hooks/useCompositionContentLoader.ts +2 -1
  89. package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
  90. package/src/hooks/useDomEditPersist.test.tsx +335 -0
  91. package/src/hooks/useDomEditSession.test.tsx +30 -1
  92. package/src/hooks/useDomEditSession.ts +10 -0
  93. package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
  94. package/src/hooks/useDomEditTextCommits.ts +8 -2
  95. package/src/hooks/useDomGeometryCommit.test.tsx +5 -1
  96. package/src/hooks/useDomGeometryCommit.ts +7 -9
  97. package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
  98. package/src/hooks/useEditHistoryActions.test.tsx +15 -0
  99. package/src/hooks/useEditHistoryActions.ts +16 -5
  100. package/src/hooks/useElementLifecycleOps.ts +2 -1
  101. package/src/hooks/useEnableKeyframes.test.ts +32 -1
  102. package/src/hooks/useEnableKeyframes.ts +2 -1
  103. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
  104. package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
  105. package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
  106. package/src/hooks/useFileManager.ts +14 -10
  107. package/src/hooks/useFileTree.ts +3 -2
  108. package/src/hooks/useFrameCapture.ts +5 -1
  109. package/src/hooks/useFreezeFrame.test.ts +39 -3
  110. package/src/hooks/useFreezeFrame.ts +7 -3
  111. package/src/hooks/useGroupCommits.ts +2 -1
  112. package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
  113. package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
  114. package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
  115. package/src/hooks/useGsapAwareEditing.ts +17 -8
  116. package/src/hooks/useGsapAwareGroupMove.ts +18 -18
  117. package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
  118. package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
  119. package/src/hooks/useGsapScriptCommits.ts +21 -17
  120. package/src/hooks/useLintModal.ts +2 -1
  121. package/src/hooks/usePersistentEditHistory.test.ts +302 -3
  122. package/src/hooks/usePersistentEditHistory.ts +50 -17
  123. package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
  124. package/src/hooks/usePreviewPersistence.ts +13 -11
  125. package/src/hooks/useProjectFileWriter.ts +20 -19
  126. package/src/hooks/useRemoveBackground.ts +2 -1
  127. package/src/hooks/useSdkSession.ts +2 -1
  128. package/src/hooks/useServerConnection.ts +4 -3
  129. package/src/hooks/useStudioSelectionPublisher.ts +2 -1
  130. package/src/hooks/useThumbnailStill.test.ts +100 -0
  131. package/src/hooks/useThumbnailStill.ts +37 -0
  132. package/src/hooks/useTimelineDeleteOps.ts +2 -1
  133. package/src/index.ts +4 -0
  134. package/src/player/components/AudioWaveform.tsx +3 -2
  135. package/src/player/components/CompositionThumbnail.tsx +2 -1
  136. package/src/player/components/clipMenuNormalize.ts +2 -1
  137. package/src/player/components/clipPeakMap.ts +2 -1
  138. package/src/player/hooks/useTimelinePlayer.ts +5 -1
  139. package/src/player/lib/thumbnailScheduler.test.ts +16 -0
  140. package/src/player/lib/thumbnailScheduler.ts +10 -2
  141. package/src/player/sceneSwap.ts +2 -1
  142. package/src/telemetry/client.ts +2 -1
  143. package/src/utils/blockInstaller.ts +2 -1
  144. package/src/utils/elementGsap.test.ts +9 -0
  145. package/src/utils/elementGsap.ts +7 -0
  146. package/src/utils/gsapSoftReload.test.ts +39 -0
  147. package/src/utils/gsapSoftReload.ts +9 -14
  148. package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
  149. package/src/utils/gsapUndoRestore.ts +3 -6
  150. package/src/utils/previewPrefetch.ts +2 -1
  151. package/src/utils/razorSplitTransaction.ts +12 -8
  152. package/src/utils/softReloadTargets.ts +5 -6
  153. package/src/utils/studioApiFetch.test.ts +75 -0
  154. package/src/utils/studioApiFetch.ts +13 -0
  155. package/src/utils/studioFileHistory.ts +5 -1
  156. package/src/utils/studioHelpers.ts +7 -1
  157. package/src/utils/studioPendingEdits.test.ts +23 -6
  158. package/src/utils/studioPendingEdits.ts +32 -8
  159. package/src/utils/studioTelemetry.ts +2 -1
  160. package/src/utils/timelineCompositionInsert.ts +3 -2
  161. package/src/webmcp/StudioAgentTools.tsx +2 -1
  162. package/dist/chunk-TXZMMOMA.js.map +0 -1
  163. /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
package/dist/index.js CHANGED
@@ -71,6 +71,10 @@ import {
71
71
  } from "./chunk-5VSXRGIX.js";
72
72
  import {
73
73
  DOM_EDIT_LAYER_OBSERVER_INIT,
74
+ STUDIO_FLAT_INSPECTOR_ENABLED,
75
+ STUDIO_SDK_CUTOVER_ENABLED,
76
+ STUDIO_SDK_CUTOVER_FAMILIES,
77
+ STUDIO_SDK_RESOLVER_SHADOW_ENABLED,
74
78
  buildDefaultDomEditTextField,
75
79
  buildDomEditPatchTarget,
76
80
  buildDomEditRichTextPatchOperation,
@@ -88,13 +92,16 @@ import {
88
92
  getDomEditTargetKey,
89
93
  getDomLayerPatchTarget,
90
94
  isElementComputedVisible,
95
+ isSdkFamilyEnabled,
91
96
  isTextEditableSelection,
92
97
  liveLayerElement,
98
+ manualEditingAvailability_exports,
93
99
  readHfId,
94
100
  resolveAllVisualDomEditTargets,
95
101
  resolveDomEditSelection,
96
- serializeDomEditTextFields
97
- } from "./chunk-TXZMMOMA.js";
102
+ serializeDomEditTextFields,
103
+ studioApiFetch
104
+ } from "./chunk-3F47EFCT.js";
98
105
  import {
99
106
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
100
107
  LAYER_REVEAL_PRIOR_Z_ATTR,
@@ -155,9 +162,7 @@ import {
155
162
  toHexColor,
156
163
  wrapTimeline
157
164
  } from "./chunk-5EKWJZLM.js";
158
- import {
159
- __export
160
- } from "./chunk-PZ5AY32C.js";
165
+ import "./chunk-PZ5AY32C.js";
161
166
 
162
167
  // src/components/EditorShell.tsx
163
168
  import { memo as memo33, useCallback as useCallback74 } from "react";
@@ -545,7 +550,7 @@ async function flushEvents() {
545
550
  const controller = new AbortController();
546
551
  const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
547
552
  try {
548
- await fetch(`${POSTHOG_HOST}/batch/`, {
553
+ await studioApiFetch(`${POSTHOG_HOST}/batch/`, {
549
554
  method: "POST",
550
555
  headers: { "Content-Type": "application/json" },
551
556
  body: JSON.stringify({ api_key: POSTHOG_API_KEY2, batch }),
@@ -4218,8 +4223,8 @@ function useAbandonEditsOnReadOnly(readOnly, abandon) {
4218
4223
  if (readOnly) abandonNow();
4219
4224
  }, [readOnly]);
4220
4225
  }
4221
- function reportPressOnTravel(start, refuse, threshold = 4) {
4222
- if (!refuse) return;
4226
+ function reportPressOnTravel(start, refuse2, threshold = 4) {
4227
+ if (!refuse2) return;
4223
4228
  const stop2 = () => {
4224
4229
  window.removeEventListener("pointermove", onMove, true);
4225
4230
  window.removeEventListener("pointerup", stop2, true);
@@ -4229,7 +4234,7 @@ function reportPressOnTravel(start, refuse, threshold = 4) {
4229
4234
  if (Math.hypot(event.clientX - start.clientX, event.clientY - start.clientY) < threshold)
4230
4235
  return;
4231
4236
  stop2();
4232
- refuse();
4237
+ refuse2();
4233
4238
  };
4234
4239
  window.addEventListener("pointermove", onMove, true);
4235
4240
  window.addEventListener("pointerup", stop2, true);
@@ -4359,6 +4364,10 @@ var PROPERTY_DEFAULTS = {
4359
4364
  width: 100,
4360
4365
  height: 100
4361
4366
  };
4367
+ function keyframeEases(anim) {
4368
+ if (!anim.keyframes) return { easeEach: anim.ease };
4369
+ return { ease: anim.ease, easeEach: anim.keyframes.easeEach };
4370
+ }
4362
4371
  function isInstantHold(animation) {
4363
4372
  return animation.method === "set" || (animation.method === "to" || animation.method === "fromTo") && resolveTweenDuration(animation) === 0;
4364
4373
  }
@@ -6736,7 +6745,7 @@ function flush() {
6736
6745
  }
6737
6746
  function send(url, payload) {
6738
6747
  try {
6739
- void fetch(url, {
6748
+ void studioApiFetch(url, {
6740
6749
  method: "POST",
6741
6750
  headers: { "Content-Type": "application/json" },
6742
6751
  body: payload,
@@ -11634,6 +11643,7 @@ function asElement(target) {
11634
11643
  // src/utils/studioPendingEdits.ts
11635
11644
  var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
11636
11645
  var pendingEdits = /* @__PURE__ */ new Map();
11646
+ var NOT_SAVED = () => Promise.resolve(false);
11637
11647
  var adopting = false;
11638
11648
  function waitForPostBlurEffects() {
11639
11649
  return new Promise((resolve) => setTimeout(resolve, 0));
@@ -11677,7 +11687,7 @@ function trackStudioPendingEdit(result) {
11677
11687
  if (!result) return void 0;
11678
11688
  const promise = Promise.resolve(result);
11679
11689
  if (adopting) return promise;
11680
- pendingEdits.set(promise, { revert: null });
11690
+ pendingEdits.set(promise, { revert: null, landed: NOT_SAVED });
11681
11691
  promise.then(
11682
11692
  () => pendingEdits.delete(promise),
11683
11693
  () => pendingEdits.delete(promise)
@@ -11696,6 +11706,8 @@ function beginStudioPendingEdit(revert) {
11696
11706
  const promise = trackStudioPendingEdit(new Promise((resolve) => settle = resolve));
11697
11707
  const entry2 = pendingEdits.get(promise);
11698
11708
  entry2.revert = revert;
11709
+ let landed = Promise.resolve(false);
11710
+ entry2.landed = () => landed;
11699
11711
  return {
11700
11712
  settle,
11701
11713
  reverted: () => entry2.revert === null && revert !== null,
@@ -11703,7 +11715,12 @@ function beginStudioPendingEdit(revert) {
11703
11715
  adopt(start) {
11704
11716
  adopting = true;
11705
11717
  try {
11706
- return start();
11718
+ const committed = start();
11719
+ landed = Promise.resolve(committed).then(
11720
+ () => true,
11721
+ () => false
11722
+ );
11723
+ return committed;
11707
11724
  } catch (error) {
11708
11725
  settle();
11709
11726
  throw error;
@@ -11713,14 +11730,19 @@ function beginStudioPendingEdit(revert) {
11713
11730
  }
11714
11731
  };
11715
11732
  }
11716
- function revertNewestStudioPendingEdit() {
11733
+ function paintBackNewestStudioPendingEdit() {
11717
11734
  const newest = [...pendingEdits.values()].at(-1);
11718
11735
  const revert = newest?.revert;
11719
11736
  if (!newest || !revert) return null;
11720
11737
  newest.revert = null;
11721
- return revert();
11738
+ return { showAgain: revert(), landed: newest.landed };
11722
11739
  }
11723
- async function flushStudioPendingEdits() {
11740
+ function revertNewestStudioPendingEdit() {
11741
+ return paintBackNewestStudioPendingEdit()?.showAgain ?? null;
11742
+ }
11743
+ async function flushStudioPendingEdits({
11744
+ onlyCurrent = false
11745
+ } = {}) {
11724
11746
  const active = focusedField();
11725
11747
  if (active) {
11726
11748
  active.blur();
@@ -11731,10 +11753,12 @@ async function flushStudioPendingEdits() {
11731
11753
  window.dispatchEvent(
11732
11754
  new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
11733
11755
  );
11756
+ const current = onlyCurrent ? new Set(pendingEdits.keys()) : null;
11757
+ const waiting = () => [...pendingEdits.keys()].filter((edit) => !current || current.has(edit));
11734
11758
  let conflict;
11735
11759
  let firstFailure;
11736
- while (detail.promises.length > 0 || pendingEdits.size > 0) {
11737
- const promises = [...detail.promises, ...pendingEdits.keys()];
11760
+ while (detail.promises.length > 0 || waiting().length > 0) {
11761
+ const promises = [...detail.promises, ...waiting()];
11738
11762
  detail.promises = [];
11739
11763
  const batchFailures = inspectDrainFailures(await Promise.allSettled(promises));
11740
11764
  conflict ??= batchFailures.conflict;
@@ -11774,6 +11798,7 @@ var SAVES_AN_EDIT = {
11774
11798
  handleDomAttributeBatchCommit: true,
11775
11799
  handleDomPathOffsetCommit: true,
11776
11800
  handleDomGroupPathOffsetCommit: true,
11801
+ handleDomGroupMoveBy: true,
11777
11802
  handleDomZIndexReorderCommit: true,
11778
11803
  handleDomBoxSizeCommit: true,
11779
11804
  handleDomRotationCommit: true,
@@ -11894,6 +11919,7 @@ function DomEditProvider({
11894
11919
  handleDomAttributeBatchCommit,
11895
11920
  handleDomPathOffsetCommit,
11896
11921
  handleDomGroupPathOffsetCommit,
11922
+ handleDomGroupMoveBy,
11897
11923
  handleDomZIndexReorderCommit,
11898
11924
  handleDomBoxSizeCommit,
11899
11925
  handleDomRotationCommit,
@@ -11983,6 +12009,7 @@ function DomEditProvider({
11983
12009
  handleDomAttributeBatchCommit,
11984
12010
  handleDomPathOffsetCommit,
11985
12011
  handleDomGroupPathOffsetCommit,
12012
+ handleDomGroupMoveBy,
11986
12013
  handleDomZIndexReorderCommit,
11987
12014
  handleDomBoxSizeCommit,
11988
12015
  handleDomRotationCommit,
@@ -12059,6 +12086,7 @@ function DomEditProvider({
12059
12086
  handleDomAttributeBatchCommit,
12060
12087
  handleDomPathOffsetCommit,
12061
12088
  handleDomGroupPathOffsetCommit,
12089
+ handleDomGroupMoveBy,
12062
12090
  handleDomZIndexReorderCommit,
12063
12091
  handleDomBoxSizeCommit,
12064
12092
  handleDomRotationCommit,
@@ -14543,8 +14571,8 @@ function TimelineProvider({
14543
14571
  useLayoutEffect6(() => {
14544
14572
  onReadOnlyPressRef.current = onReadOnlyPress;
14545
14573
  });
14546
- const refuse = useCallback24(() => onReadOnlyPressRef.current?.(), []);
14547
- const readOnlyPress = readOnly ? refuse : null;
14574
+ const refuse2 = useCallback24(() => onReadOnlyPressRef.current?.(), []);
14575
+ const readOnlyPress = readOnly ? refuse2 : null;
14548
14576
  return /* @__PURE__ */ jsx24(TimelineReadOnlyContext.Provider, { value: readOnlyPress, children: /* @__PURE__ */ jsx24(TimelineEditProvider, { value: readOnly ? NO_EDITS : editContext ?? props, children: /* @__PURE__ */ jsx24(TimelineProviderState, { ...props, children }) }) });
14549
14577
  }
14550
14578
  function TimelineProviderState({ children, ...props }) {
@@ -15591,6 +15619,15 @@ var ThumbnailScheduler = class {
15591
15619
  entry2.controller = controller;
15592
15620
  this.activeByBucket[bucket]++;
15593
15621
  this.activeByKind[entry2.request.kind]++;
15622
+ let slotFree = false;
15623
+ const freeSlot = () => {
15624
+ if (slotFree) return;
15625
+ slotFree = true;
15626
+ this.activeByBucket[bucket]--;
15627
+ this.activeByKind[entry2.request.kind]--;
15628
+ this.pump();
15629
+ };
15630
+ controller.signal.addEventListener("abort", freeSlot, { once: true });
15594
15631
  this.notify(entry2);
15595
15632
  const pending2 = this.loadWithTimeout(entry2, controller);
15596
15633
  void pending2.then((result) => this.acceptResult(entry2, controller, result)).catch((reason) => {
@@ -15608,8 +15645,7 @@ var ThumbnailScheduler = class {
15608
15645
  this.evictFailures();
15609
15646
  }).finally(() => {
15610
15647
  if (entry2.controller === controller) entry2.controller = null;
15611
- this.activeByBucket[bucket]--;
15612
- this.activeByKind[entry2.request.kind]--;
15648
+ freeSlot();
15613
15649
  this.pump();
15614
15650
  });
15615
15651
  }
@@ -15906,7 +15942,7 @@ async function loadWaveform(audioUrl, waveformUrl, signal) {
15906
15942
  return waveformUrl ? await fetchWaveformPeaks(waveformUrl, signal) : await decodeWaveformPeaks(audioUrl, signal);
15907
15943
  }
15908
15944
  async function fetchWaveformPeaks(url, signal) {
15909
- const response = await fetch(url, { signal });
15945
+ const response = await studioApiFetch(url, { signal });
15910
15946
  if (!response.ok) throw new Error(`Waveform request failed (${response.status})`);
15911
15947
  const data = await response.json();
15912
15948
  if (typeof data !== "object" || data === null || !("peaks" in data) || !Array.isArray(data.peaks) || !data.peaks.every((peak) => typeof peak === "number")) {
@@ -15915,7 +15951,7 @@ async function fetchWaveformPeaks(url, signal) {
15915
15951
  return data.peaks;
15916
15952
  }
15917
15953
  async function decodeWaveformPeaks(url, signal) {
15918
- const response = await fetch(url, { signal });
15954
+ const response = await studioApiFetch(url, { signal });
15919
15955
  if (!response.ok) throw new Error(`Audio request failed (${response.status})`);
15920
15956
  const buffer = await response.arrayBuffer();
15921
15957
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
@@ -16722,7 +16758,9 @@ function serializeStudioFileMutations(writer, targetPaths, task) {
16722
16758
 
16723
16759
  // src/utils/studioFileHistory.ts
16724
16760
  async function readProjectFileContent(pid, path) {
16725
- const response = await fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent(path)}`));
16761
+ const response = await studioApiFetch(
16762
+ buildProjectApiPath(pid, `/files/${encodeURIComponent(path)}`)
16763
+ );
16726
16764
  if (!response.ok) {
16727
16765
  throw await createStudioSaveHttpError(response, `Failed to read ${path}`);
16728
16766
  }
@@ -16903,6 +16941,28 @@ function applyAuthoredInlineOpacity(style, authored) {
16903
16941
  else style.setProperty("opacity", authored);
16904
16942
  }
16905
16943
 
16944
+ // src/utils/elementGsap.ts
16945
+ function gsapOf(element) {
16946
+ return element.ownerDocument.defaultView?.gsap;
16947
+ }
16948
+ function setElementGsapPosition(element, x, y) {
16949
+ const gsap3 = gsapOf(element);
16950
+ if (!gsap3?.set) return false;
16951
+ gsap3.set(element, { x, y });
16952
+ return true;
16953
+ }
16954
+ function setElementGsapScale(element, x, y) {
16955
+ const gsap3 = gsapOf(element);
16956
+ if (!gsap3?.set) return false;
16957
+ gsap3.set(element, { scaleX: x, scaleY: y });
16958
+ return true;
16959
+ }
16960
+ function elementTargets(tween) {
16961
+ return (tween.targets?.() ?? []).filter(
16962
+ (t2) => Boolean(t2?.style && t2.nodeType)
16963
+ );
16964
+ }
16965
+
16906
16966
  // src/utils/softReloadTargets.ts
16907
16967
  var NESTED_VARS = /* @__PURE__ */ new Set(["css", "startAt", "keyframes"]);
16908
16968
  function tweenedProps(vars) {
@@ -16923,14 +16983,14 @@ function authoringFile(el) {
16923
16983
  }
16924
16984
  function addTweenTargets(tween, targets) {
16925
16985
  const props = tweenedProps(tween.vars);
16926
- for (const el of tween.targets?.() ?? []) {
16986
+ for (const el of elementTargets(tween)) {
16927
16987
  const seen = targets.get(el) ?? /* @__PURE__ */ new Set();
16928
16988
  for (const prop of props) seen.add(prop);
16929
16989
  targets.set(el, seen);
16930
16990
  }
16931
16991
  }
16932
- function collectResetTargets(win, doc, targetKeys, reparse = []) {
16933
- const targets = new Map(reparse.map((el) => [el, /* @__PURE__ */ new Set()]));
16992
+ function collectResetTargets(win, doc, targetKeys) {
16993
+ const targets = /* @__PURE__ */ new Map();
16934
16994
  const timelines = win.__timelines ?? {};
16935
16995
  const others = new Set(
16936
16996
  Object.entries(timelines).flatMap(([key2, tl]) => targetKeys.includes(key2) ? [] : [tl])
@@ -16949,7 +17009,7 @@ function tweenTargetsIn(timelines) {
16949
17009
  for (const tl of timelines) {
16950
17010
  try {
16951
17011
  for (const tween of tl?.getChildren?.(true) ?? []) {
16952
- for (const el of tween.targets?.() ?? []) els.add(el);
17012
+ for (const el of elementTargets(tween)) els.add(el);
16953
17013
  }
16954
17014
  } catch {
16955
17015
  }
@@ -17063,7 +17123,8 @@ function runSuppressed(win, reload) {
17063
17123
  if (win.__hfSuppressSceneMutations) win.__hfSuppressSceneMutations(reload);
17064
17124
  else reload();
17065
17125
  return true;
17066
- } catch {
17126
+ } catch (error) {
17127
+ console.error("[Studio] GSAP soft reload threw; falling back to a full reload", error);
17067
17128
  return false;
17068
17129
  }
17069
17130
  }
@@ -17090,7 +17151,7 @@ function timelineKeys(scriptText) {
17090
17151
  return [...scriptText.matchAll(/__timelines\s*\[\s*["'`]([^"'`]+)["'`]\s*\]/g)].map((m) => m[1]).filter((key2) => key2 !== "__proxied");
17091
17152
  }
17092
17153
  function applySoftReload(iframe, scriptText, options = {}) {
17093
- const { onAsyncFailure, currentTimeOverride, authoredHtml, reparse = [] } = options;
17154
+ const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
17094
17155
  if (!iframe || !scriptText) return "cannot-soft-reload";
17095
17156
  const win = iframe.contentWindow;
17096
17157
  const doc = iframe.contentDocument;
@@ -17120,7 +17181,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
17120
17181
  const readAuthoredOpacity = (el) => findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
17121
17182
  const doReload = () => {
17122
17183
  const timelines = win.__timelines;
17123
- const targets = collectResetTargets(win, doc, targetKeys, reparse);
17184
+ const targets = collectResetTargets(win, doc, targetKeys);
17124
17185
  if (timelines) {
17125
17186
  for (const key2 of targetKeys) {
17126
17187
  const tl = timelines[key2];
@@ -17135,15 +17196,10 @@ function applySoftReload(iframe, scriptText, options = {}) {
17135
17196
  }
17136
17197
  const allTargets = [...targets.keys()];
17137
17198
  if (allTargets.length > 0 && win.gsap?.set) {
17138
- const saved2 = [];
17139
- for (const el of allTargets) {
17140
- const styled = el;
17141
- if (styled.style?.cssText != null) saved2.push([styled, styled.style.cssText]);
17142
- }
17143
- try {
17144
- win.gsap.set(allTargets, { clearProps: "all" });
17145
- } catch {
17146
- }
17199
+ const saved2 = allTargets.map(
17200
+ (el) => [el, el.style.cssText]
17201
+ );
17202
+ win.gsap.set(allTargets, { clearProps: "all" });
17147
17203
  for (const [el, css2] of saved2) {
17148
17204
  const s = el.style;
17149
17205
  s.cssText = css2;
@@ -17289,7 +17345,7 @@ var GsapPreviewConvergenceError = class extends Error {
17289
17345
  var GsapOwnershipProtocolError = class extends GsapPreviewConvergenceError {
17290
17346
  };
17291
17347
  async function requireGsapOwnershipProtocol(projectId) {
17292
- const response = await fetch(
17348
+ const response = await studioApiFetch(
17293
17349
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-capabilities`
17294
17350
  );
17295
17351
  if (!response.ok) {
@@ -17300,18 +17356,18 @@ async function requireGsapOwnershipProtocol(projectId) {
17300
17356
  throw new GsapOwnershipProtocolError("Invalid GSAP mutation capability response");
17301
17357
  }
17302
17358
  }
17303
- async function rollbackOwnedMutation(projectId, targetPath, expected, restore) {
17359
+ async function rollbackOwnedMutation(projectId, targetPath, expected, restore2) {
17304
17360
  if (targetPath.includes("\0") || targetPath.includes("..")) {
17305
17361
  throw new Error(`Unsafe path: ${targetPath}`);
17306
17362
  }
17307
- const response = await fetch(
17363
+ const response = await studioApiFetch(
17308
17364
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-rollback/${encodeURIComponent(targetPath)}`,
17309
17365
  {
17310
17366
  method: "POST",
17311
17367
  // Deliberately unclaimed: a rollback runs because a mutation did not
17312
17368
  // converge, so let the restored file reload the preview.
17313
17369
  headers: { "Content-Type": "application/json" },
17314
- body: JSON.stringify({ expected, restore })
17370
+ body: JSON.stringify({ expected, restore: restore2 })
17315
17371
  }
17316
17372
  );
17317
17373
  if (!response.ok) {
@@ -17343,7 +17399,7 @@ function readMutationError(value, fallback) {
17343
17399
  async function postGsapMutation(projectId, filePath, mutation, fallback) {
17344
17400
  let response;
17345
17401
  try {
17346
- response = await fetch(
17402
+ response = await studioApiFetch(
17347
17403
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(filePath)}`,
17348
17404
  {
17349
17405
  method: "POST",
@@ -17370,7 +17426,7 @@ async function readFileContent(projectId, targetPath) {
17370
17426
  if (targetPath.includes("\0") || targetPath.includes("..")) {
17371
17427
  throw new Error(`Unsafe path: ${targetPath}`);
17372
17428
  }
17373
- const response = await fetch(
17429
+ const response = await studioApiFetch(
17374
17430
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(targetPath)}`
17375
17431
  );
17376
17432
  if (!response.ok) {
@@ -20480,6 +20536,10 @@ function normalizeDomEditStyleValue(property, value) {
20480
20536
  function isImageBackgroundValue(value) {
20481
20537
  return /^url\(/i.test(value.trim());
20482
20538
  }
20539
+ function cssPropertyName(property) {
20540
+ if (property.startsWith("--")) return property;
20541
+ return property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^ms-/, "-ms-");
20542
+ }
20483
20543
  function isManualGeometryStyleProperty(property) {
20484
20544
  return property === "left" || property === "top" || property === "width" || property === "height";
20485
20545
  }
@@ -20626,7 +20686,7 @@ function isPlainRecord(value) {
20626
20686
  }
20627
20687
  async function resolveAssetHasAudio(projectId, assetPath) {
20628
20688
  try {
20629
- const response = await fetch(mediaMetadataUrl(projectId, assetPath));
20689
+ const response = await studioApiFetch(mediaMetadataUrl(projectId, assetPath));
20630
20690
  if (!response.ok) return null;
20631
20691
  const data = await response.json();
20632
20692
  const metadata = isPlainRecord(data) ? data.metadata : void 0;
@@ -22356,7 +22416,7 @@ function TimelineTrackRow({
22356
22416
  ),
22357
22417
  propertyRows.map((row) => {
22358
22418
  const group = row.propertyGroup;
22359
- const keyframeCount = row.items.filter((item) => item.kind === "keyframe").length;
22419
+ const keyframeCount2 = row.items.filter((item) => item.kind === "keyframe").length;
22360
22420
  const easeCount = row.items.filter((item) => item.kind === "ease").length;
22361
22421
  return (
22362
22422
  // ponytail: aria-owns maps this hidden logical row onto the two visible
@@ -22387,7 +22447,7 @@ function TimelineTrackRow({
22387
22447
  "aria-colindex": 2,
22388
22448
  "aria-owns": timelineLogicalRowCellId(lanesId, row.id, "content"),
22389
22449
  children: [
22390
- keyframeCount,
22450
+ keyframeCount2,
22391
22451
  " keyframes, ",
22392
22452
  easeCount,
22393
22453
  " ease controls"
@@ -24125,7 +24185,7 @@ var peakMapRequests = /* @__PURE__ */ new Map();
24125
24185
  function loadPeakMap(url) {
24126
24186
  const pending2 = peakMapRequests.get(url);
24127
24187
  if (pending2) return pending2;
24128
- const request = fetch(url).then((res) => res.ok ? res.json() : null).then((body) => isPeakMap(body) ? body : null).catch(() => null);
24188
+ const request = studioApiFetch(url).then((res) => res.ok ? res.json() : null).then((body) => isPeakMap(body) ? body : null).catch(() => null);
24129
24189
  peakMapRequests.set(url, request);
24130
24190
  return request;
24131
24191
  }
@@ -24200,7 +24260,7 @@ function isNormalizePlan(value) {
24200
24260
  (key2) => typeof Reflect.get(value, key2) === "number"
24201
24261
  );
24202
24262
  }
24203
- async function requestNormalizePlan(projectId, el, fetchImpl = fetch) {
24263
+ async function requestNormalizePlan(projectId, el, fetchImpl = studioApiFetch) {
24204
24264
  const origin2 = window.location.origin;
24205
24265
  const relative = resolvePreviewRelative(
24206
24266
  resolveMediaPreviewUrl(el.src ?? "", projectId, origin2),
@@ -25257,7 +25317,7 @@ async function measureCarve(doc, voices, strength, bed) {
25257
25317
  const Ctor = window.OfflineAudioContext ?? window.webkitOfflineAudioContext;
25258
25318
  if (!Ctor) return null;
25259
25319
  const decode = async (relative, clock) => {
25260
- const res = await fetch(new URL(relative, doc.baseURI).href);
25320
+ const res = await studioApiFetch(new URL(relative, doc.baseURI).href);
25261
25321
  const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE).decodeAudioData(
25262
25322
  await res.arrayBuffer()
25263
25323
  );
@@ -27147,7 +27207,7 @@ function compositionThumbnailRequest(url, projectId, { sessionEpoch = 0, priorit
27147
27207
  };
27148
27208
  }
27149
27209
  async function loadCompositionImage(url, signal) {
27150
- const response = await fetch(url, { signal });
27210
+ const response = await studioApiFetch(url, { signal });
27151
27211
  if (!response.ok) throw new Error(`Composition thumbnail failed (${response.status})`);
27152
27212
  const blob = await response.blob();
27153
27213
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
@@ -29369,6 +29429,14 @@ var BOX_CHANNELS = [
29369
29429
  function gsapWritesBox(el) {
29370
29430
  return gsapWritesPosition(el) || gsapWritesChannels(el, BOX_CHANNELS);
29371
29431
  }
29432
+ var GSAP_OWNS = {
29433
+ move: gsapWritesPosition,
29434
+ resize: gsapWritesBox,
29435
+ rotate: gsapWritesRotation
29436
+ };
29437
+ function editsPlainCss(el, gesture) {
29438
+ return !GSAP_OWNS[gesture](el);
29439
+ }
29372
29440
  function elementHasNonHoldTween(el, channels) {
29373
29441
  const win = el.ownerDocument.defaultView;
29374
29442
  return hasNonHoldTween(win?.__timelines ?? null, el, channels);
@@ -29921,7 +29989,7 @@ function sceneSwapFor(iframe) {
29921
29989
  );
29922
29990
  cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
29923
29991
  const swapping = (async () => {
29924
- const response = await fetch(url, { signal: deadline.signal });
29992
+ const response = await studioApiFetch(url, { signal: deadline.signal });
29925
29993
  if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
29926
29994
  const html2 = await response.text();
29927
29995
  if (!isCurrent()) throw new Error("superseded by a newer reload");
@@ -31048,7 +31116,9 @@ function useTimelinePlayer({
31048
31116
  const slot = previewGeneration();
31049
31117
  const isCurrent = () => gen === refreshGenRef.current && slot === previewGeneration();
31050
31118
  const swap = sceneSwapFor(iframe);
31051
- if (!swap || isRefreshingRef.current) return reloadWholeFilm(url.toString());
31119
+ const swapWouldReplaceGestureNode = !!iframe.contentDocument && isStudioManualEditGestureLiveIn(iframe.contentDocument);
31120
+ if (!swap || isRefreshingRef.current || swapWouldReplaceGestureNode)
31121
+ return reloadWholeFilm(url.toString());
31052
31122
  swap(url.toString(), isCurrent, cancel.signal).catch((error) => {
31053
31123
  if (!isCurrent()) return;
31054
31124
  logReload("scene-swap-refused", { reason: String(error) });
@@ -31363,7 +31433,7 @@ function NLEProvider({
31363
31433
  setCompIdToSrc(emptyMap);
31364
31434
  setCompositionSourceMap(emptyMap);
31365
31435
  onCompIdToSrcChangeRef.current?.(emptyMap);
31366
- fetch(buildProjectApiPath(projectId, `/files/index.html`), {
31436
+ studioApiFetch(buildProjectApiPath(projectId, `/files/index.html`), {
31367
31437
  signal: controller.signal
31368
31438
  }).then((r) => {
31369
31439
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
@@ -31899,7 +31969,7 @@ function renderPosterForNextOpen(projectId) {
31899
31969
  priority: "overscan",
31900
31970
  discardWhenReleased: true,
31901
31971
  load: async (signal) => {
31902
- await (await fetch(url, { signal })).arrayBuffer();
31972
+ await (await studioApiFetch(url, { signal })).arrayBuffer();
31903
31973
  return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
31904
31974
  }
31905
31975
  };
@@ -35897,7 +35967,7 @@ function OffCanvasIndicators({
35897
35967
  const selectOffCanvas = async () => {
35898
35968
  const el = elements.current.get(r.key);
35899
35969
  if (!el) return;
35900
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-D7VYFJJD.js");
35970
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-OL7GLSS4.js");
35901
35971
  const acp = activeCompositionPathRef.current ?? "index.html";
35902
35972
  const sel = await resolveDomEditSelection2(el, {
35903
35973
  activeCompositionPath: acp,
@@ -36563,6 +36633,13 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
36563
36633
  baseGsapY
36564
36634
  };
36565
36635
  }
36636
+ function restoreDragOffset(element) {
36637
+ const origin2 = (axis) => Number.parseFloat(element.getAttribute(`data-hf-drag-initial-offset-${axis}`) ?? "") || 0;
36638
+ element.style.setProperty("--hf-studio-offset-x", `${origin2("x")}px`);
36639
+ element.style.setProperty("--hf-studio-offset-y", `${origin2("y")}px`);
36640
+ element.removeAttribute("data-hf-drag-initial-offset-x");
36641
+ element.removeAttribute("data-hf-drag-initial-offset-y");
36642
+ }
36566
36643
 
36567
36644
  // src/components/editor/plainTranslate.ts
36568
36645
  var TOKEN = /\s*(?:(\d*\.?\d+(?:e[-+]?\d+)?)(px|%)?|([a-z]+)\(|([-+*/(),]))/giy;
@@ -36665,11 +36742,11 @@ function writeTranslatePx(el, p) {
36665
36742
  }
36666
36743
  el.style.setProperty("translate", formatTranslatePx(p));
36667
36744
  }
36668
- function readMoveOffset(el, plainTranslate = !gsapWritesPosition(el)) {
36745
+ function readMoveOffset(el, plainTranslate = editsPlainCss(el, "move")) {
36669
36746
  return plainTranslate ? readTranslatePx(el) : readStudioPathOffset(el);
36670
36747
  }
36671
36748
  function readShownRotation(el) {
36672
- return gsapWritesRotation(el) ? readStudioRotation(el) : { angle: roundTo3(readCssRotation(el)) };
36749
+ return editsPlainCss(el, "rotate") ? { angle: roundTo3(readCssRotation(el)) } : readStudioRotation(el);
36673
36750
  }
36674
36751
 
36675
36752
  // src/components/editor/manualOffsetDrag.ts
@@ -36723,11 +36800,11 @@ function getRectCenter(element) {
36723
36800
  if (!finiteRect(rect) || rect.width <= 0 && rect.height <= 0) {
36724
36801
  return null;
36725
36802
  }
36726
- let point = {
36727
- x: rect.left + rect.width / 2,
36728
- y: rect.top + rect.height / 2
36729
- };
36730
- let win = element.ownerDocument.defaultView;
36803
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
36804
+ return toTopScreenPoint(element.ownerDocument, point);
36805
+ }
36806
+ function toTopScreenPoint(doc, point) {
36807
+ let win = doc.defaultView;
36731
36808
  while (win) {
36732
36809
  const frameElement = getFrameElement(win);
36733
36810
  if (!frameElement) break;
@@ -36833,8 +36910,7 @@ function resolveManualOffsetForPointerDelta(input) {
36833
36910
  };
36834
36911
  }
36835
36912
  function createManualOffsetDragMember(input) {
36836
- const gsapOwns = input.gesture === "resize" ? gsapWritesBox : gsapWritesPosition;
36837
- const plainTranslate = !!input.gesture && !gsapOwns(input.element);
36913
+ const plainTranslate = !!input.gesture && editsPlainCss(input.element, input.gesture === "resize" ? "resize" : "move");
36838
36914
  const initialOffset = plainTranslate ? readTranslatePx(input.element) : readAppliedStudioPathOffset(input.element);
36839
36915
  if (!finitePoint(initialOffset))
36840
36916
  return { ok: false, reason: UNREADABLE_TRANSLATE, selection: input.selection };
@@ -37835,7 +37911,7 @@ function startGesture(kind, e, opts, options) {
37835
37911
  }
37836
37912
  }
37837
37913
  try {
37838
- const plain = !!pathOffsetMember?.plainTranslate || !gsapWritesRotation(sel.element);
37914
+ const plain = !!pathOffsetMember?.plainTranslate || editsPlainCss(sel.element, "rotate");
37839
37915
  const plainRotation = plain && kind === "rotate" ? readCssRotationTarget(sel.element) : null;
37840
37916
  const rotation = { angle: readRotationBase(sel.element, plain) };
37841
37917
  const overlayBounds = overlayEl?.getBoundingClientRect();
@@ -38368,7 +38444,7 @@ function createDomEditOverlayGestureHandlers(opts) {
38368
38444
  return;
38369
38445
  }
38370
38446
  applyRotationDraft(sel.element, finalRotation.angle, g.plainRotation);
38371
- const commit = g.plainRotation ? { ...finalRotation, plain: g.plainRotation } : finalRotation;
38447
+ const commit = { ...finalRotation, plain: g.plainRotation };
38372
38448
  void Promise.resolve(opts.onRotationCommitRef.current(sel, commit)).then((result) => trackPreviewEditResult("rotate", "drag", result)).catch((error) => {
38373
38449
  logGestureCommitFailure("rotate commit failed", error);
38374
38450
  if (g.manualEditDragToken && isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
@@ -38427,14 +38503,14 @@ function createDomEditOverlayGestureHandlers(opts) {
38427
38503
  hasMember: !!member,
38428
38504
  inlineStyle: sel.element.getAttribute("style")
38429
38505
  });
38430
- const restore = () => {
38506
+ const restore2 = () => {
38431
38507
  if (!g.manualEditDragToken || !isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
38432
38508
  return;
38433
38509
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
38434
38510
  if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
38435
38511
  };
38436
38512
  void Promise.resolve(
38437
- opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore)
38513
+ opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2, member)
38438
38514
  ).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38439
38515
  logGestureCommitFailure("resize commit failed", error);
38440
38516
  }).finally(() => {
@@ -41580,77 +41656,6 @@ function isDomEditPersistOutcome(value) {
41580
41656
  return typeof Reflect.get(value, "sourceFile") === "string" && typeof Reflect.get(value, "version") === "string" && typeof Reflect.get(value, "changed") === "boolean";
41581
41657
  }
41582
41658
 
41583
- // src/components/editor/manualEditingAvailability.ts
41584
- var manualEditingAvailability_exports = {};
41585
- __export(manualEditingAvailability_exports, {
41586
- STUDIO_FLAT_INSPECTOR_ENABLED: () => STUDIO_FLAT_INSPECTOR_ENABLED,
41587
- STUDIO_SDK_CUTOVER_ENABLED: () => STUDIO_SDK_CUTOVER_ENABLED,
41588
- STUDIO_SDK_CUTOVER_FAMILIES: () => STUDIO_SDK_CUTOVER_FAMILIES,
41589
- STUDIO_SDK_RESOLVER_SHADOW_ENABLED: () => STUDIO_SDK_RESOLVER_SHADOW_ENABLED,
41590
- resolveStudioBooleanEnvFlag: () => resolveStudioBooleanEnvFlag
41591
- });
41592
-
41593
- // src/utils/sdkCutoverPolicy.ts
41594
- var STUDIO_SDK_OPERATION_FAMILIES = [
41595
- "dom",
41596
- "timing",
41597
- "gsap-animation",
41598
- "gsap-keyframe",
41599
- "lifecycle"
41600
- ];
41601
- function resolveEnabledSdkFamilies(env2, masterEnabled) {
41602
- if (!masterEnabled) return /* @__PURE__ */ new Set();
41603
- const raw = env2["VITE_STUDIO_SDK_CUTOVER_FAMILIES"];
41604
- if (typeof raw !== "string" || raw.trim() === "") return new Set(STUDIO_SDK_OPERATION_FAMILIES);
41605
- const requested = raw.split(",").map((family) => family.trim()).filter(Boolean);
41606
- const known = new Set(STUDIO_SDK_OPERATION_FAMILIES);
41607
- const unknown = requested.filter((family) => !known.has(family));
41608
- if (unknown.length > 0) {
41609
- throw new Error(`Unknown Studio SDK cutover families: ${unknown.join(", ")}`);
41610
- }
41611
- return new Set(requested);
41612
- }
41613
- function isSdkFamilyEnabled(masterEnabled, configured, family) {
41614
- return masterEnabled && (configured?.has(family) ?? true);
41615
- }
41616
-
41617
- // src/components/editor/manualEditingAvailability.ts
41618
- var TRUTHY_ENV_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on", "enabled"]);
41619
- var FALSY_ENV_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off", "disabled"]);
41620
- function resolveStudioBooleanEnvFlag(env2, names, fallback) {
41621
- for (const name of names) {
41622
- const value = env2[name];
41623
- if (typeof value === "boolean") return value;
41624
- if (typeof value !== "string") continue;
41625
- const normalized = value.trim().toLowerCase();
41626
- if (!normalized) continue;
41627
- if (TRUTHY_ENV_VALUES.has(normalized)) return true;
41628
- if (FALSY_ENV_VALUES.has(normalized)) return false;
41629
- }
41630
- return fallback;
41631
- }
41632
- var runtimeEnv = typeof window !== "undefined" ? window.__HF_STUDIO_ENV__ ?? {} : {};
41633
- var env = { ...import.meta.env ?? {}, ...runtimeEnv };
41634
- var STUDIO_SDK_CUTOVER_ENABLED = resolveStudioBooleanEnvFlag(
41635
- env,
41636
- ["VITE_STUDIO_SDK_CUTOVER_ENABLED"],
41637
- true
41638
- );
41639
- var STUDIO_SDK_CUTOVER_FAMILIES = resolveEnabledSdkFamilies(
41640
- env,
41641
- STUDIO_SDK_CUTOVER_ENABLED
41642
- );
41643
- var STUDIO_SDK_RESOLVER_SHADOW_ENABLED = resolveStudioBooleanEnvFlag(
41644
- env,
41645
- ["VITE_STUDIO_SDK_RESOLVER_SHADOW_ENABLED"],
41646
- true
41647
- );
41648
- var STUDIO_FLAT_INSPECTOR_ENABLED = resolveStudioBooleanEnvFlag(
41649
- env,
41650
- ["VITE_STUDIO_ENABLE_FLAT_INSPECTOR", "VITE_STUDIO_FLAT_INSPECTOR_ENABLED"],
41651
- true
41652
- );
41653
-
41654
41659
  // src/utils/sdkOpMapping.ts
41655
41660
  function patchOpsToSdkEditOps(hfId, ops) {
41656
41661
  const result = [];
@@ -42772,7 +42777,7 @@ function useElementLifecycleOps({
42772
42777
  writeFile: writeProjectFile,
42773
42778
  recordEdit: editHistory.recordEdit,
42774
42779
  rewrite: async (originalContent) => {
42775
- const removeResponse = await fetch(
42780
+ const removeResponse = await studioApiFetch(
42776
42781
  buildProjectApiPath(
42777
42782
  pid,
42778
42783
  `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`
@@ -44716,7 +44721,9 @@ import { editabilityForProvenance } from "@hyperframes/core/gsap-parser";
44716
44721
  var GSAP_EDIT_BLOCK_COPY = {
44717
44722
  "no-selector": "This layer needs a stable selector before Studio can save the edit.",
44718
44723
  "unroll-required": "This motion comes from a helper or loop. Choose Unroll to edit it explicitly.",
44719
- "source-uneditable": "This animation is computed at runtime. Edit the animation in the Code tab."
44724
+ "source-uneditable": "This animation is computed at runtime. Edit the animation in the Code tab.",
44725
+ "keyframes-uneditable": "Studio can't add this edit to the animation's keyframes. Edit this animation in the Code tab.",
44726
+ "mixed-files": "These layers are animated in different files. Move each file's layers separately."
44720
44727
  };
44721
44728
  var GsapEditBlockedError = class extends Error {
44722
44729
  constructor(reason, detail) {
@@ -44943,62 +44950,6 @@ async function commitStaticGsapSize(selection, size, selector, existingSet, call
44943
44950
  { label: "Resize layer", softReload: true }
44944
44951
  );
44945
44952
  }
44946
- function positiveOr(n, fallback) {
44947
- return Number.isFinite(n) && n > 0 ? roundToLayoutPx(n) : fallback;
44948
- }
44949
- function resolvePriorSize(sizeSet, el, fallbackW, fallbackH) {
44950
- if (sizeSet) {
44951
- return {
44952
- width: positiveOr(Number(sizeSet.properties.width), fallbackW),
44953
- height: positiveOr(Number(sizeSet.properties.height), fallbackH)
44954
- };
44955
- }
44956
- const ow = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_WIDTH_ATTR) ?? "");
44957
- const oh = Number.parseFloat(el?.getAttribute(STUDIO_ORIGINAL_HEIGHT_ATTR) ?? "");
44958
- return { width: positiveOr(ow, fallbackW), height: positiveOr(oh, fallbackH) };
44959
- }
44960
- async function commitKeyframedSizeFromResize(selection, size, selector, sizeSet, animatedTween, callbacks) {
44961
- const ts = resolveTweenStart(animatedTween) ?? 0;
44962
- const td = resolveTweenDuration(animatedTween);
44963
- if (!(td > 0)) return false;
44964
- const newW = roundToLayoutPx(size.width);
44965
- const newH = roundToLayoutPx(size.height);
44966
- const prior = resolvePriorSize(sizeSet, selection.element, newW, newH);
44967
- const ct = usePlayerStore.getState().currentTime;
44968
- const pct = Math.max(0, Math.min(100, Math.round((ct - ts) / td * 1e3) / 10));
44969
- const pcts = new Set(
44970
- animatedTween.keyframes?.keyframes.map((k) => k.percentage) ?? [0, 100]
44971
- );
44972
- pcts.add(0);
44973
- pcts.add(100);
44974
- pcts.add(pct);
44975
- const keyframes = Array.from(pcts).sort((a, b) => a - b).map((p) => ({
44976
- percentage: p,
44977
- properties: Math.abs(p - pct) < 0.05 ? { width: newW, height: newH } : { ...prior }
44978
- }));
44979
- const addLabel = `Resize (size keyframe ${pct.toFixed(0)}%)`;
44980
- const target = newTweenTarget(selection);
44981
- if (!target) return false;
44982
- await callbacks.commitMutation(
44983
- selection,
44984
- {
44985
- type: "add-with-keyframes",
44986
- targetSelector: target,
44987
- position: roundTo3(ts),
44988
- duration: roundTo3(td),
44989
- keyframes
44990
- },
44991
- sizeSet ? { label: addLabel, skipReload: true } : { label: addLabel, softReload: true }
44992
- );
44993
- if (sizeSet) {
44994
- await callbacks.commitMutation(
44995
- selection,
44996
- { type: "delete", animationId: sizeSet.id },
44997
- { label: "Resize layer", softReload: true }
44998
- );
44999
- }
45000
- return true;
45001
- }
45002
44953
  async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, iframe, selector, callbacks) {
45003
44954
  const el = selection.element;
45004
44955
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
@@ -45023,7 +44974,6 @@ async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, ifr
45023
44974
  }
45024
44975
  const ts = resolveTweenStart(effectiveAnim);
45025
44976
  const td = resolveTweenDuration(effectiveAnim);
45026
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
45027
44977
  let kfs = effectiveAnim.keyframes?.keyframes ?? [];
45028
44978
  if (kfs.length === 0) {
45029
44979
  const fromProps = effectiveAnim.fromProperties ?? {};
@@ -45055,7 +45005,7 @@ async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, ifr
45055
45005
  position: roundTo3(ts ?? 0),
45056
45006
  duration: roundTo3(td || 1),
45057
45007
  keyframes: shifted,
45058
- ease
45008
+ ...keyframeEases(effectiveAnim)
45059
45009
  },
45060
45010
  { label: "Move animation path", softReload: true, beforeReload: restoreOffset }
45061
45011
  );
@@ -45070,7 +45020,6 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
45070
45020
  }
45071
45021
  const ts = resolveTweenStart(effectiveAnim);
45072
45022
  const td = resolveTweenDuration(effectiveAnim);
45073
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
45074
45023
  const keys = Object.keys(newValues);
45075
45024
  const at = (props, key2) => typeof props[key2] === "number" ? props[key2] : PROPERTY_DEFAULTS[key2] ?? 0;
45076
45025
  const kfs = effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
@@ -45103,7 +45052,7 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
45103
45052
  position: roundTo3(ts ?? 0),
45104
45053
  duration: roundTo3(td || 1),
45105
45054
  keyframes: shifted,
45106
- ease
45055
+ ...keyframeEases(effectiveAnim)
45107
45056
  },
45108
45057
  { label, softReload: true }
45109
45058
  );
@@ -46261,7 +46210,7 @@ import { useCallback as useCallback68, useMemo as useMemo45 } from "react";
46261
46210
  import { useEffect as useEffect62, useMemo as useMemo44, useRef as useRef78, useState as useState55, useCallback as useCallback67 } from "react";
46262
46211
 
46263
46212
  // src/hooks/gsapRuntimeBridge.ts
46264
- import { isXYPositionWrite } from "@hyperframes/parsers/gsap-constants";
46213
+ import { isXYPositionWrite, PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
46265
46214
 
46266
46215
  // src/hooks/gsapRuntimeReaders.ts
46267
46216
  import { classifyPropertyGroup as classifyPropertyGroup3 } from "@hyperframes/core/gsap-parser";
@@ -46367,183 +46316,382 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
46367
46316
  return result;
46368
46317
  }
46369
46318
 
46370
- // src/hooks/gsapDragPositionCommit.ts
46371
- import { PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
46372
- function buildTemporalArcKeyframes(anim, percentage, properties) {
46373
- return [
46374
- ...(anim.keyframes?.keyframes ?? []).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
46375
- percentage: keyframe.percentage,
46376
- properties: { ...keyframe.properties },
46377
- ...keyframe.ease ? { ease: keyframe.ease } : {}
46378
- })),
46379
- { percentage, properties }
46380
- ].sort((a, b) => a.percentage - b.percentage);
46319
+ // src/hooks/gsapParsedTween.ts
46320
+ var PARSED_NAME = { scale: "scaleX", rotate: "rotation" };
46321
+ function findPropTween(pt, prop) {
46322
+ for (let node = pt; node; node = node._next) {
46323
+ if (node.p === prop && typeof node.s === "number" && typeof node.c === "number") return node;
46324
+ const nested = findPropTween(node.d?._pt, prop);
46325
+ if (nested) return nested;
46326
+ }
46327
+ return null;
46381
46328
  }
46382
- async function extendTweenAndAddKeyframe(selection, anim, properties, targetTime, tweenStart, tweenDuration, callbacks, beforeReload, backfillDefaults) {
46383
- const tweenEnd = tweenStart + tweenDuration;
46384
- const newStart = Math.min(targetTime, tweenStart);
46385
- const newEnd = Math.max(targetTime, tweenEnd);
46386
- const newDuration = Math.max(0.01, newEnd - newStart);
46387
- const existingKfs = anim.keyframes?.keyframes ?? [];
46388
- const remappedKfs = [];
46389
- for (const kf of existingKfs) {
46390
- const absTime = tweenStart + kf.percentage / 100 * tweenDuration;
46391
- const newPct = Math.round((absTime - newStart) / newDuration * 1e3) / 10;
46392
- const props = { ...kf.properties };
46393
- for (const k of Object.keys(properties)) {
46394
- if (!(k in props) && backfillDefaults?.[k] != null) props[k] = backfillDefaults[k];
46395
- }
46396
- remappedKfs.push({ percentage: newPct, properties: props });
46397
- }
46398
- const targetPct = Math.round((targetTime - newStart) / newDuration * 1e3) / 10;
46399
- remappedKfs.push({ percentage: targetPct, properties });
46400
- remappedKfs.sort((a, b) => a.percentage - b.percentage);
46401
- await callbacks.commitMutation(
46402
- selection,
46403
- {
46404
- type: "replace-with-keyframes",
46405
- animationId: anim.id,
46406
- targetSelector: anim.targetSelector,
46407
- position: roundTo3(newStart),
46408
- duration: roundTo3(newDuration),
46409
- keyframes: remappedKfs
46410
- },
46411
- {
46412
- label: `Move layer (extended keyframe)`,
46413
- keyframeAction: "add",
46414
- softReload: true,
46415
- beforeReload
46416
- }
46417
- );
46329
+ function endsIn(tween, prop) {
46330
+ const pt = findPropTween(tween._pt, PARSED_NAME[prop] ?? prop);
46331
+ if (!pt) return null;
46332
+ const pair = [pt.s, pt.s + pt.c];
46333
+ return tween._from ? [pair[1], pair[0]] : pair;
46418
46334
  }
46419
- async function commitKeyframedPosition(selection, anim, properties, callbacks, beforeReload, backfillDefaults) {
46420
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46421
- const computedPct = computeCurrentPercentage(selection, anim);
46422
- const pct = activeKeyframePct ?? computedPct;
46423
- await callbacks.commitMutation(
46424
- selection,
46425
- {
46426
- type: "add-keyframe",
46427
- animationId: anim.id,
46428
- percentage: pct,
46429
- properties,
46430
- ...backfillDefaults ? { backfillDefaults } : {}
46431
- },
46432
- { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload }
46433
- );
46434
- if (activeKeyframePct != null) {
46435
- setActiveKeyframePct(null);
46436
- parkPlayheadOnKeyframe(anim, pct);
46335
+ function findParsedTween(iframe, element, anim) {
46336
+ const win = iframe?.contentWindow;
46337
+ const start = resolveTweenStart(anim);
46338
+ if (!win?.__timelines || start == null) return null;
46339
+ const steps = anim.keyframes?.keyframes.map((kf) => kf.properties) ?? [anim.properties];
46340
+ const props = [...new Set(steps.flatMap((step) => Object.keys(step)))];
46341
+ const keyframed = Boolean(anim.keyframes);
46342
+ for (const timeline of Object.values(win.__timelines)) {
46343
+ for (const tween of timeline?.getChildren?.(true) ?? []) {
46344
+ const targets = tween.targets?.() ?? [];
46345
+ if (!targets.includes(element) && !targets.some((t2) => element.id && t2.id === element.id))
46346
+ continue;
46347
+ if (!startsAt(tween, start)) continue;
46348
+ const vars = tween.vars ?? {};
46349
+ const carries = keyframed ? "keyframes" in vars : props.some((p) => p in vars);
46350
+ if (!carries || !((tween.duration?.() ?? 0) > 0)) continue;
46351
+ parseUnplayed(win, element, tween, props);
46352
+ return tween;
46353
+ }
46437
46354
  }
46355
+ return null;
46438
46356
  }
46439
- function resolveDragRuntime(iframe, selector) {
46440
- if (!iframe || !selector) return null;
46441
- const win = iframe.contentWindow;
46442
- const gsap3 = win?.gsap;
46443
- if (typeof gsap3?.getProperty !== "function" || typeof gsap3.set !== "function") return null;
46444
- let el = null;
46357
+ function startsAt(tween, start) {
46358
+ const delay2 = Number(tween.vars?.delay) || 0;
46359
+ return Math.abs((tween.startTime?.() ?? Number.NaN) - delay2 - start) <= 1e-3;
46360
+ }
46361
+ function withLiveTiming(anim, tween) {
46362
+ const start = tween?.startTime?.();
46363
+ return {
46364
+ ...anim,
46365
+ ...Number.isFinite(start) && { resolvedStart: start },
46366
+ ...anim.duration == null && tween && { duration: tween.duration?.() }
46367
+ };
46368
+ }
46369
+ function parseUnplayed(win, element, tween, props) {
46370
+ const seek = !isParsed(tween) && seekable(win, tween);
46371
+ if (!seek) return;
46372
+ const { parent, gsap: gsap3 } = seek;
46373
+ const before = layerStates(tween, element);
46374
+ const now2 = parent.time();
46375
+ const start = tween.startTime?.() ?? 0;
46376
+ gsap3.set(element, { clearProps: props.join(",") });
46377
+ for (const p of props) gsap3.getProperty(element, p);
46445
46378
  try {
46446
- el = iframe.contentDocument?.querySelector(selector) ?? null;
46447
- } catch {
46448
- return null;
46379
+ parent.seek(start, true);
46380
+ parent.seek(start + (tween.duration?.() ?? 0), true);
46381
+ } finally {
46382
+ parent.seek(now2, true);
46383
+ restore(before);
46449
46384
  }
46450
- if (!el) return null;
46451
- const timelines = win?.__timelines;
46452
- const mainTl = timelines ? Object.values(timelines)[0] : void 0;
46453
- if (typeof mainTl?.seek !== "function") return null;
46385
+ }
46386
+ function isParsed(tween) {
46387
+ const parts = tween.timeline?.getChildren?.() ?? [];
46388
+ return Boolean(tween._initted) && parts.every((part) => part._initted);
46389
+ }
46390
+ function seekable(win, tween) {
46391
+ const { parent } = tween;
46392
+ const gsap3 = win.gsap;
46393
+ if (!parent?.seek || !parent.time || !gsap3?.getProperty || !gsap3.set) return null;
46454
46394
  return {
46455
- gsapLib: gsap3,
46456
- el,
46457
- mainTl
46458
- };
46395
+ parent: { seek: parent.seek.bind(parent), time: parent.time.bind(parent) },
46396
+ gsap: { getProperty: gsap3.getProperty.bind(gsap3), set: gsap3.set.bind(gsap3) }
46397
+ };
46398
+ }
46399
+ function layerStates(tween, element) {
46400
+ const layers = /* @__PURE__ */ new Set([element]);
46401
+ for (const child of tween.parent?.getChildren?.(true, true, false) ?? [])
46402
+ for (const target of elementTargets(child)) layers.add(target);
46403
+ return [...layers].map((layer) => ({
46404
+ layer,
46405
+ attributes: new Map([...layer.attributes].map((a) => [a.name, a.value])),
46406
+ cache: layer._gsap && { ...layer._gsap }
46407
+ }));
46459
46408
  }
46460
- async function commitFlatViaKeyframes(selection, anim, properties, callbacks, beforeReload, iframe, selector, backfillDefaults) {
46461
- const ct = usePlayerStore.getState().currentTime;
46462
- const ts = resolveTweenStart(anim);
46463
- const td = resolveEditableTweenDuration(anim, selection);
46464
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46465
- const outsideRange = activeKeyframePct == null && ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
46466
- const resolvedFromValues = {};
46467
- const runtime = resolveDragRuntime(iframe, selector);
46468
- if (runtime && ts !== null) {
46469
- const { gsapLib, el, mainTl } = runtime;
46470
- const draggedValues = {};
46471
- const channelsTheClearWipes = /* @__PURE__ */ new Set([
46472
- ...Object.keys(properties),
46473
- ...PROPERTY_GROUPS.position
46474
- ]);
46475
- for (const key2 of channelsTheClearWipes) {
46476
- const v = Number(gsapLib.getProperty(el, key2));
46477
- if (Number.isFinite(v)) draggedValues[key2] = v;
46409
+ function restore(states) {
46410
+ for (const { layer, attributes, cache: cache2 } of states) {
46411
+ restoreAttributes(layer, attributes);
46412
+ if (cache2 && layer._gsap) restoreCache(layer._gsap, cache2);
46413
+ }
46414
+ }
46415
+ function restoreAttributes(layer, attributes) {
46416
+ for (const { name } of [...layer.attributes])
46417
+ if (!attributes.has(name)) layer.removeAttribute(name);
46418
+ for (const [name, value] of attributes)
46419
+ if (layer.getAttribute(name) !== value) layer.setAttribute(name, value);
46420
+ }
46421
+ function restoreCache(live, cache2) {
46422
+ for (const key2 of Object.keys(live)) if (!(key2 in cache2)) delete live[key2];
46423
+ Object.assign(live, cache2);
46424
+ }
46425
+ function parsedImplicitEndValue(tween) {
46426
+ return (prop, end) => {
46427
+ if (!tween) return null;
46428
+ for (const part of partsFrom(tween, end)) {
46429
+ const pair = endsIn(part, prop);
46430
+ if (pair) return end === "start" ? pair[0] : pair[1];
46431
+ if (prop in (part.vars ?? {})) return null;
46478
46432
  }
46479
- try {
46480
- gsapLib.set(el, { clearProps: Object.keys(properties).join(",") });
46481
- mainTl.seek(ts);
46482
- for (const key2 of Object.keys(properties)) {
46483
- const v = Number(gsapLib.getProperty(el, key2));
46484
- if (Number.isFinite(v)) resolvedFromValues[key2] = roundTo3(v);
46485
- }
46486
- mainTl.seek(ct);
46487
- } catch {
46488
- for (const key2 of Object.keys(resolvedFromValues)) delete resolvedFromValues[key2];
46489
- } finally {
46490
- if (Object.keys(draggedValues).length > 0) gsapLib.set(el, draggedValues);
46433
+ return null;
46434
+ };
46435
+ }
46436
+ function partsFrom(tween, end) {
46437
+ const parts = tween.timeline?.getChildren?.() ?? [];
46438
+ if (parts.length === 0) return [tween];
46439
+ return end === "start" ? parts : [...parts].reverse();
46440
+ }
46441
+ function parsedTweenEase(iframe, tween) {
46442
+ if (!tween) return null;
46443
+ const win = iframe?.contentWindow;
46444
+ const ease = tween.vars?.ease ?? tween.parent?.vars?.defaults?.ease ?? win?.gsap?.defaults?.().ease;
46445
+ if (typeof ease === "string") return ease;
46446
+ const named = (name) => win?.gsap?.parseEase?.(name) === ease;
46447
+ return typeof ease === "function" ? BUILT_IN_EASES.find(named) ?? null : null;
46448
+ }
46449
+ var BUILT_IN_EASES = [
46450
+ "none",
46451
+ ...[
46452
+ "power1",
46453
+ "power2",
46454
+ "power3",
46455
+ "power4",
46456
+ "sine",
46457
+ "expo",
46458
+ "circ",
46459
+ "back",
46460
+ "elastic",
46461
+ "bounce"
46462
+ ].flatMap((e) => [`${e}.in`, `${e}.out`, `${e}.inOut`])
46463
+ ];
46464
+ function withExactStepTimes(anim, tween) {
46465
+ const data = anim.keyframes;
46466
+ const parts = tween?.timeline?.getChildren?.() ?? [];
46467
+ const total = tween?.duration?.() ?? 0;
46468
+ if (data?.format !== "object-array" || parts.length !== data.keyframes.length || !(total > 0))
46469
+ return anim;
46470
+ const ends = parts.map((part) => ((part.startTime?.() ?? 0) + (part.duration?.() ?? 0)) / total);
46471
+ if (ends.some((end) => !Number.isFinite(end))) return anim;
46472
+ const keyframes = data.keyframes.map((kf, i) => ({
46473
+ ...kf,
46474
+ percentage: Math.round(ends[i] * 1e5) / 1e3
46475
+ }));
46476
+ return { ...anim, keyframes: { ...data, keyframes } };
46477
+ }
46478
+
46479
+ // src/hooks/gsapTweenReach.ts
46480
+ function tweenReach(animation, el) {
46481
+ const doc = el?.ownerDocument;
46482
+ if (!el?.isConnected || !doc || animation.hasUnresolvedSelector) return "unknown";
46483
+ let hits;
46484
+ try {
46485
+ hits = [...doc.querySelectorAll(animation.targetSelector)];
46486
+ } catch {
46487
+ return "unknown";
46488
+ }
46489
+ if (!hits.includes(el)) return "elsewhere";
46490
+ return hits.length === 1 ? "own" : "shared";
46491
+ }
46492
+ function tweensForThisElement(selection, animations) {
46493
+ return animations.filter((a) => {
46494
+ const reach = tweenReach(a, selection.element);
46495
+ return reach === "own" || reach === "unknown";
46496
+ });
46497
+ }
46498
+
46499
+ // src/hooks/gsapValueAtPlayhead.ts
46500
+ var PERCENTAGE_SEGMENT_EASE = "power1.inOut";
46501
+ var ARRAY_STEP_EASE = "none";
46502
+ var roundPct2 = (pct) => Math.round(pct * 1e3) / 1e3;
46503
+ var isLinear = (ease) => !ease || ease === "none" || ease === "linear";
46504
+ function refuse(reason) {
46505
+ return { ok: false, reason };
46506
+ }
46507
+ function normalize(edit) {
46508
+ const { anim } = edit;
46509
+ const data = anim.keyframes;
46510
+ if (data) {
46511
+ if (!isLinear(anim.ease) || !isLinear(data.ease)) return { reason: "eased-keyframes" };
46512
+ if (data.format === "simple-array") return { reason: "simple-array-keyframes" };
46513
+ const arrayStep = data.format === "object-array";
46514
+ return {
46515
+ keyframes: data.keyframes.map((kf) => ({
46516
+ ...kf,
46517
+ properties: { ...kf.properties },
46518
+ ...arrayStep ? { ease: kf.ease ?? ARRAY_STEP_EASE } : {}
46519
+ })),
46520
+ ...data.easeEach && !arrayStep ? { easeEach: data.easeEach } : {}
46521
+ };
46522
+ }
46523
+ const easeEach = anim.ease ?? edit.parsedEase;
46524
+ if (!easeEach) return { reason: "unknown-ease" };
46525
+ const to = { percentage: 100, properties: { ...anim.properties } };
46526
+ if (anim.method === "to") return { keyframes: [to], easeEach };
46527
+ if (anim.method === "fromTo") {
46528
+ return { keyframes: [{ percentage: 0, properties: { ...anim.fromProperties } }, to], easeEach };
46529
+ }
46530
+ if (anim.method === "from") {
46531
+ const end = {};
46532
+ for (const prop of Object.keys(anim.properties)) {
46533
+ const value = edit.implicitEndValue(prop, "end");
46534
+ if (value == null) return { reason: "implicit-end-unknown" };
46535
+ end[prop] = value;
46491
46536
  }
46537
+ return {
46538
+ keyframes: [
46539
+ { percentage: 0, properties: { ...anim.properties } },
46540
+ { percentage: 100, properties: end }
46541
+ ],
46542
+ easeEach
46543
+ };
46492
46544
  }
46493
- if (outsideRange && ts !== null) {
46494
- const coalesceKey2 = `gsap:convert-drag:${anim.id}`;
46495
- await callbacks.commitMutation(
46496
- selection,
46497
- {
46498
- type: "convert-to-keyframes",
46499
- animationId: anim.id,
46500
- ...Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}
46501
- },
46502
- { label: "Convert to keyframes for drag", skipReload: true, coalesceKey: coalesceKey2 }
46503
- );
46504
- const fresh = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
46505
- const converted = fresh.find((a) => a.id === anim.id && a.keyframes) ?? fresh.find((a) => a.targetSelector === anim.targetSelector && a.keyframes) ?? anim;
46506
- const convertedStart = resolveTweenStart(converted) ?? ts;
46507
- const convertedDur = resolveTweenDuration(converted) || td;
46508
- await extendTweenAndAddKeyframe(
46509
- selection,
46510
- converted,
46511
- properties,
46512
- ct,
46513
- convertedStart,
46514
- convertedDur,
46515
- callbacks,
46516
- beforeReload
46517
- );
46518
- return;
46545
+ return { reason: "not-a-tween" };
46546
+ }
46547
+ function valueAt(keyframes, prop, side) {
46548
+ const carrying = keyframes.filter((kf2) => kf2.properties[prop] != null);
46549
+ const kf = side === "first" ? carrying[0] : carrying.at(-1);
46550
+ return kf ? { percentage: kf.percentage, value: kf.properties[prop] } : null;
46551
+ }
46552
+ function upsert(keyframes, percentage, properties, ease) {
46553
+ const hit = keyframes.find((kf) => Math.abs(kf.percentage - percentage) <= KEYFRAME_PCT_MATCH);
46554
+ if (hit) {
46555
+ Object.assign(hit.properties, properties);
46556
+ return hit;
46519
46557
  }
46520
- const coalesceKey = `gsap:convert-drag:${anim.id}`;
46521
- await callbacks.commitMutation(
46522
- selection,
46523
- {
46524
- type: "convert-to-keyframes",
46525
- animationId: anim.id,
46526
- ...Object.keys(resolvedFromValues).length > 0 ? { resolvedFromValues } : {}
46527
- },
46528
- { label: "Convert to keyframes for drag", skipReload: true, coalesceKey }
46529
- );
46530
- const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
46531
- const editedSelected = activeKeyframePct != null;
46532
- if (editedSelected) setActiveKeyframePct(null);
46533
- await callbacks.commitMutation(
46534
- selection,
46535
- {
46536
- type: "add-keyframe",
46558
+ const added = { percentage, properties, ...ease ? { ease } : {} };
46559
+ keyframes.push(added);
46560
+ return added;
46561
+ }
46562
+ function heldEnds(norm, end, implicit, backfilled) {
46563
+ const props = new Set(norm.keyframes.flatMap((kf) => Object.keys(kf.properties)));
46564
+ const held = {};
46565
+ for (const prop of props) {
46566
+ const known = valueAt(norm.keyframes, prop, end === "start" ? "first" : "last");
46567
+ const value = known && (end === "end" || known.percentage <= 0) ? known.value : backfilled[prop] ?? implicit(prop, "start");
46568
+ if (value == null) return null;
46569
+ held[prop] = value;
46570
+ }
46571
+ return held;
46572
+ }
46573
+ function planValueAtPlayhead(edit) {
46574
+ const { anim, values } = edit;
46575
+ const start = resolveTweenStart(anim);
46576
+ const duration = resolveTweenDuration(anim);
46577
+ if (start == null || !(duration > 0)) return refuse("no-timing");
46578
+ const norm = normalize(edit);
46579
+ if ("reason" in norm) return refuse(norm.reason);
46580
+ const keyframes = norm.keyframes;
46581
+ const authored = keyframes.length;
46582
+ if ("scaleX" in values || "scaleY" in values) {
46583
+ for (const kf of keyframes) {
46584
+ const { scale, ...rest } = kf.properties;
46585
+ if (scale != null) kf.properties = { scaleX: scale, scaleY: scale, ...rest };
46586
+ }
46587
+ }
46588
+ const backfilled = {};
46589
+ for (const [prop, value] of Object.entries(edit.backfill ?? {})) {
46590
+ if (!(prop in values) || keyframes.some((kf) => kf.properties[prop] != null)) continue;
46591
+ if (edit.holdFromStart && !keyframes.some((kf) => kf.percentage <= 0))
46592
+ keyframes.unshift({ percentage: 0, properties: {} });
46593
+ for (const kf of keyframes) kf.properties[prop] = value;
46594
+ backfilled[prop] = value;
46595
+ }
46596
+ if (Object.keys(values).some((prop) => !keyframes.some((kf) => kf.properties[prop] != null)))
46597
+ return refuse("implicit-end-unknown");
46598
+ let position = start;
46599
+ let span = duration;
46600
+ const pct = "percentage" in edit.at ? edit.at.percentage : roundPct2((edit.at.time - start) / duration * 100);
46601
+ if (pct >= -KEYFRAME_PCT_MATCH && pct <= 100 + KEYFRAME_PCT_MATCH) {
46602
+ const at = Math.min(100, Math.max(0, pct));
46603
+ const next = [...keyframes].sort((a, b) => a.percentage - b.percentage).find((kf) => kf.percentage > at + KEYFRAME_PCT_MATCH);
46604
+ upsert(keyframes, at, { ...values }, next?.ease);
46605
+ } else {
46606
+ const time = edit.at.time;
46607
+ const before = time < start;
46608
+ const held = heldEnds(norm, before ? "start" : "end", edit.implicitEndValue, backfilled);
46609
+ if (!held) return refuse("implicit-end-unknown");
46610
+ position = before ? time : start;
46611
+ span = before ? start + duration - time : time - start;
46612
+ for (const kf of keyframes) {
46613
+ kf.percentage = roundPct2(
46614
+ (start + kf.percentage / 100 * duration - position) / span * 100
46615
+ );
46616
+ }
46617
+ const oldEdge = roundPct2(((before ? start : start + duration) - position) / span * 100);
46618
+ const linear = isLinear(norm.easeEach ?? PERCENTAGE_SEGMENT_EASE) ? void 0 : "none";
46619
+ const edge = upsert(keyframes, oldEdge, held, linear);
46620
+ if (before && linear) edge.ease ??= linear;
46621
+ upsert(keyframes, before ? 0 : 100, { ...held, ...values }, before ? void 0 : linear);
46622
+ }
46623
+ keyframes.sort((a, b) => a.percentage - b.percentage);
46624
+ return {
46625
+ ok: true,
46626
+ mutation: {
46627
+ type: "replace-with-keyframes",
46537
46628
  animationId: anim.id,
46538
- percentage: pct,
46539
- properties,
46540
- ...backfillDefaults ? { backfillDefaults } : {}
46541
- },
46542
- { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey }
46629
+ targetSelector: anim.targetSelector,
46630
+ position: roundTo3(position),
46631
+ duration: roundTo3(span),
46632
+ keyframes,
46633
+ ...norm.easeEach ? { easeEach: norm.easeEach } : {}
46634
+ },
46635
+ added: keyframes.length > authored
46636
+ };
46637
+ }
46638
+ function planValueEdit(selection, anim, values, iframe, { backfill, holdFromStart } = {}) {
46639
+ if (tweenReach(anim, selection.element) === "shared") return refuse("shared-tween");
46640
+ const { activeKeyframePct, currentTime } = usePlayerStore.getState();
46641
+ const tween = findParsedTween(iframe, selection.element, anim);
46642
+ const timed = withExactStepTimes(anim, tween);
46643
+ return planValueAtPlayhead({
46644
+ anim: withLiveTiming(timed, tween),
46645
+ at: activeKeyframePct != null ? { percentage: activeKeyframePct } : { time: currentTime },
46646
+ values,
46647
+ backfill,
46648
+ holdFromStart,
46649
+ implicitEndValue: parsedImplicitEndValue(tween),
46650
+ parsedEase: parsedTweenEase(iframe, tween)
46651
+ });
46652
+ }
46653
+ async function commitValueAtPlayhead(selection, anim, values, iframe, callbacks, options) {
46654
+ await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
46655
+ const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46656
+ const plan = planValueEdit(selection, anim, values, iframe, options);
46657
+ if (!plan.ok) return { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
46658
+ await callbacks.commitMutation(selection, plan.mutation, {
46659
+ label: options.label,
46660
+ softReload: true,
46661
+ beforeReload: options.beforeReload,
46662
+ ...plan.added && { keyframeAction: "add" }
46663
+ });
46664
+ if (activeKeyframePct != null) {
46665
+ setActiveKeyframePct(null);
46666
+ parkPlayheadOnKeyframe(anim, activeKeyframePct);
46667
+ }
46668
+ return { status: "persisted" };
46669
+ }
46670
+
46671
+ // src/hooks/gsapDragPositionCommit.ts
46672
+ function buildTemporalArcKeyframes(anim, percentage, properties) {
46673
+ return [
46674
+ ...(anim.keyframes?.keyframes ?? []).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
46675
+ percentage: keyframe.percentage,
46676
+ properties: { ...keyframe.properties },
46677
+ ...keyframe.ease ? { ease: keyframe.ease } : {}
46678
+ })),
46679
+ { percentage, properties }
46680
+ ].sort((a, b) => a.percentage - b.percentage);
46681
+ }
46682
+ function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, iframe) {
46683
+ if (anim.arcPath?.enabled) return { status: "persisted" };
46684
+ const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
46685
+ selection.element,
46686
+ studioOffset,
46687
+ gsapPos
46543
46688
  );
46544
- if (editedSelected) parkPlayheadOnKeyframe(anim, pct);
46689
+ const plan = planValueEdit(selection, anim, { x: newX, y: newY }, iframe, {
46690
+ backfill: { x: baseGsapX, y: baseGsapY }
46691
+ });
46692
+ return plan.ok ? { status: "persisted" } : { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
46545
46693
  }
46546
- async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos, iframe, selector, callbacks) {
46694
+ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos, iframe, callbacks) {
46547
46695
  const el = selection.element;
46548
46696
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
46549
46697
  el,
@@ -46558,8 +46706,6 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46558
46706
  el.removeAttribute("data-hf-drag-initial-offset-x");
46559
46707
  el.removeAttribute("data-hf-drag-initial-offset-y");
46560
46708
  };
46561
- const backfillDefaults = { x: baseGsapX, y: baseGsapY };
46562
- const ct = usePlayerStore.getState().currentTime;
46563
46709
  if (anim.arcPath?.enabled) {
46564
46710
  const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46565
46711
  const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
@@ -46581,11 +46727,12 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46581
46727
  );
46582
46728
  setActiveKeyframePct(null);
46583
46729
  parkPlayheadOnKeyframe(anim, pct);
46584
- return;
46730
+ return { status: "persisted" };
46585
46731
  }
46586
46732
  const tweenStart = resolveTweenStart(anim);
46587
46733
  const tweenDuration = resolveEditableTweenDuration(anim, selection);
46588
- if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2) return;
46734
+ if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
46735
+ return { status: "persisted" };
46589
46736
  const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
46590
46737
  await callbacks.commitMutation(
46591
46738
  selection,
@@ -46605,171 +46752,12 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46605
46752
  beforeReload: restoreOffset
46606
46753
  }
46607
46754
  );
46608
- return;
46609
- }
46610
- if (anim.keyframes) {
46611
- const newId = await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
46612
- const effectiveAnim = newId ? { ...anim, id: newId } : anim;
46613
- const dragProps = { x: newX, y: newY };
46614
- const ts = resolveTweenStart(effectiveAnim);
46615
- const td = resolveEditableTweenDuration(effectiveAnim, selection);
46616
- const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
46617
- const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
46618
- if (outsideRange && !hasSelectedKeyframe) {
46619
- await extendTweenAndAddKeyframe(
46620
- selection,
46621
- effectiveAnim,
46622
- dragProps,
46623
- ct,
46624
- ts,
46625
- td,
46626
- callbacks,
46627
- restoreOffset,
46628
- backfillDefaults
46629
- );
46630
- } else {
46631
- await commitKeyframedPosition(
46632
- selection,
46633
- effectiveAnim,
46634
- dragProps,
46635
- callbacks,
46636
- restoreOffset,
46637
- backfillDefaults
46638
- );
46639
- }
46640
- } else if (anim.method === "from" || anim.method === "fromTo") {
46641
- const ct2 = usePlayerStore.getState().currentTime;
46642
- const ts = resolveTweenStart(anim);
46643
- const td = resolveEditableTweenDuration(anim, selection);
46644
- const hasSelectedKeyframe = usePlayerStore.getState().activeKeyframePct != null;
46645
- const outsideRange = !hasSelectedKeyframe && ts !== null && td > 0 && (ct2 < ts - 0.01 || ct2 > ts + td + 0.01);
46646
- const dragProps = { x: newX, y: newY };
46647
- if (outsideRange && ts !== null) {
46648
- await callbacks.commitMutation(
46649
- selection,
46650
- { type: "split-into-property-groups", animationId: anim.id },
46651
- { label: "Split from() for drag", skipReload: true }
46652
- );
46653
- const allAnims = callbacks.fetchAnimations ? await callbacks.fetchAnimations() : [];
46654
- const existingPosAnim = allAnims.find(
46655
- (a) => a.propertyGroup === "position" && a.targetSelector === anim.targetSelector
46656
- );
46657
- if (existingPosAnim?.keyframes) {
46658
- const posTs = resolveTweenStart(existingPosAnim);
46659
- const posTd = resolveEditableTweenDuration(existingPosAnim, selection);
46660
- if (posTs !== null) {
46661
- await extendTweenAndAddKeyframe(
46662
- selection,
46663
- existingPosAnim,
46664
- { x: newX, y: newY },
46665
- ct2,
46666
- posTs,
46667
- posTd,
46668
- callbacks,
46669
- restoreOffset,
46670
- backfillDefaults
46671
- );
46672
- return;
46673
- }
46674
- }
46675
- const newStart = Math.min(ct2, ts);
46676
- const newEnd = Math.max(ct2, ts + td);
46677
- const newDuration = Math.max(0.01, newEnd - newStart);
46678
- const dragBefore = ct2 < ts;
46679
- const origStartPct = Math.round((ts - newStart) / newDuration * 1e3) / 10;
46680
- const origEndPct = Math.round((ts + td - newStart) / newDuration * 1e3) / 10;
46681
- const keyframes = [];
46682
- if (dragBefore) {
46683
- keyframes.push({ percentage: 0, properties: { x: newX, y: newY } });
46684
- if (origStartPct > 0.5 && origStartPct < 99.5) {
46685
- keyframes.push({ percentage: origStartPct, properties: { x: 0, y: 0 } });
46686
- }
46687
- keyframes.push({ percentage: 100, properties: { x: 0, y: 0 } });
46688
- } else {
46689
- keyframes.push({ percentage: 0, properties: { x: 0, y: 0 } });
46690
- if (origEndPct > 0.5 && origEndPct < 99.5) {
46691
- keyframes.push({ percentage: origEndPct, properties: { x: 0, y: 0 } });
46692
- }
46693
- keyframes.push({ percentage: 100, properties: { x: newX, y: newY } });
46694
- }
46695
- keyframes.sort((a, b) => a.percentage - b.percentage);
46696
- const baseKf = {
46697
- targetSelector: anim.targetSelector,
46698
- position: roundTo3(newStart),
46699
- duration: roundTo3(newDuration),
46700
- keyframes
46701
- };
46702
- await callbacks.commitMutation(
46703
- selection,
46704
- existingPosAnim ? { type: "replace-with-keyframes", animationId: existingPosAnim.id, ...baseKf } : { type: "add-with-keyframes", ...baseKf },
46705
- {
46706
- label: "Move layer (from extended)",
46707
- keyframeAction: "add",
46708
- softReload: true,
46709
- beforeReload: restoreOffset
46710
- }
46711
- );
46712
- } else {
46713
- const coalesceKey = `gsap:convert-drag:${anim.id}`;
46714
- await callbacks.commitMutation(
46715
- selection,
46716
- {
46717
- type: "convert-to-keyframes",
46718
- animationId: anim.id
46719
- },
46720
- { label: "Convert from() for drag", skipReload: true, coalesceKey }
46721
- );
46722
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46723
- const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
46724
- if (activeKeyframePct != null) setActiveKeyframePct(null);
46725
- await callbacks.commitMutation(
46726
- selection,
46727
- {
46728
- type: "add-keyframe",
46729
- animationId: anim.id,
46730
- percentage: pct,
46731
- properties: dragProps,
46732
- ...backfillDefaults ? { backfillDefaults } : {}
46733
- },
46734
- {
46735
- label: `Move layer (keyframe ${pct}%)`,
46736
- softReload: true,
46737
- beforeReload: restoreOffset,
46738
- coalesceKey
46739
- }
46740
- );
46741
- }
46742
- } else {
46743
- await commitFlatViaKeyframes(
46744
- selection,
46745
- anim,
46746
- { x: newX, y: newY },
46747
- callbacks,
46748
- restoreOffset,
46749
- iframe,
46750
- selector,
46751
- backfillDefaults
46752
- );
46753
- }
46754
- }
46755
-
46756
- // src/hooks/gsapTweenReach.ts
46757
- function tweenReach(animation, el) {
46758
- const doc = el?.ownerDocument;
46759
- if (!el?.isConnected || !doc || animation.hasUnresolvedSelector) return "unknown";
46760
- let hits;
46761
- try {
46762
- hits = [...doc.querySelectorAll(animation.targetSelector)];
46763
- } catch {
46764
- return "unknown";
46755
+ return { status: "persisted" };
46765
46756
  }
46766
- if (!hits.includes(el)) return "elsewhere";
46767
- return hits.length === 1 ? "own" : "shared";
46768
- }
46769
- function tweensForThisElement(selection, animations) {
46770
- return animations.filter((a) => {
46771
- const reach = tweenReach(a, selection.element);
46772
- return reach === "own" || reach === "unknown";
46757
+ return commitValueAtPlayhead(selection, anim, { x: newX, y: newY }, iframe, callbacks, {
46758
+ label: "Move layer",
46759
+ backfill: { x: baseGsapX, y: baseGsapY },
46760
+ beforeReload: restoreOffset
46773
46761
  });
46774
46762
  }
46775
46763
 
@@ -46795,40 +46783,59 @@ function animHasPosition(anim) {
46795
46783
  function findGsapPositionAnimation(animations, selector) {
46796
46784
  if (animations.length === 0) return null;
46797
46785
  const currentTime = usePlayerStore.getState().currentTime;
46798
- const scored = animations.filter((a) => animHasPosition(a) || a.keyframes || animations.length === 1).map((a) => {
46786
+ const scored = animations.filter(animHasPosition).map((a) => {
46799
46787
  let score = 0;
46800
- if (animHasPosition(a)) score += 10;
46801
46788
  if (a.keyframes) score += 5;
46802
46789
  if (selector && a.targetSelector === selector) score += 8;
46803
46790
  else if (a.targetSelector.includes(",")) score -= 5;
46804
- const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
46805
- const dur = a.duration ?? 0;
46806
- if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) score += 50;
46807
- else
46808
- score -= Math.round(
46809
- Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5
46810
- );
46811
- return { anim: a, score };
46791
+ return { anim: a, score: score + playheadProximity(a, currentTime) };
46812
46792
  });
46813
46793
  scored.sort((a, b) => b.score - a.score);
46814
- return scored[0]?.anim ?? animations[0];
46794
+ return scored[0]?.anim ?? null;
46795
+ }
46796
+ function playheadProximity(a, currentTime) {
46797
+ const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
46798
+ const dur = a.duration ?? 0;
46799
+ if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
46800
+ return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
46801
+ }
46802
+ function statesValueAt(anim, time) {
46803
+ const duration = resolveTweenDuration(anim);
46804
+ if (!(duration > 0)) return false;
46805
+ const pct = (time - (resolveTweenStart(anim) ?? 0)) / duration * 100;
46806
+ const near3 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
46807
+ if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near3(kf.percentage));
46808
+ if (anim.method === "from") return near3(0);
46809
+ return near3(100) || anim.method === "fromTo" && near3(0);
46815
46810
  }
46816
46811
  function pickClosestToPlayhead(anims) {
46817
46812
  if (anims.length <= 1) return anims[0] ?? null;
46818
46813
  const ct = usePlayerStore.getState().currentTime;
46819
46814
  return anims.reduce((best, a) => {
46820
- const s = resolveTweenStart(a) ?? 0;
46821
- const e = s + resolveTweenDuration(a);
46822
- const dist = ct >= s && ct <= e ? 0 : Math.min(Math.abs(ct - s), Math.abs(ct - e));
46823
- const bestS = resolveTweenStart(best) ?? 0;
46824
- const bestE = bestS + resolveTweenDuration(best);
46825
- const bestDist = ct >= bestS && ct <= bestE ? 0 : Math.min(Math.abs(ct - bestS), Math.abs(ct - bestE));
46815
+ const dist = playheadDistance(a, ct);
46816
+ const bestDist = playheadDistance(best, ct);
46826
46817
  if (dist < bestDist) return a;
46827
- if (dist === bestDist && (a.keyframes?.keyframes.length ?? 0) > (best.keyframes?.keyframes.length ?? 0))
46828
- return a;
46818
+ const stated = dist === 0 && bestDist === 0 ? statedValueWinner(a, best, ct) : null;
46819
+ if (stated) return stated;
46820
+ if (dist === bestDist && keyframeCount(a) > keyframeCount(best)) return a;
46829
46821
  return best;
46830
46822
  });
46831
46823
  }
46824
+ function playheadDistance(anim, time) {
46825
+ const s = resolveTweenStart(anim) ?? 0;
46826
+ const e = s + resolveTweenDuration(anim);
46827
+ return time >= s && time <= e ? 0 : Math.min(Math.abs(time - s), Math.abs(time - e));
46828
+ }
46829
+ function statedValueWinner(a, b, time) {
46830
+ const [mine, theirs] = [statesValueAt(a, time), statesValueAt(b, time)];
46831
+ if (mine !== theirs) return mine ? a : b;
46832
+ const [s, bS] = [resolveTweenStart(a) ?? 0, resolveTweenStart(b) ?? 0];
46833
+ if (mine && s !== bS) return s > bS ? a : b;
46834
+ return null;
46835
+ }
46836
+ function keyframeCount(anim) {
46837
+ return anim.keyframes?.keyframes.length ?? 0;
46838
+ }
46832
46839
 
46833
46840
  // src/hooks/gsapElementMatch.ts
46834
46841
  function getAnimationsForElement(animations, target, element) {
@@ -46858,40 +46865,19 @@ function getAnimationsForElement(animations, target, element) {
46858
46865
  // src/hooks/gsapRuntimeBridge.ts
46859
46866
  var POSITION_CHANNEL_SET = new Set(POSITION_CHANNELS);
46860
46867
  var ROTATION_CHANNEL_SET = new Set(ROTATION_CHANNELS);
46861
- async function resolveGroupTween(group, animations, selection, commitMutation, fetchFallbackAnimations) {
46862
- const groupAnims = animations.filter((a) => a.propertyGroup === group);
46863
- const groupAnim = pickClosestToPlayhead(groupAnims);
46864
- if (groupAnim) return { anim: groupAnim, animations };
46865
- const legacyMixed = animations.find((a) => !a.propertyGroup);
46866
- if (legacyMixed) {
46867
- await commitMutation(
46868
- selection,
46869
- { type: "split-into-property-groups", animationId: legacyMixed.id },
46870
- { label: "Split mixed tween into property groups", skipReload: true }
46871
- );
46872
- if (fetchFallbackAnimations) {
46873
- const fresh = await fetchFallbackAnimations();
46874
- const freshGroupAnim = fresh.find((a) => a.propertyGroup === group);
46875
- if (freshGroupAnim) return { anim: freshGroupAnim, animations: fresh };
46876
- }
46877
- }
46878
- if (!legacyMixed && fetchFallbackAnimations) {
46879
- const fresh = await fetchFallbackAnimations();
46880
- const freshGroupAnim = fresh.find((a) => a.propertyGroup === group);
46881
- if (freshGroupAnim) return { anim: freshGroupAnim, animations: fresh };
46882
- const freshLegacy = fresh.find((a) => !a.propertyGroup);
46883
- if (freshLegacy) {
46884
- await commitMutation(
46885
- selection,
46886
- { type: "split-into-property-groups", animationId: freshLegacy.id },
46887
- { label: "Split mixed tween into property groups", skipReload: true }
46888
- );
46889
- const reFetched = await fetchFallbackAnimations();
46890
- const reFetchedGroup = reFetched.find((a) => a.propertyGroup === group);
46891
- if (reFetchedGroup) return { anim: reFetchedGroup, animations: reFetched };
46892
- }
46893
- }
46894
- return null;
46868
+ async function resolveGroupTween(group, animations, _selection, _commitMutation, fetchFallbackAnimations) {
46869
+ const inGroup = (list) => {
46870
+ const tagged = list.filter((a) => a.propertyGroup === group);
46871
+ const props = new Set(PROPERTY_GROUPS[group]);
46872
+ const mixed = list.filter((a) => !a.propertyGroup && animationWritesAnyProperty(a, props));
46873
+ return pickClosestToPlayhead(tagged.length > 0 ? tagged : mixed);
46874
+ };
46875
+ const anim = inGroup(animations);
46876
+ if (anim) return { anim, animations };
46877
+ if (!fetchFallbackAnimations) return null;
46878
+ const fresh = await fetchFallbackAnimations();
46879
+ const freshAnim = inGroup(fresh);
46880
+ return freshAnim ? { anim: freshAnim, animations: fresh } : null;
46895
46881
  }
46896
46882
  var writesPosition = (a) => animationWritesAnyProperty(a, POSITION_CHANNEL_SET);
46897
46883
  function positionedOnlyBySharedTween(selection, animations) {
@@ -46939,6 +46925,36 @@ function oneUndoStep(commit) {
46939
46925
  const coalesceKey = `gsap:drag:${++dragGestureCounter}`;
46940
46926
  return (selection, mutation, options) => commit(selection, mutation, { ...options, coalesceKey, coalesceMs: Number.POSITIVE_INFINITY });
46941
46927
  }
46928
+ var isPositionWriteOf = (selector) => (a) => a.targetSelector === selector && isXYPositionWrite(a);
46929
+ function positionWriteKeeper(animations, selector) {
46930
+ const dupes = animations.filter(isPositionWriteOf(selector));
46931
+ if (dupes.length < 2 || dupes.filter((a) => !isInstantHold(a)).length > 1) return null;
46932
+ return dupes.find((a) => a.keyframes) ?? dupes.find((a) => (a.duration ?? 0) > 0) ?? dupes[0];
46933
+ }
46934
+ function dragRoute(posAnim, iframe, selector, altKey) {
46935
+ const hasNonHold = hasNonHoldTweenForElement(iframe, selector, void 0, POSITION_CHANNELS);
46936
+ const hasKeyframedPosTween = !!posAnim?.keyframes && resolveTweenDuration(posAnim) > 0;
46937
+ if (!hasNonHold && !hasKeyframedPosTween) return "static";
46938
+ return altKey || !usePlayerStore.getState().autoKeyframeEnabled ? "whole-path" : "at-playhead";
46939
+ }
46940
+ async function planDrag(selection, offset, allAnimations, iframe, options) {
46941
+ const selector = selectorFromSelection(selection);
46942
+ if (!selector) return { status: "blocked", reason: "no-selector" };
46943
+ const own = tweensForThisElement(selection, allAnimations);
46944
+ const keeper = positionWriteKeeper(own, selector);
46945
+ const animations = keeper ? own.filter((a) => a === keeper || !isPositionWriteOf(selector)(a)) : own;
46946
+ const resolved2 = await resolveGroupTween("position", animations, selection, async () => {
46947
+ });
46948
+ const posAnim = resolved2?.anim ?? findGsapPositionAnimation(animations, selector);
46949
+ const route = dragRoute(posAnim, iframe, selector, options.altKey);
46950
+ if (route === "static") return { status: "persisted" };
46951
+ if (!posAnim) {
46952
+ return { status: "blocked", reason: "source-uneditable", detail: "no-position-tween" };
46953
+ }
46954
+ if (route === "whole-path") return { status: "persisted" };
46955
+ const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
46956
+ return gsapPositionFromDragOutcome(selection, posAnim, offset, gsapPos, iframe);
46957
+ }
46942
46958
  async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, gestureCommit, fetchAllAnimations, options) {
46943
46959
  if (!options?.preflightPassed) {
46944
46960
  const preflight = await preflightGsapDragIntercept(
@@ -46948,7 +46964,10 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46948
46964
  fetchAllAnimations,
46949
46965
  options?.group
46950
46966
  );
46951
- if (preflight.status !== "persisted" || options?.preflightOnly) return preflight;
46967
+ if (preflight.status !== "persisted") return preflight;
46968
+ if (options?.preflightOnly) {
46969
+ return options.group ? planDrag(selection, offset, allAnimations, iframe, options) : preflight;
46970
+ }
46952
46971
  }
46953
46972
  const animations = tweensForThisElement(selection, allAnimations);
46954
46973
  const fetchFallbackAnimations = fetchAllAnimations && (async () => tweensForThisElement(selection, await fetchAllAnimations()));
@@ -46956,12 +46975,10 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46956
46975
  if (!selector) return { status: "blocked", reason: "no-selector" };
46957
46976
  const commitMutation = oneUndoStep(gestureCommit);
46958
46977
  let workingAnimations = animations;
46959
- const isPosWrite = (a) => a.targetSelector === selector && isXYPositionWrite(a);
46960
- if (animations.filter(isPosWrite).length > 1 && fetchFallbackAnimations) {
46978
+ if (animations.filter(isPositionWriteOf(selector)).length > 1 && fetchFallbackAnimations) {
46961
46979
  const fresh = await fetchFallbackAnimations();
46962
- const dupes = fresh.filter(isPosWrite);
46963
- if (dupes.length > 1) {
46964
- const keeper = dupes.find((a) => a.keyframes) ?? dupes.find((a) => (a.duration ?? 0) > 0) ?? dupes[0];
46980
+ const keeper = positionWriteKeeper(fresh, selector);
46981
+ if (keeper) {
46965
46982
  await commitMutation(
46966
46983
  selection,
46967
46984
  {
@@ -46994,9 +47011,8 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46994
47011
  }
46995
47012
  }
46996
47013
  const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
46997
- const hasNonHold = hasNonHoldTweenForElement(iframe, selector, void 0, POSITION_CHANNELS);
46998
- const hasKeyframedPosTween = !!posAnim?.keyframes && resolveTweenDuration(posAnim) > 0;
46999
- if (!hasNonHold && !hasKeyframedPosTween) {
47014
+ const route = dragRoute(posAnim, iframe, selector, options?.altKey);
47015
+ if (route === "static") {
47000
47016
  const existingSet = posAnim && isInstantHold(posAnim) && posAnim.targetSelector === selector ? posAnim : findExistingPositionWrite(resolvedAnimations, selector, selection.element);
47001
47017
  await commitStaticGsapPosition(selection, offset, gsapPos, selector, existingSet, {
47002
47018
  commitMutation,
@@ -47009,19 +47025,20 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
47009
47025
  }
47010
47026
  if (fetchFallbackAnimations) {
47011
47027
  const fresh = await fetchFallbackAnimations();
47012
- const freshMatch = fresh.find(
47013
- (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
47028
+ const freshMatch = fresh.find((a) => a.id === posAnim.id) ?? pickClosestToPlayhead(
47029
+ fresh.filter(
47030
+ (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
47031
+ )
47014
47032
  );
47015
47033
  if (freshMatch && freshMatch.id !== posAnim.id) {
47016
47034
  posAnim = freshMatch;
47017
47035
  }
47018
47036
  }
47019
47037
  const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations };
47020
- const autoKeyframeEnabled = usePlayerStore.getState().autoKeyframeEnabled;
47021
- if (options?.altKey || !autoKeyframeEnabled) {
47038
+ if (route === "whole-path") {
47022
47039
  await commitWholePathOffset(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
47023
47040
  } else {
47024
- await commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
47041
+ return commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, cbs);
47025
47042
  }
47026
47043
  return { status: "persisted" };
47027
47044
  }
@@ -47065,7 +47082,9 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
47065
47082
  const resolvedAnimations = resolved2?.animations ?? workingAnimations;
47066
47083
  let anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, ROTATION_CHANNEL_SET) ? resolved2.anim : null;
47067
47084
  if (!anim) {
47068
- anim = workingAnimations.find((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET)) ?? null;
47085
+ anim = pickClosestToPlayhead(
47086
+ workingAnimations.filter((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET))
47087
+ );
47069
47088
  }
47070
47089
  const newRotation = roundTo3(angle);
47071
47090
  if (!anim || isInstantHold(anim)) {
@@ -47089,35 +47108,14 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
47089
47108
  );
47090
47109
  return { status: "persisted" };
47091
47110
  }
47092
- if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
47093
- const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
47094
- if (newId) anim = { ...anim, id: newId };
47095
- } else if (!anim.keyframes) {
47096
- const resolvedFromValues = selector ? readAllAnimatedProperties(iframe, selector, anim, "rotation") : void 0;
47097
- await commitMutation(
47098
- selection,
47099
- { type: "convert-to-keyframes", animationId: anim.id, resolvedFromValues },
47100
- { label: "Convert to keyframes for rotation", skipReload: true }
47101
- );
47102
- }
47103
- const runtimeProps = readAllAnimatedProperties(iframe, selector, anim, "rotation");
47104
- const backfillDefaults = { ...runtimeProps };
47105
- if (!("rotation" in runtimeProps)) {
47106
- backfillDefaults.rotation = readGsapProperty(iframe, selector, "rotation") ?? 0;
47107
- }
47108
- const properties = { ...runtimeProps, rotation: newRotation };
47109
- await commitMutation(
47111
+ return commitValueAtPlayhead(
47110
47112
  selection,
47111
- {
47112
- type: "add-keyframe",
47113
- animationId: anim.id,
47114
- percentage: pct,
47115
- properties,
47116
- backfillDefaults
47117
- },
47118
- { label: `Rotate (keyframe ${pct}%)`, softReload: true }
47113
+ anim,
47114
+ { rotation: newRotation },
47115
+ iframe,
47116
+ { commitMutation, fetchAnimations: fetchFallbackAnimations },
47117
+ { label: "Rotate", backfill: { rotation: newRotation }, holdFromStart: true }
47119
47118
  );
47120
- return { status: "persisted" };
47121
47119
  }
47122
47120
 
47123
47121
  // src/hooks/keyframeCacheAstLoad.ts
@@ -47141,7 +47139,7 @@ function fetchParsedAnimations(projectId, sourceFile, options = {}) {
47141
47139
  async function requestParsedAnimations(projectId, sourceFile) {
47142
47140
  if (!isPreviewBooted(projectId) && !await whenPreviewBooted(projectId)) return null;
47143
47141
  try {
47144
- const res = await fetch(
47142
+ const res = await studioApiFetch(
47145
47143
  `/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
47146
47144
  // Always revalidate; an unchanged file answers 304. No per-call timestamp
47147
47145
  // (a deterministic-render no-no).
@@ -47433,8 +47431,8 @@ var MIN_TWEEN_DURATION = 0.01;
47433
47431
  var round33 = (n) => Math.round(n * 1e3) / 1e3;
47434
47432
  var clamp5 = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
47435
47433
  function resolveFlatTweenBoundaryRetime(opts) {
47436
- const { keyframeCount, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
47437
- if (keyframeCount > 0) return null;
47434
+ const { keyframeCount: keyframeCount2, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
47435
+ if (keyframeCount2 > 0) return null;
47438
47436
  if (draggedTweenPct !== 0 && draggedTweenPct !== 100) return { kind: "noop" };
47439
47437
  const draggedTime = draggedTweenPct === 0 ? tweenStart : tweenEnd;
47440
47438
  if (Math.abs(dropAbsTime - draggedTime) <= EPSILON_TIME) return { kind: "noop" };
@@ -49368,7 +49366,7 @@ function StudioAgentTools() {
49368
49366
  // fetches the URL itself.
49369
49367
  probeFrame: async (url) => {
49370
49368
  try {
49371
- const response = await fetch(url, { method: "HEAD" });
49369
+ const response = await studioApiFetch(url, { method: "HEAD" });
49372
49370
  return { ok: response.ok, status: response.status };
49373
49371
  } catch {
49374
49372
  return { ok: false, status: 0 };
@@ -50779,6 +50777,23 @@ function useRenderClipContent({
50779
50777
  );
50780
50778
  }
50781
50779
 
50780
+ // src/hooks/useThumbnailStill.ts
50781
+ import { useMemo as useMemo49 } from "react";
50782
+ var LOADING = Object.freeze({ status: "loading" });
50783
+ var FAILED = Object.freeze({ status: "failed" });
50784
+ function useThumbnailStill(url, projectId, { sessionEpoch = 0, priority = "visible" } = {}) {
50785
+ const request = useMemo49(
50786
+ () => url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null,
50787
+ [priority, projectId, sessionEpoch, url]
50788
+ );
50789
+ const snapshot3 = useThumbnailLease(request);
50790
+ const ready = snapshot3.status === "ready" && snapshot3.value.kind === "image" ? snapshot3.value.url : null;
50791
+ return useMemo49(
50792
+ () => ready ? { status: "ready", url: ready } : snapshot3.status === "error" ? FAILED : LOADING,
50793
+ [ready, snapshot3.status]
50794
+ );
50795
+ }
50796
+
50782
50797
  // src/components/editor/SourceEditor.tsx
50783
50798
  import { useRef as useRef82, useCallback as useCallback76, useEffect as useEffect68, memo as memo35 } from "react";
50784
50799
  import {
@@ -51002,13 +51017,13 @@ var SourceEditor = memo35(function SourceEditor2({
51002
51017
  });
51003
51018
 
51004
51019
  // src/components/editor/PropertyPanel.tsx
51005
- import { memo as memo39, useMemo as useMemo64, useRef as useRef113, useState as useState101 } from "react";
51020
+ import { memo as memo39, useMemo as useMemo65, useRef as useRef113, useState as useState101 } from "react";
51006
51021
 
51007
51022
  // src/components/editor/InspectorHeaderActions.tsx
51008
51023
  import { Eye as Eye3, EyeSlash as EyeSlash2 } from "@phosphor-icons/react";
51009
51024
 
51010
51025
  // src/contexts/DesignPanelInputContext.tsx
51011
- import { useCallback as useCallback77, useContext as useContext12, useMemo as useMemo49 } from "react";
51026
+ import { useCallback as useCallback77, useContext as useContext12, useMemo as useMemo50 } from "react";
51012
51027
 
51013
51028
  // src/utils/designInputTracking.ts
51014
51029
  var COALESCE_WINDOW_MS = 600;
@@ -51051,7 +51066,7 @@ function DesignPanelInputProvider({
51051
51066
  children
51052
51067
  }) {
51053
51068
  const parent = useContext12(DesignPanelInputContext);
51054
- const value = useMemo49(
51069
+ const value = useMemo50(
51055
51070
  () => ({ ui: ui ?? parent.ui, section: section ?? parent.section }),
51056
51071
  [ui, section, parent.ui, parent.section]
51057
51072
  );
@@ -51587,7 +51602,7 @@ function readGsapRuntimeValuesForPanel(gsapAnimId, gsapAnimations, element, prev
51587
51602
  const el = findPreviewNode(iframe.contentDocument, element);
51588
51603
  if (!el) return null;
51589
51604
  const propKeys = collectPanelPropKeys(gsapAnimations);
51590
- const readsTransform = gsapWritesPosition(el);
51605
+ const readsTransform = !editsPlainCss(el, "move");
51591
51606
  const result = {};
51592
51607
  for (const prop of propKeys) {
51593
51608
  if (!readsTransform && GSAP_TRANSFORM_KEYS.has(prop)) continue;
@@ -52267,7 +52282,7 @@ function createTransformCommitHandlers({
52267
52282
  "This element's position can't be edited here yet \u2014 it is driven by its animation"
52268
52283
  ))
52269
52284
  return;
52270
- const plainTranslate = !gsapWritesPosition(element.element);
52285
+ const plainTranslate = editsPlainCss(element.element, "move");
52271
52286
  const current = readMoveOffset(element.element, plainTranslate);
52272
52287
  if (!Number.isFinite(current.x) || !Number.isFinite(current.y))
52273
52288
  return void showToast?.(UNREADABLE_TRANSLATE);
@@ -52716,7 +52731,7 @@ function MediaSection({
52716
52731
  import { isHfColorGradingActive as isHfColorGradingActive2 } from "@hyperframes/core/color-grading";
52717
52732
 
52718
52733
  // src/components/editor/propertyPanelColorGradingControls.tsx
52719
- import { useMemo as useMemo50, useRef as useRef87, useState as useState63 } from "react";
52734
+ import { useMemo as useMemo51, useRef as useRef87, useState as useState63 } from "react";
52720
52735
  import {
52721
52736
  HF_COLOR_GRADING_PRESETS,
52722
52737
  normalizeHfColorGrading as normalizeHfColorGrading2
@@ -53160,7 +53175,7 @@ function ColorGradingControls({
53160
53175
  const [detailSettings, setDetailSettings] = useState63(null);
53161
53176
  const [lutImporting, setLutImporting] = useState63(false);
53162
53177
  const [lutImportError, setLutImportError] = useState63(null);
53163
- const lutAssets = useMemo50(
53178
+ const lutAssets = useMemo51(
53164
53179
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
53165
53180
  [assets]
53166
53181
  );
@@ -53440,7 +53455,7 @@ function ColorGradingControls({
53440
53455
  }
53441
53456
 
53442
53457
  // src/components/editor/useColorGradingController.ts
53443
- import { useCallback as useCallback82, useEffect as useEffect75, useMemo as useMemo51, useRef as useRef89, useState as useState65 } from "react";
53458
+ import { useCallback as useCallback82, useEffect as useEffect75, useMemo as useMemo52, useRef as useRef89, useState as useState65 } from "react";
53444
53459
  import {
53445
53460
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR3,
53446
53461
  hasHfColorGradingAuthoredValues,
@@ -53803,7 +53818,7 @@ function useColorGradingController({
53803
53818
  state: "pending",
53804
53819
  message: "Waiting for runtime"
53805
53820
  }));
53806
- const selectedAssetPath = useMemo51(
53821
+ const selectedAssetPath = useMemo52(
53807
53822
  () => selectedMediaAssetPath(element, projectId),
53808
53823
  [element, projectId]
53809
53824
  );
@@ -53850,7 +53865,7 @@ function useColorGradingController({
53850
53865
  trackStudioPendingEdit(onSetAttributeLive(COLOR_GRADING_DATA_KEY, value));
53851
53866
  };
53852
53867
  }, [identityKey]);
53853
- const target = useMemo51(
53868
+ const target = useMemo52(
53854
53869
  () => ({
53855
53870
  id: element.id ?? null,
53856
53871
  hfId: element.hfId ?? null,
@@ -53871,7 +53886,7 @@ function useColorGradingController({
53871
53886
  return;
53872
53887
  }
53873
53888
  const controller = new AbortController();
53874
- fetch(cacheKey, { signal: controller.signal }).then(async (response) => {
53889
+ studioApiFetch(cacheKey, { signal: controller.signal }).then(async (response) => {
53875
53890
  if (!response.ok) return { ok: false };
53876
53891
  const data = await response.json();
53877
53892
  return { ok: true, metadata: data?.metadata ?? null };
@@ -54830,7 +54845,7 @@ function ColorField({
54830
54845
  }
54831
54846
 
54832
54847
  // src/components/editor/propertyPanelFont.tsx
54833
- import { useEffect as useEffect77, useMemo as useMemo52, useRef as useRef91, useState as useState67, useSyncExternalStore as useSyncExternalStore8 } from "react";
54848
+ import { useEffect as useEffect77, useMemo as useMemo53, useRef as useRef91, useState as useState67, useSyncExternalStore as useSyncExternalStore8 } from "react";
54834
54849
 
54835
54850
  // src/components/editor/fontAssets.ts
54836
54851
  var FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i;
@@ -54945,7 +54960,7 @@ var fontLists = {
54945
54960
  var fontListsRequest = null;
54946
54961
  var fontListListeners = /* @__PURE__ */ new Set();
54947
54962
  async function fetchFontList(url) {
54948
- const data = await (await fetch(url)).json();
54963
+ const data = await (await studioApiFetch(url)).json();
54949
54964
  if (!Array.isArray(data.fonts)) throw new Error(`${url} returned no font list`);
54950
54965
  return data.fonts;
54951
54966
  }
@@ -55076,13 +55091,13 @@ function FontFamilyField({
55076
55091
  setImportingFonts(false);
55077
55092
  }
55078
55093
  };
55079
- const projectFontAssets = useMemo52(
55094
+ const projectFontAssets = useMemo53(
55080
55095
  () => uniqueFontOptions(
55081
55096
  importedFonts.map((f) => ({ family: f.family, source: "Imported" }))
55082
55097
  ),
55083
55098
  [importedFonts]
55084
55099
  );
55085
- const options = useMemo52(() => {
55100
+ const options = useMemo53(() => {
55086
55101
  if (!open) return [];
55087
55102
  const documentFonts = collectDocumentFontFamilies();
55088
55103
  return sortFontOptions(
@@ -55098,7 +55113,7 @@ function FontFamilyField({
55098
55113
  ])
55099
55114
  );
55100
55115
  }, [open, currentFamily, lists, localFonts, projectFontAssets]);
55101
- const filteredOptions = useMemo52(() => {
55116
+ const filteredOptions = useMemo53(() => {
55102
55117
  const matches = options.filter((o) => fontMatchesQuery(o.family, query));
55103
55118
  if (query.trim()) return matches.slice(0, 200);
55104
55119
  const bySource = /* @__PURE__ */ new Map();
@@ -55128,7 +55143,7 @@ function FontFamilyField({
55128
55143
  };
55129
55144
  const importSystemFont = async (family) => {
55130
55145
  if (!onImportFonts) return null;
55131
- const response = await fetch(`/api/fonts/file?family=${encodeURIComponent(family)}`);
55146
+ const response = await studioApiFetch(`/api/fonts/file?family=${encodeURIComponent(family)}`);
55132
55147
  if (!response.ok) return null;
55133
55148
  const blob = await response.blob();
55134
55149
  const ext2 = response.headers.get("Content-Disposition")?.match(/\.(\w+)"?$/)?.[1] ?? "ttf";
@@ -55334,7 +55349,7 @@ function FontFamilyField({
55334
55349
  import { useEffect as useEffect79 } from "react";
55335
55350
 
55336
55351
  // src/components/panels/VariablesBindElement.tsx
55337
- import { useMemo as useMemo53, useState as useState69 } from "react";
55352
+ import { useMemo as useMemo54, useState as useState69 } from "react";
55338
55353
 
55339
55354
  // src/components/panels/VariablesValueControls.tsx
55340
55355
  import { useState as useState68 } from "react";
@@ -55616,7 +55631,7 @@ function VariablesBindElement({
55616
55631
  }) {
55617
55632
  const [activeKey, setActiveKey] = useState69(null);
55618
55633
  const [idDraft, setIdDraft] = useState69("");
55619
- const actions = useMemo53(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
55634
+ const actions = useMemo54(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
55620
55635
  if (actions.length === 0) return null;
55621
55636
  const active = actions.find((a) => a.key === activeKey) ?? null;
55622
55637
  const trimmedId = sanitizeId(idDraft);
@@ -55727,7 +55742,7 @@ function uniqueId(base2, existing) {
55727
55742
  }
55728
55743
 
55729
55744
  // src/contexts/VariablePromoteContext.tsx
55730
- import { useContext as useContext13, useEffect as useEffect78, useMemo as useMemo54, useState as useState70 } from "react";
55745
+ import { useContext as useContext13, useEffect as useEffect78, useMemo as useMemo55, useState as useState70 } from "react";
55731
55746
  import { jsx as jsx129 } from "react/jsx-runtime";
55732
55747
  var VariablePromoteContext = createStableContext(
55733
55748
  "VariablePromoteContext",
@@ -55750,15 +55765,15 @@ function VariablePromoteProvider({
55750
55765
  if (!session) return;
55751
55766
  return session.on("change", () => setRevision((r) => r + 1));
55752
55767
  }, [session]);
55753
- const actions = useMemo54(() => {
55768
+ const actions = useMemo55(() => {
55754
55769
  if (!session || !selection) return [];
55755
55770
  return buildBindActions(selection, session);
55756
55771
  }, [session, selection, revision]);
55757
- const declarations = useMemo54(() => {
55772
+ const declarations = useMemo55(() => {
55758
55773
  if (!session) return [];
55759
55774
  return session.getVariableDeclarations();
55760
55775
  }, [session, revision]);
55761
- const value = useMemo54(
55776
+ const value = useMemo55(
55762
55777
  () => ({ session, selection, actions, declarations, persist: persist2, onPersistError }),
55763
55778
  [session, selection, actions, declarations, persist2, onPersistError]
55764
55779
  );
@@ -55767,7 +55782,7 @@ function VariablePromoteProvider({
55767
55782
  function useVariablePromoteChannel(channel) {
55768
55783
  const ctx3 = useContext13(VariablePromoteContext);
55769
55784
  const key2 = channel.kind === "style" ? `style:${channel.prop}` : channel.kind;
55770
- return useMemo54(() => {
55785
+ return useMemo55(() => {
55771
55786
  if (!ctx3 || !ctx3.session || !ctx3.selection?.hfId) return null;
55772
55787
  const { session, selection, actions, declarations, persist: persist2, onPersistError } = ctx3;
55773
55788
  const hfId = selection.hfId;
@@ -56191,7 +56206,7 @@ function insertGradientStop(model, position) {
56191
56206
  }
56192
56207
 
56193
56208
  // src/components/editor/propertyPanelFill.tsx
56194
- import { useMemo as useMemo55, useRef as useRef92, useState as useState71 } from "react";
56209
+ import { useMemo as useMemo56, useRef as useRef92, useState as useState71 } from "react";
56195
56210
 
56196
56211
  // src/components/icons/ReverseGradientIcon.tsx
56197
56212
  import { jsx as jsx131, jsxs as jsxs97 } from "react/jsx-runtime";
@@ -56266,8 +56281,8 @@ function ImageFillField({
56266
56281
  const fileInputRef = useRef92(null);
56267
56282
  const [uploading, setUploading] = useState71(false);
56268
56283
  const [uploadError, setUploadError] = useState71(null);
56269
- const imageAssets = useMemo55(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
56270
- const selectedAsset = useMemo55(
56284
+ const imageAssets = useMemo56(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
56285
+ const selectedAsset = useMemo56(
56271
56286
  () => resolveSelectedAsset(value, sourceFile, imageAssets),
56272
56287
  [imageAssets, sourceFile, value]
56273
56288
  );
@@ -57734,7 +57749,7 @@ import { useCallback as useCallback87, useState as useState76 } from "react";
57734
57749
  import { useShallow } from "zustand/react/shallow";
57735
57750
 
57736
57751
  // src/components/editor/AnimationCard.tsx
57737
- import { memo as memo37, useCallback as useCallback86, useEffect as useEffect82, useMemo as useMemo56, useRef as useRef95, useState as useState75 } from "react";
57752
+ import { memo as memo37, useCallback as useCallback86, useEffect as useEffect82, useMemo as useMemo57, useRef as useRef95, useState as useState75 } from "react";
57738
57753
  import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
57739
57754
 
57740
57755
  // src/components/editor/gsapAnimationHelpers.ts
@@ -58275,21 +58290,21 @@ var AnimationCard = memo37(function AnimationCard2({
58275
58290
  segment?.scrollIntoView({ block: "nearest", behavior: "smooth" });
58276
58291
  pendingAutoScrollRef.current = false;
58277
58292
  }, [expandedKfPct]);
58278
- const usedProps = useMemo56(
58293
+ const usedProps = useMemo57(
58279
58294
  () => new Set(Object.keys(animation.properties)),
58280
58295
  [animation.properties]
58281
58296
  );
58282
- const availableProps = useMemo56(
58297
+ const availableProps = useMemo57(
58283
58298
  () => SUPPORTED_PROPS.filter(
58284
58299
  (p) => !usedProps.has(p) && (animation.method === "set" || !BOOLEAN_PROPS.has(p))
58285
58300
  ),
58286
58301
  [usedProps, animation.method]
58287
58302
  );
58288
- const usedFromProps = useMemo56(
58303
+ const usedFromProps = useMemo57(
58289
58304
  () => new Set(Object.keys(animation.fromProperties ?? {})),
58290
58305
  [animation.fromProperties]
58291
58306
  );
58292
- const availableFromProps = useMemo56(
58307
+ const availableFromProps = useMemo57(
58293
58308
  () => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p) && !BOOLEAN_PROPS.has(p)),
58294
58309
  [usedFromProps]
58295
58310
  );
@@ -58336,7 +58351,7 @@ var AnimationCard = memo37(function AnimationCard2({
58336
58351
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
58337
58352
  const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
58338
58353
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
58339
- const summary = useMemo56(() => buildTweenSummary(animation), [animation]);
58354
+ const summary = useMemo57(() => buildTweenSummary(animation), [animation]);
58340
58355
  const setKeys = Object.keys(animation.properties);
58341
58356
  if (animation.method === "set" && // `every` is vacuously true on an empty bag — require at least one key so a
58342
58357
  // property-less set doesn't masquerade as a position row.
@@ -59484,7 +59499,7 @@ function PropertyPanel3dTransform({
59484
59499
  }
59485
59500
 
59486
59501
  // src/components/editor/PropertyPanelFlat.tsx
59487
- import { useEffect as useEffect103, useMemo as useMemo63, useRef as useRef112, useState as useState100 } from "react";
59502
+ import { useEffect as useEffect103, useMemo as useMemo64, useRef as useRef112, useState as useState100 } from "react";
59488
59503
  import { useShallow as useShallow2 } from "zustand/react/shallow";
59489
59504
 
59490
59505
  // src/components/editor/audioFxSummary.ts
@@ -61654,7 +61669,7 @@ function motionSectionLabel(args) {
61654
61669
  }
61655
61670
 
61656
61671
  // src/components/editor/propertyPanelAudioFxGroup.tsx
61657
- import { useMemo as useMemo59, useState as useState88 } from "react";
61672
+ import { useMemo as useMemo60, useState as useState88 } from "react";
61658
61673
  import {
61659
61674
  HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR11,
61660
61675
  HF_AUDIO_FX_DATA_KEY as HF_AUDIO_FX_DATA_KEY2,
@@ -61674,7 +61689,7 @@ import {
61674
61689
  } from "@hyperframes/core/audio-automation";
61675
61690
 
61676
61691
  // src/components/editor/propertyPanelFxSection.tsx
61677
- import { useCallback as useCallback90, useEffect as useEffect90, useMemo as useMemo58, useRef as useRef100, useState as useState86 } from "react";
61692
+ import { useCallback as useCallback90, useEffect as useEffect90, useMemo as useMemo59, useRef as useRef100, useState as useState86 } from "react";
61678
61693
 
61679
61694
  // src/components/editor/audioFxRevealTarget.ts
61680
61695
  import { parseAutomationTarget as parseAutomationTarget2 } from "@hyperframes/core/audio-automation";
@@ -62632,7 +62647,7 @@ function FxEqModule({
62632
62647
  import { getAudioFxPreset as getAudioFxPreset4 } from "@hyperframes/core/audio-fx-presets";
62633
62648
 
62634
62649
  // src/components/editor/propertyPanelFxNodeRow.tsx
62635
- import { useMemo as useMemo57, useState as useState85 } from "react";
62650
+ import { useMemo as useMemo58, useState as useState85 } from "react";
62636
62651
  import {
62637
62652
  defaultAudioFxParams as defaultAudioFxParams4,
62638
62653
  getAudioFxDef as getAudioFxDef4
@@ -63069,11 +63084,11 @@ function FxNodeRow({
63069
63084
  }) {
63070
63085
  const trackInput = useTrackDesignInput();
63071
63086
  const registryDef = getAudioFxDef4(node.type);
63072
- const def = useMemo57(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
63087
+ const def = useMemo58(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
63073
63088
  const primary = registryDef ? primaryParamOf(registryDef) : null;
63074
63089
  const profile = getAudioFxProfile2(node.type);
63075
- const derived = useMemo57(() => profileParam(node.type), [node.type]);
63076
- const onlyPrimary = useMemo57(
63090
+ const derived = useMemo58(() => profileParam(node.type), [node.type]);
63091
+ const onlyPrimary = useMemo58(
63077
63092
  () => def && primary ? { ...def, params: def.params.filter((p) => p.key === primary) } : def,
63078
63093
  [def, primary]
63079
63094
  );
@@ -63745,12 +63760,12 @@ function FxSection({
63745
63760
  [chain.nodes, mutate, onRemoveNodeAutomation, trackKind]
63746
63761
  );
63747
63762
  const [carveOpen, setCarveOpen] = useState86(true);
63748
- const carveNodes = useMemo58(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
63749
- const handBuilt = useMemo58(
63763
+ const carveNodes = useMemo59(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
63764
+ const handBuilt = useMemo59(
63750
63765
  () => chain.nodes.map((node, i) => ({ node, i })).filter(({ node }) => !node.fromCarve && !node.fromEq),
63751
63766
  [chain.nodes]
63752
63767
  );
63753
- const runs = useMemo58(() => {
63768
+ const runs = useMemo59(() => {
63754
63769
  const out = [];
63755
63770
  for (const item of handBuilt) {
63756
63771
  const preset = item.node.fromPreset;
@@ -63761,8 +63776,8 @@ function FxSection({
63761
63776
  return out;
63762
63777
  }, [handBuilt]);
63763
63778
  const [collapsedRuns, setCollapsedRuns] = useState86(/* @__PURE__ */ new Set());
63764
- const eqIds = useMemo58(() => audioEqIds(chain), [chain]);
63765
- const positions = useMemo58(() => {
63779
+ const eqIds = useMemo59(() => audioEqIds(chain), [chain]);
63780
+ const positions = useMemo59(() => {
63766
63781
  const map = /* @__PURE__ */ new Map();
63767
63782
  let at = (showCarve ? 1 : 0) + eqIds.length;
63768
63783
  for (const { i } of handBuilt) map.set(i, ++at);
@@ -64192,7 +64207,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
64192
64207
  if (cached5?.src === src) return cached5;
64193
64208
  const Ctor = window.OfflineAudioContext ?? window.webkitOfflineAudioContext;
64194
64209
  if (!Ctor) return null;
64195
- const res = await fetch(new URL(src, doc.baseURI).href);
64210
+ const res = await studioApiFetch(new URL(src, doc.baseURI).href);
64196
64211
  const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE2).decodeAudioData(
64197
64212
  await res.arrayBuffer()
64198
64213
  );
@@ -64391,7 +64406,7 @@ function AudioFxGroup({
64391
64406
  const removeNodeAutomation = (nodeId) => removeNodesAutomation([nodeId]);
64392
64407
  const [analysing, setAnalysing] = useState88(false);
64393
64408
  const storeElements = usePlayerStore((s) => s.elements);
64394
- const signalPath = useMemo59(() => {
64409
+ const signalPath = useMemo60(() => {
64395
64410
  const doc = element.element?.ownerDocument;
64396
64411
  return audioFxSignalPath(
64397
64412
  element.tagName?.toLowerCase(),
@@ -65064,7 +65079,7 @@ function createGsapLivePreview(iframeRef) {
65064
65079
  }
65065
65080
 
65066
65081
  // src/components/editor/propertyPanelFlatColorGradingSection.tsx
65067
- import { useEffect as useEffect100, useMemo as useMemo61, useRef as useRef111, useState as useState96 } from "react";
65082
+ import { useEffect as useEffect100, useMemo as useMemo62, useRef as useRef111, useState as useState96 } from "react";
65068
65083
  import {
65069
65084
  HF_COLOR_GRADING_GRADE_PRESETS,
65070
65085
  normalizeHfColorGrading as normalizeHfColorGrading6,
@@ -65099,7 +65114,7 @@ function presetPreviewHandlers({
65099
65114
  import { useState as useState92 } from "react";
65100
65115
 
65101
65116
  // src/components/editor/propertyPanelColorCurveGraph.tsx
65102
- import { useMemo as useMemo60, useRef as useRef104 } from "react";
65117
+ import { useMemo as useMemo61, useRef as useRef104 } from "react";
65103
65118
  import {
65104
65119
  compileHfColorCurve,
65105
65120
  compileHfHueCurve,
@@ -65287,8 +65302,8 @@ function CurveGraph({
65287
65302
  onCancel
65288
65303
  }) {
65289
65304
  const pointerRef = useRef104(null);
65290
- const samples = useMemo60(() => samplesFor(points, tab), [points, tab]);
65291
- const path = useMemo60(() => curvePath(samples, tab), [samples, tab]);
65305
+ const samples = useMemo61(() => samplesFor(points, tab), [points, tab]);
65306
+ const path = useMemo61(() => curvePath(samples, tab), [samples, tab]);
65292
65307
  const selectRelativePoint = (offset) => {
65293
65308
  if (points.length === 0) return;
65294
65309
  const current = selectedIndex ?? (offset > 0 ? -1 : 0);
@@ -67078,14 +67093,14 @@ function FlatColorGradingSection({
67078
67093
  const lutInputRef = useRef111(null);
67079
67094
  const [lutOpen, setLutOpen] = useState96(false);
67080
67095
  const [detailSettingsOpen, setDetailSettingsOpen] = useState96(null);
67081
- const lutAssets = useMemo61(
67096
+ const lutAssets = useMemo62(
67082
67097
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
67083
67098
  [assets]
67084
67099
  );
67085
67100
  const lut = grading.lut;
67086
67101
  const selectedLutName = lut?.src ? lut.src.split("/").pop() ?? lut.src : null;
67087
- const resolvedGrading = useMemo61(() => resolveColorGrading(grading), [grading]);
67088
- const secondaryInputGrading = useMemo61(
67102
+ const resolvedGrading = useMemo62(() => resolveColorGrading(grading), [grading]);
67103
+ const secondaryInputGrading = useMemo62(
67089
67104
  () => resolveColorGrading({
67090
67105
  intensity: 1,
67091
67106
  adjust: resolvedGrading.adjust,
@@ -67103,7 +67118,7 @@ function FlatColorGradingSection({
67103
67118
  ]
67104
67119
  );
67105
67120
  const actions = createColorGradingActions(grading, onCommitColorGrading);
67106
- const scopesRefreshKey = useMemo61(() => serializeHfColorGrading2(grading), [grading]);
67121
+ const scopesRefreshKey = useMemo62(() => serializeHfColorGrading2(grading), [grading]);
67107
67122
  useEffect100(() => {
67108
67123
  if (presetPreviews.status === "idle") onRequestPresetPreviews();
67109
67124
  }, [onRequestPresetPreviews, presetPreviews.status]);
@@ -68233,7 +68248,7 @@ function FlatEffectsSection({
68233
68248
  import { useState as useState99 } from "react";
68234
68249
 
68235
68250
  // src/hooks/useBlockCatalog.ts
68236
- import { useState as useState98, useEffect as useEffect102, useMemo as useMemo62 } from "react";
68251
+ import { useState as useState98, useEffect as useEffect102, useMemo as useMemo63 } from "react";
68237
68252
 
68238
68253
  // src/utils/blockCategories.ts
68239
68254
  import {
@@ -68291,7 +68306,7 @@ function getCategoryColors(category) {
68291
68306
  var catalogCache = null;
68292
68307
  var catalogRequest = null;
68293
68308
  function loadCatalog() {
68294
- catalogRequest ??= fetch("/api/registry/blocks").then((response) => {
68309
+ catalogRequest ??= studioApiFetch("/api/registry/blocks").then((response) => {
68295
68310
  if (!response.ok) throw new Error("Failed to load catalog");
68296
68311
  return response.json();
68297
68312
  }).then((items) => {
@@ -68332,7 +68347,7 @@ function useBlockCatalog() {
68332
68347
  cancelled = true;
68333
68348
  };
68334
68349
  }, []);
68335
- const filteredBlocks = useMemo62(() => {
68350
+ const filteredBlocks = useMemo63(() => {
68336
68351
  let result = blocks;
68337
68352
  if (category) {
68338
68353
  result = result.filter((b) => b.category === category);
@@ -68648,7 +68663,7 @@ function PropertyPanelFlat({
68648
68663
  onSetAttributeQuiet ?? onSetAttributeLive,
68649
68664
  linkedSpeed
68650
68665
  );
68651
- const audioGroupLabel = useMemo63(() => {
68666
+ const audioGroupLabel = useMemo64(() => {
68652
68667
  const doc = element.element?.ownerDocument;
68653
68668
  const id = element.id;
68654
68669
  if (!doc || !id) return void 0;
@@ -69214,12 +69229,12 @@ var PropertyPanel = memo39(function PropertyPanel2(props) {
69214
69229
  const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
69215
69230
  const iframeRef = previewIframeRef ?? { current: null };
69216
69231
  const gsapAnimIdForMemo = element ? gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null : null;
69217
- const gsapRuntimeValues = useMemo64(
69232
+ const gsapRuntimeValues = useMemo65(
69218
69233
  () => element ? readGsapRuntimeValuesForPanel(gsapAnimIdForMemo, gsapAnimations, element, iframeRef) : null,
69219
69234
  // eslint-disable-next-line react-hooks/exhaustive-deps -- iframeRef is stable; currentTime drives re-reads during playback
69220
69235
  [gsapAnimIdForMemo, gsapAnimations, element, currentTime]
69221
69236
  );
69222
- const gsapBorderRadius = useMemo64(
69237
+ const gsapBorderRadius = useMemo65(
69223
69238
  () => element ? readGsapBorderRadiusForPanel(gsapRuntimeValues, gsapAnimations, element, iframeRef) : null,
69224
69239
  // eslint-disable-next-line react-hooks/exhaustive-deps
69225
69240
  [gsapRuntimeValues, gsapAnimations, element, currentTime]
@@ -69596,11 +69611,11 @@ var PropertyPanel = memo39(function PropertyPanel2(props) {
69596
69611
  });
69597
69612
 
69598
69613
  // src/components/editor/FileTree.tsx
69599
- import { memo as memo41, useState as useState103, useCallback as useCallback92, useMemo as useMemo66, useRef as useRef115 } from "react";
69614
+ import { memo as memo41, useState as useState103, useCallback as useCallback92, useMemo as useMemo67, useRef as useRef115 } from "react";
69600
69615
  import { Plus as Plus2, FolderSimplePlus as FolderSimplePlus2 } from "@phosphor-icons/react";
69601
69616
 
69602
69617
  // src/components/editor/FileTreeNodes.tsx
69603
- import { memo as memo40, useState as useState102, useCallback as useCallback91, useMemo as useMemo65, useRef as useRef114, useEffect as useEffect104 } from "react";
69618
+ import { memo as memo40, useState as useState102, useCallback as useCallback91, useMemo as useMemo66, useRef as useRef114, useEffect as useEffect104 } from "react";
69604
69619
  import {
69605
69620
  PencilSimple,
69606
69621
  Copy,
@@ -70103,7 +70118,7 @@ var TreeFolder = memo40(function TreeFolder2({
70103
70118
  }) {
70104
70119
  const [isOpen, setIsOpen] = useState102(defaultOpen);
70105
70120
  const toggle = useCallback91(() => setIsOpen((v) => !v), []);
70106
- const children = useMemo65(() => sortChildren(node.children), [node.children]);
70121
+ const children = useMemo66(() => sortChildren(node.children), [node.children]);
70107
70122
  const Chevron = isOpen ? ChevronDown : ChevronRight;
70108
70123
  const isDragOver = dragOverFolder === node.fullPath;
70109
70124
  const isRenaming = inlineInput?.mode === "rename" && inlineInput.originalPath === node.fullPath;
@@ -70234,8 +70249,8 @@ var FileTree = memo41(function FileTree2({
70234
70249
  onImportFiles,
70235
70250
  lintFindingsByFile
70236
70251
  }) {
70237
- const tree = useMemo66(() => buildTree(files), [files]);
70238
- const children = useMemo66(() => sortChildren(tree.children), [tree]);
70252
+ const tree = useMemo67(() => buildTree(files), [files]);
70253
+ const children = useMemo67(() => sortChildren(tree.children), [tree]);
70239
70254
  const [contextMenu, setContextMenu] = useState103(null);
70240
70255
  const [inlineInput, setInlineInput] = useState103(null);
70241
70256
  const [deleteTarget, setDeleteTarget] = useState103(null);
@@ -70502,7 +70517,7 @@ var FileTree = memo41(function FileTree2({
70502
70517
  });
70503
70518
 
70504
70519
  // src/App.tsx
70505
- import { useState as useState158, useCallback as useCallback194, useRef as useRef187, useMemo as useMemo88 } from "react";
70520
+ import { useState as useState158, useCallback as useCallback194, useRef as useRef187, useMemo as useMemo89 } from "react";
70506
70521
 
70507
70522
  // src/hooks/useStableHandlers.ts
70508
70523
  import { useRef as useRef116 } from "react";
@@ -70534,9 +70549,9 @@ function useStableHandlers(value, scope) {
70534
70549
  }
70535
70550
 
70536
70551
  // src/hooks/useShellSlices.ts
70537
- import { useMemo as useMemo67 } from "react";
70552
+ import { useMemo as useMemo68 } from "react";
70538
70553
  function useHistoryFlags({ canUndo, canRedo, undoLabel, redoLabel }) {
70539
- return useMemo67(
70554
+ return useMemo68(
70540
70555
  () => ({ canUndo, canRedo, undoLabel, redoLabel }),
70541
70556
  [canUndo, canRedo, undoLabel, redoLabel]
70542
70557
  );
@@ -70552,7 +70567,7 @@ function useToolbarSession(session) {
70552
70567
  handleGsapConvertToKeyframes,
70553
70568
  handleGsapRemoveKeyframe
70554
70569
  } = session;
70555
- return useMemo67(
70570
+ return useMemo68(
70556
70571
  () => ({
70557
70572
  domEditSelection,
70558
70573
  selectedGsapAnimations,
@@ -70625,7 +70640,7 @@ function useRightPanelIntent() {
70625
70640
  }
70626
70641
 
70627
70642
  // src/components/renders/useRenderQueue.ts
70628
- import { useState as useState105, useEffect as useEffect107, useCallback as useCallback95, useRef as useRef119, useMemo as useMemo68 } from "react";
70643
+ import { useState as useState105, useEffect as useEffect107, useCallback as useCallback95, useRef as useRef119, useMemo as useMemo69 } from "react";
70629
70644
 
70630
70645
  // src/components/renders/serverError.ts
70631
70646
  async function readServerError(res) {
@@ -70665,7 +70680,7 @@ function parseStatus(body) {
70665
70680
  }
70666
70681
  async function probe() {
70667
70682
  try {
70668
- const res = await fetch("/api/environment/ffmpeg");
70683
+ const res = await studioApiFetch("/api/environment/ffmpeg");
70669
70684
  return res.ok ? parseStatus(await res.json()) : null;
70670
70685
  } catch {
70671
70686
  return null;
@@ -70889,7 +70904,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
70889
70904
  const loadRenders = useCallback95(async () => {
70890
70905
  if (!projectId) return;
70891
70906
  try {
70892
- const res = await fetch(buildProjectApiPath(projectId, `/renders`));
70907
+ const res = await studioApiFetch(buildProjectApiPath(projectId, `/renders`));
70893
70908
  if (!res.ok) {
70894
70909
  setLoadError(`Couldn't load render history (server error ${res.status}).`);
70895
70910
  return;
@@ -70976,7 +70991,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
70976
70991
  }
70977
70992
  let res;
70978
70993
  try {
70979
- res = await fetch(buildProjectApiPath(projectId, `/render`), {
70994
+ res = await studioApiFetch(buildProjectApiPath(projectId, `/render`), {
70980
70995
  method: "POST",
70981
70996
  headers: { "Content-Type": "application/json" },
70982
70997
  body: JSON.stringify(body)
@@ -71069,7 +71084,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71069
71084
  )
71070
71085
  );
71071
71086
  try {
71072
- const res = await fetch(`/api/render/${jobId}/cancel`, { method: "POST" });
71087
+ const res = await studioApiFetch(`/api/render/${jobId}/cancel`, { method: "POST" });
71073
71088
  if (!res.ok && res.status !== 404) {
71074
71089
  setActionError("Couldn't cancel on the server \u2014 the render may still be running.");
71075
71090
  return;
@@ -71091,7 +71106,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71091
71106
  setActionError(null);
71092
71107
  closeActiveEventSource(jobId);
71093
71108
  try {
71094
- const res = await fetch(`/api/render/${jobId}`, { method: "DELETE" });
71109
+ const res = await studioApiFetch(`/api/render/${jobId}`, { method: "DELETE" });
71095
71110
  if (!res.ok) {
71096
71111
  setActionError("Couldn't delete the render \u2014 it's still on disk.");
71097
71112
  return;
@@ -71148,7 +71163,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71148
71163
  };
71149
71164
  }, [projectId]);
71150
71165
  const isRendering = jobs.some((j) => j.status === "rendering");
71151
- return useMemo68(
71166
+ return useMemo69(
71152
71167
  () => ({
71153
71168
  jobs,
71154
71169
  isRendering,
@@ -71574,7 +71589,7 @@ async function captureProjectProvenance(projectId, files, compositions) {
71574
71589
  };
71575
71590
  if (!scaffolded) return;
71576
71591
  try {
71577
- const res = await fetch(
71592
+ const res = await studioApiFetch(
71578
71593
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(CONFIG_FILE)}`)
71579
71594
  );
71580
71595
  if (!res.ok) return;
@@ -72195,7 +72210,7 @@ function ProjectUnreachableBanner({ projectId }) {
72195
72210
  const [served, setServed] = useState110(null);
72196
72211
  useEffect110(() => {
72197
72212
  let cancelled = false;
72198
- fetch("/api/projects").then((res) => res.ok ? res.json() : { projects: [] }).then((data) => {
72213
+ studioApiFetch("/api/projects").then((res) => res.ok ? res.json() : { projects: [] }).then((data) => {
72199
72214
  if (!cancelled) setServed(data.projects ?? []);
72200
72215
  }).catch(() => {
72201
72216
  if (!cancelled) setServed([]);
@@ -72305,11 +72320,14 @@ function useCaptionSync(projectId) {
72305
72320
  }
72306
72321
  const seqAtSave = editSeqRef.current;
72307
72322
  const overrides = buildOverrides(state.model);
72308
- fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`), {
72309
- method: "PUT",
72310
- headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
72311
- body: JSON.stringify(overrides, null, 2)
72312
- }).then((res) => {
72323
+ studioApiFetch(
72324
+ buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
72325
+ {
72326
+ method: "PUT",
72327
+ headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
72328
+ body: JSON.stringify(overrides, null, 2)
72329
+ }
72330
+ ).then((res) => {
72313
72331
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
72314
72332
  if (editSeqRef.current === seqAtSave) pendingRef.current = false;
72315
72333
  const s = useCaptionStore.getState();
@@ -72362,7 +72380,7 @@ function useCaptionSync(projectId) {
72362
72380
  if (!pid) return;
72363
72381
  let data;
72364
72382
  try {
72365
- const res = await fetch(
72383
+ const res = await studioApiFetch(
72366
72384
  buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`)
72367
72385
  );
72368
72386
  if (!res.ok) return;
@@ -72418,13 +72436,15 @@ function useCaptionSync(projectId) {
72418
72436
  }
72419
72437
 
72420
72438
  // src/hooks/usePersistentEditHistory.ts
72421
- import { useCallback as useCallback99, useEffect as useEffect111, useMemo as useMemo69, useRef as useRef126, useState as useState111 } from "react";
72439
+ import { useCallback as useCallback99, useEffect as useEffect111, useMemo as useMemo70, useRef as useRef126, useState as useState111 } from "react";
72422
72440
  var EMPTY3 = { entries: [], back: null, forward: null };
72423
72441
  var OWN_ENTRIES_KEPT = 100;
72424
72442
  function createOwnHistory() {
72425
72443
  const own = /* @__PURE__ */ new Map();
72426
72444
  let next = null;
72427
72445
  let changes = 0;
72446
+ let claimed = { count: 0, id: "" };
72447
+ const undoneIds = /* @__PURE__ */ new Set();
72428
72448
  const remember = (id, files) => {
72429
72449
  const known = own.get(id) ?? {};
72430
72450
  for (const [path, { before, after }] of Object.entries(files)) {
@@ -72445,7 +72465,15 @@ function createOwnHistory() {
72445
72465
  if (seen === changes) next = { undo: view.back, redo: view.forward };
72446
72466
  },
72447
72467
  changes: () => changes,
72468
+ claimCount: () => claimed.count,
72469
+ claimedAfter: (count) => claimed.count > count && claimed.id ? claimed.id : null,
72470
+ targetFor: (id, pressedAt) => !id || !undoneIds.has(id) ? id : claimed.count > pressedAt ? false : null,
72471
+ noteClaim: (id) => {
72472
+ claimed = { count: claimed.count + 1, id };
72473
+ },
72448
72474
  stepped: (entry2) => {
72475
+ if (entry2.undoes) undoneIds.add(entry2.undoes);
72476
+ if (undoneIds.size > OWN_ENTRIES_KEPT) undoneIds.delete(undoneIds.values().next().value);
72449
72477
  const undone = entry2.undoes ? own.get(entry2.undoes) : void 0;
72450
72478
  if (!undone) return;
72451
72479
  const swapped = Object.entries(undone).map(([path, f]) => [
@@ -72461,14 +72489,15 @@ function createOwnHistory() {
72461
72489
  const paths = files ? Object.keys(files) : [];
72462
72490
  if (!files || paths.length !== step.paths.length || !paths.every((p) => step.paths.includes(p)))
72463
72491
  return null;
72464
- const restore = paths.map((path) => [
72492
+ const restore2 = paths.map((path) => [
72465
72493
  path,
72466
72494
  { previous: files[path].after, restored: files[path].before }
72467
72495
  ]);
72468
- return { id: step.id, files: Object.fromEntries(restore) };
72496
+ return { id: step.id, files: Object.fromEntries(restore2) };
72469
72497
  },
72470
72498
  clear: () => {
72471
72499
  own.clear();
72500
+ undoneIds.clear();
72472
72501
  next = null;
72473
72502
  changes += 1;
72474
72503
  }
@@ -72478,8 +72507,14 @@ var DEFAULT_COALESCE_MS = 300;
72478
72507
  function historyUrl(projectId, path = "") {
72479
72508
  return `/api/projects/${encodeURIComponent(projectId)}/history${path}`;
72480
72509
  }
72510
+ async function cantUndo(paths) {
72511
+ return { ok: false, reason: "content-mismatch", paths: paths ?? [] };
72512
+ }
72513
+ function unionPaths(...lists) {
72514
+ return [...new Set(lists.flatMap((list) => list ?? []))];
72515
+ }
72481
72516
  async function post(url, body, headers = {}) {
72482
- const response = await fetch(url, {
72517
+ const response = await studioApiFetch(url, {
72483
72518
  method: "POST",
72484
72519
  headers: { "Content-Type": "application/json", ...headers },
72485
72520
  body: JSON.stringify(body)
@@ -72538,9 +72573,10 @@ function usePersistentEditHistory({ projectId }) {
72538
72573
  const refresh = useCallback99(async () => {
72539
72574
  if (!projectId) return;
72540
72575
  const seen = own.changes();
72541
- const response = await fetch(historyUrl(projectId)).catch(() => null);
72542
- const next = response?.ok ? await response.json() : EMPTY3;
72576
+ const response = await studioApiFetch(historyUrl(projectId)).catch(() => null);
72577
+ const next = response?.ok ? await response.json().catch(() => null) : EMPTY3;
72543
72578
  if (projectIdRef.current !== projectId) return;
72579
+ if (!next) return console.error("The history's reply was unreadable.");
72544
72580
  setView(next);
72545
72581
  own.offered(seen, next);
72546
72582
  }, [projectId, own]);
@@ -72552,9 +72588,9 @@ function usePersistentEditHistory({ projectId }) {
72552
72588
  void refresh().finally(() => setLoaded(true));
72553
72589
  }, [refresh, own]);
72554
72590
  const recordEdit = useCallback99(
72555
- async ({ label, coalesceKey, coalesceMs, files }) => {
72591
+ async ({ label, coalesceKey, coalesceMs, files, created = [] }) => {
72556
72592
  if (!projectId) return;
72557
- const paths = Object.keys(files);
72593
+ const paths = [...Object.keys(files), ...created];
72558
72594
  own.overtake();
72559
72595
  const reply = await post(historyUrl(projectId, "/claim"), {
72560
72596
  label,
@@ -72563,7 +72599,10 @@ function usePersistentEditHistory({ projectId }) {
72563
72599
  ...coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }
72564
72600
  });
72565
72601
  const claimed = claimHeld(reply, label);
72566
- if (claimed) own.remember(claimed, files);
72602
+ if (claimed) {
72603
+ own.remember(claimed, files);
72604
+ own.noteClaim(claimed);
72605
+ }
72567
72606
  heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
72568
72607
  await refresh();
72569
72608
  },
@@ -72572,14 +72611,17 @@ function usePersistentEditHistory({ projectId }) {
72572
72611
  const step = useCallback99(
72573
72612
  async (direction, callbacks) => {
72574
72613
  if (!projectId) return { ok: false, reason: "empty" };
72614
+ const candidate = direction === "undo" && callbacks.claimedAfter !== void 0 ? own.claimedAfter(callbacks.claimedAfter) : null;
72575
72615
  const next = direction === "undo" ? view.back : view.forward;
72576
- const paths = [.../* @__PURE__ */ new Set([...next?.paths ?? [], ...heldClaimRef.current?.paths ?? []])];
72616
+ const stepPaths = candidate ? Object.keys(own.afterOf(candidate)) : next?.paths;
72617
+ const paths = unionPaths(stepPaths, heldClaimRef.current?.paths);
72618
+ const pressedAt = own.claimCount();
72577
72619
  own.overtake();
72578
- const run = async () => {
72620
+ const attempt = async (target) => {
72579
72621
  const previous = await readAll(paths, callbacks.readFile);
72580
72622
  const posted = await post(
72581
- historyUrl(projectId, "/step"),
72582
- { direction: direction === "undo" ? "back" : "forward" },
72623
+ historyUrl(projectId, target ? "/undo" : "/step"),
72624
+ target ? { entryId: target } : { direction: direction === "undo" ? "back" : "forward" },
72583
72625
  studioWriteHeaders()
72584
72626
  );
72585
72627
  heldClaimRef.current = null;
@@ -72606,6 +72648,10 @@ function usePersistentEditHistory({ projectId }) {
72606
72648
  )
72607
72649
  };
72608
72650
  };
72651
+ const run = () => {
72652
+ const target = own.targetFor(candidate, pressedAt);
72653
+ return target === false ? cantUndo(stepPaths) : attempt(target);
72654
+ };
72609
72655
  return callbacks.serialize ? callbacks.serialize(paths, run) : run();
72610
72656
  },
72611
72657
  [projectId, view, refresh, own]
@@ -72616,7 +72662,7 @@ function usePersistentEditHistory({ projectId }) {
72616
72662
  }, [own, refresh]);
72617
72663
  const undo = useCallback99((callbacks) => step("undo", callbacks), [step]);
72618
72664
  const redo = useCallback99((callbacks) => step("redo", callbacks), [step]);
72619
- const state = useMemo69(() => {
72665
+ const state = useMemo70(() => {
72620
72666
  const backAt = Math.max(view.back?.endedAt ?? 0, heldClaimRef.current?.at ?? 0);
72621
72667
  const redoAt = redoneAt(view);
72622
72668
  return {
@@ -72637,6 +72683,7 @@ function usePersistentEditHistory({ projectId }) {
72637
72683
  undo,
72638
72684
  redo,
72639
72685
  predict: own.predict,
72686
+ claims: own.claimCount,
72640
72687
  noteOutsideChange
72641
72688
  };
72642
72689
  }
@@ -72701,7 +72748,7 @@ function usePanelLayout(initialState2) {
72701
72748
  import { useState as useState113, useCallback as useCallback104, useRef as useRef130 } from "react";
72702
72749
 
72703
72750
  // src/hooks/useFileTree.ts
72704
- import { useState as useState112, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo70, useRef as useRef128 } from "react";
72751
+ import { useState as useState112, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo71, useRef as useRef128 } from "react";
72705
72752
  var EMPTY_FILE_LIST = [];
72706
72753
  function useFileTree({ projectId, projectIdRef }) {
72707
72754
  const [fetched, setFetched] = useState112(null);
@@ -72714,7 +72761,7 @@ function useFileTree({ projectId, projectIdRef }) {
72714
72761
  if (!projectId) return;
72715
72762
  let cancelled = false;
72716
72763
  setFetched({ projectId, loaded: false, fileTree: [], compositionPaths: [], projectDir: null });
72717
- fetch(buildProjectApiPath(projectId)).then((r) => {
72764
+ studioApiFetch(buildProjectApiPath(projectId)).then((r) => {
72718
72765
  if (!r.ok) throw new Error(`tree fetch failed: ${r.status}`);
72719
72766
  return r.json();
72720
72767
  }).then((data) => {
@@ -72747,7 +72794,7 @@ function useFileTree({ projectId, projectIdRef }) {
72747
72794
  refreshAbortRef.current = controller;
72748
72795
  let data;
72749
72796
  try {
72750
- const res = await fetch(buildProjectApiPath(pid), { signal: controller.signal });
72797
+ const res = await studioApiFetch(buildProjectApiPath(pid), { signal: controller.signal });
72751
72798
  data = await res.json();
72752
72799
  } catch (err) {
72753
72800
  if (err instanceof DOMException && err.name === "AbortError") return;
@@ -72765,11 +72812,11 @@ function useFileTree({ projectId, projectIdRef }) {
72765
72812
  );
72766
72813
  }
72767
72814
  }, [projectIdRef]);
72768
- const assets = useMemo70(
72815
+ const assets = useMemo71(
72769
72816
  () => fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
72770
72817
  [fileTree]
72771
72818
  );
72772
- const fontAssets = useMemo70(
72819
+ const fontAssets = useMemo71(
72773
72820
  () => (projectId ? assets : []).filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
72774
72821
  family: fontFamilyFromAssetPath(asset),
72775
72822
  path: asset,
@@ -72913,9 +72960,18 @@ function useEditorSave({
72913
72960
  }
72914
72961
 
72915
72962
  // src/hooks/useProjectFileWriter.ts
72916
- import { useCallback as useCallback103, useMemo as useMemo71 } from "react";
72963
+ import { useCallback as useCallback103, useMemo as useMemo72 } from "react";
72964
+ async function fetchProjectFile(projectId, path, query) {
72965
+ if (!projectId) throw new Error("No active project");
72966
+ const response = await studioApiFetch(
72967
+ `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}${query}`
72968
+ );
72969
+ if (!response.ok) throw new Error(`Failed to read ${path}`);
72970
+ const data = await response.json();
72971
+ return { content: data.content, version: data.version ?? response.headers.get("etag") };
72972
+ }
72917
72973
  function useProjectFileWriter({ projectId }) {
72918
- const fileVersionScope = useMemo71(
72974
+ const fileVersionScope = useMemo72(
72919
72975
  () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
72920
72976
  [projectId]
72921
72977
  );
@@ -72928,28 +72984,18 @@ function useProjectFileWriter({ projectId }) {
72928
72984
  );
72929
72985
  const readProjectFile = useCallback103(
72930
72986
  async (path) => {
72931
- if (!projectId) throw new Error("No active project");
72932
- const response = await fetch(
72933
- `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`
72934
- );
72935
- if (!response.ok) throw new Error(`Failed to read ${path}`);
72936
- const data = await response.json();
72937
- if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
72938
- fileVersions.set(path, data.version ?? response.headers.get("etag"));
72939
- return data.content;
72987
+ const { content, version } = await fetchProjectFile(projectId, path, "");
72988
+ if (typeof content !== "string") throw new Error(`Missing file contents for ${path}`);
72989
+ fileVersions.set(path, version);
72990
+ return content;
72940
72991
  },
72941
72992
  [fileVersions, projectId]
72942
72993
  );
72943
72994
  const readOptionalProjectFile = useCallback103(
72944
72995
  async (path) => {
72945
- if (!projectId) throw new Error("No active project");
72946
- const response = await fetch(
72947
- `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`
72948
- );
72949
- if (!response.ok) throw new Error(`Failed to read ${path}`);
72950
- const data = await response.json();
72951
- fileVersions.set(path, data.version ?? response.headers.get("etag"));
72952
- return typeof data.content === "string" ? data.content : "";
72996
+ const { content, version } = await fetchProjectFile(projectId, path, "?optional=1");
72997
+ fileVersions.set(path, version);
72998
+ return typeof content === "string" ? content : "";
72953
72999
  },
72954
73000
  [fileVersions, projectId]
72955
73001
  );
@@ -72959,7 +73005,7 @@ function useProjectFileWriter({ projectId }) {
72959
73005
  const writeProjectId = projectId;
72960
73006
  let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
72961
73007
  if (expectedVersion === void 0) {
72962
- const preflight = await fetch(
73008
+ const preflight = await studioApiFetch(
72963
73009
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`
72964
73010
  );
72965
73011
  if (preflight.ok) {
@@ -72979,7 +73025,7 @@ function useProjectFileWriter({ projectId }) {
72979
73025
  await retryStudioSave(async () => {
72980
73026
  let response;
72981
73027
  try {
72982
- response = await fetch(
73028
+ response = await studioApiFetch(
72983
73029
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
72984
73030
  {
72985
73031
  method: "PUT",
@@ -73020,7 +73066,7 @@ function useProjectFileWriter({ projectId }) {
73020
73066
  },
73021
73067
  [fileVersions, projectId]
73022
73068
  );
73023
- return useMemo71(
73069
+ return useMemo72(
73024
73070
  () => ({
73025
73071
  fileVersions,
73026
73072
  readProjectFile,
@@ -73124,7 +73170,7 @@ function useFileManager({
73124
73170
  setEditingFile({ path, content: null });
73125
73171
  return;
73126
73172
  }
73127
- fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`).then((r) => {
73173
+ studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`).then((r) => {
73128
73174
  if (!r.ok) throw new Error(`Failed to load ${path} (${r.status})`);
73129
73175
  return r.json();
73130
73176
  }).then((data) => {
@@ -73152,9 +73198,12 @@ function useFileManager({
73152
73198
  const requestId = ++revealRequestIdRef.current;
73153
73199
  const controller = new AbortController();
73154
73200
  revealAbortRef.current = controller;
73155
- fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`, {
73156
- signal: controller.signal
73157
- }).then((r) => r.json()).then((data) => {
73201
+ studioApiFetch(
73202
+ `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`,
73203
+ {
73204
+ signal: controller.signal
73205
+ }
73206
+ ).then((r) => r.json()).then((data) => {
73158
73207
  if (requestId !== revealRequestIdRef.current) return;
73159
73208
  if (data.content != null) {
73160
73209
  fileVersions.set(sourceFile, data.version ?? null);
@@ -73178,7 +73227,7 @@ function useFileManager({
73178
73227
  }
73179
73228
  const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
73180
73229
  try {
73181
- const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
73230
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
73182
73231
  method: "POST",
73183
73232
  body: formData
73184
73233
  });
@@ -73218,7 +73267,7 @@ function useFileManager({
73218
73267
  if (path.endsWith(".html")) {
73219
73268
  content = '<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n</head>\n<body>\n\n</body>\n</html>\n';
73220
73269
  }
73221
- const res = await fetch(
73270
+ const res = await studioApiFetch(
73222
73271
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
73223
73272
  {
73224
73273
  method: "POST",
@@ -73241,7 +73290,7 @@ function useFileManager({
73241
73290
  async (path) => {
73242
73291
  const pid = projectIdRef.current;
73243
73292
  if (!pid) return;
73244
- const res = await fetch(
73293
+ const res = await studioApiFetch(
73245
73294
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path + "/.gitkeep")}`,
73246
73295
  {
73247
73296
  method: "POST",
@@ -73263,7 +73312,7 @@ function useFileManager({
73263
73312
  async (path) => {
73264
73313
  const pid = projectIdRef.current;
73265
73314
  if (!pid) return;
73266
- const res = await fetch(
73315
+ const res = await studioApiFetch(
73267
73316
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
73268
73317
  {
73269
73318
  method: "DELETE"
@@ -73284,7 +73333,7 @@ function useFileManager({
73284
73333
  async (oldPath, newPath) => {
73285
73334
  const pid = projectIdRef.current;
73286
73335
  if (!pid) return;
73287
- const res = await fetch(
73336
+ const res = await studioApiFetch(
73288
73337
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(oldPath)}`,
73289
73338
  {
73290
73339
  method: "PATCH",
@@ -73310,7 +73359,7 @@ function useFileManager({
73310
73359
  async (path) => {
73311
73360
  const pid = projectIdRef.current;
73312
73361
  if (!pid) return;
73313
- const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
73362
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
73314
73363
  method: "POST",
73315
73364
  headers: { "Content-Type": "application/json" },
73316
73365
  body: JSON.stringify({ path })
@@ -73619,18 +73668,17 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
73619
73668
  const active = files[activeDocPath];
73620
73669
  const restoredScript = active ? extractGsapScriptText(active.restored) : null;
73621
73670
  const previousScript = active ? extractGsapScriptText(active.previous) : null;
73622
- const reparse = plan.scripted ? plan.targets.map(({ live }) => live).filter((el) => "_gsap" in el) : [];
73623
- if (reparse.length && !restoredScript) {
73671
+ const reparse = plan.scripted && plan.targets.some(({ live }) => "_gsap" in live);
73672
+ if (reparse && !restoredScript) {
73624
73673
  reloadPreview();
73625
73674
  return "full";
73626
73675
  }
73627
- if (restoredScript && (restoredScript !== previousScript || reparse.length)) {
73676
+ if (restoredScript && (restoredScript !== previousScript || reparse)) {
73628
73677
  syncStaleEditMarks(doc, active);
73629
73678
  const result = applySoftReload(iframe, restoredScript, {
73630
73679
  onAsyncFailure: reloadPreview,
73631
73680
  currentTimeOverride: currentTime,
73632
73681
  authoredHtml: active.restored,
73633
- reparse,
73634
73682
  nestedFiles
73635
73683
  });
73636
73684
  if (result === "cannot-soft-reload") {
@@ -73799,8 +73847,9 @@ function usePreviewPersistence({
73799
73847
  if (result.status !== "clean") throw result.error;
73800
73848
  }, [drainPendingDomEditSaves]);
73801
73849
  const settlePendingEdits = useCallback105(async () => {
73802
- await flushStudioPendingEdits();
73803
- await domEditSaveQueueRef.current?.waitForIdle();
73850
+ const queued = domEditSaveQueueRef.current?.waitForIdle();
73851
+ await flushStudioPendingEdits({ onlyCurrent: true });
73852
+ await queued;
73804
73853
  }, []);
73805
73854
  const resetDomEditSaveQueueBreaker = useCallback105(() => {
73806
73855
  domEditSaveQueueRef.current?.reset();
@@ -73825,20 +73874,20 @@ function usePreviewPersistence({
73825
73874
  );
73826
73875
  applyStudioManualEditsToPreviewRef.current = applyStudioManualEditsToPreview;
73827
73876
  const syncHistoryPreviewAfterApply = useCallback105(
73828
- async (restore) => {
73877
+ async (restore2) => {
73829
73878
  const projectId = usePlayerStore.getState().timelineProjectId;
73830
73879
  const nestedFiles = await settleNestedReads(
73831
- projectId ? readUndoNestedFiles(
73880
+ readUndoNestedFiles(
73832
73881
  previewIframeRef.current,
73833
73882
  activeCompPathRef.current,
73834
- restore.files,
73835
- (path) => readProjectFileContent(projectId, path)
73836
- ) : null
73883
+ restore2.files,
73884
+ (path) => projectId ? readProjectFileContent(projectId, path) : Promise.reject(new Error("No project is open to read nested files from."))
73885
+ )
73837
73886
  );
73838
73887
  const strategy = applyUndoRestoreToPreview(
73839
73888
  previewIframeRef.current,
73840
73889
  activeCompPathRef.current,
73841
- restore.files,
73890
+ restore2.files,
73842
73891
  usePlayerStore.getState().currentTime,
73843
73892
  reloadPreview,
73844
73893
  nestedFiles
@@ -73952,7 +74001,7 @@ function buildAtomicCutIntents(elements, splitTime, activeCompPath) {
73952
74001
  return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
73953
74002
  }
73954
74003
  async function readFileVersion(projectId, path) {
73955
- const response = await fetch(
74004
+ const response = await studioApiFetch(
73956
74005
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(path)}`)
73957
74006
  );
73958
74007
  if (!response.ok) throw new Error(`Failed to read ${path} before cut (${response.status})`);
@@ -73971,14 +74020,17 @@ async function requestAtomicCut(projectId, intents) {
73971
74020
  }
73972
74021
  const transactionToken = `cut:${crypto.randomUUID()}`;
73973
74022
  markStudioWriteToken(transactionToken);
73974
- const response = await fetch(buildProjectApiPath(projectId, "/file-mutations/split-batch"), {
73975
- method: "POST",
73976
- headers: {
73977
- "Content-Type": "application/json",
73978
- "X-Hyperframes-Write-Token": transactionToken
73979
- },
73980
- body: JSON.stringify({ files, transactionToken })
73981
- });
74023
+ const response = await studioApiFetch(
74024
+ buildProjectApiPath(projectId, "/file-mutations/split-batch"),
74025
+ {
74026
+ method: "POST",
74027
+ headers: {
74028
+ "Content-Type": "application/json",
74029
+ "X-Hyperframes-Write-Token": transactionToken
74030
+ },
74031
+ body: JSON.stringify({ files, transactionToken })
74032
+ }
74033
+ );
73982
74034
  const body = await response.json().catch(() => null);
73983
74035
  if (!response.ok || body?.ok !== true || !Array.isArray(body.files)) {
73984
74036
  const prefix = response.status === 409 ? "Cut conflict" : "Cut failed";
@@ -74039,14 +74091,14 @@ import { useCallback as useCallback107, useRef as useRef133 } from "react";
74039
74091
  function isFreezeFrameResponse(value) {
74040
74092
  if (typeof value !== "object" || value === null) return false;
74041
74093
  const body = value;
74042
- return typeof body.before === "string" && typeof body.after === "string" && typeof body.version === "string";
74094
+ return typeof body.before === "string" && typeof body.after === "string" && typeof body.version === "string" && typeof body.stillPath === "string";
74043
74095
  }
74044
74096
  function errorOf(value) {
74045
74097
  if (typeof value !== "object" || value === null || !("error" in value)) return null;
74046
74098
  return typeof value.error === "string" ? value.error : null;
74047
74099
  }
74048
74100
  async function readVersion(projectId, path) {
74049
- const response = await fetch(
74101
+ const response = await studioApiFetch(
74050
74102
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(path)}`)
74051
74103
  );
74052
74104
  const body = await response.json().catch(() => null);
@@ -74060,7 +74112,7 @@ async function requestFreezeFrame(input) {
74060
74112
  const expectedVersion = await readVersion(input.projectId, input.path);
74061
74113
  const transactionToken = `freeze:${crypto.randomUUID()}`;
74062
74114
  markStudioWriteToken(transactionToken);
74063
- const response = await fetch(
74115
+ const response = await studioApiFetch(
74064
74116
  buildProjectApiPath(input.projectId, "/file-mutations/freeze-frame"),
74065
74117
  {
74066
74118
  method: "POST",
@@ -74100,7 +74152,8 @@ function useFreezeFrame(options) {
74100
74152
  const result = await requestFreezeFrame({ projectId, path, element, playhead });
74101
74153
  await opts.recordEdit({
74102
74154
  label: "Freeze frame",
74103
- files: { [path]: { before: result.before, after: result.after } }
74155
+ files: { [path]: { before: result.before, after: result.after } },
74156
+ created: [result.stillPath]
74104
74157
  });
74105
74158
  opts.observeProjectFileVersion?.(path, result.version);
74106
74159
  });
@@ -74315,7 +74368,7 @@ function resolveDropTrack(input) {
74315
74368
 
74316
74369
  // src/utils/timelineCompositionInsert.ts
74317
74370
  async function insertTimelineComposition(input) {
74318
- const current = await fetch(
74371
+ const current = await studioApiFetch(
74319
74372
  buildProjectApiPath(input.projectId, `/files/${encodeURIComponent(input.targetPath)}`)
74320
74373
  );
74321
74374
  if (!current.ok) {
@@ -74323,7 +74376,7 @@ async function insertTimelineComposition(input) {
74323
74376
  }
74324
74377
  const snapshot3 = await current.json();
74325
74378
  if (typeof snapshot3.version !== "string") throw new Error("Missing composition file version");
74326
- const response = await fetch(
74379
+ const response = await studioApiFetch(
74327
74380
  buildProjectApiPath(
74328
74381
  input.projectId,
74329
74382
  `/file-mutations/insert-composition/${encodeURIComponent(input.targetPath)}`
@@ -74656,10 +74709,10 @@ function useTimelineEditRefusal(canEdit, showToast) {
74656
74709
  );
74657
74710
  }
74658
74711
  function useTimelineEditGate(canEdit, showToast) {
74659
- const refuse = useTimelineEditRefusal(canEdit, showToast);
74712
+ const refuse2 = useTimelineEditRefusal(canEdit, showToast);
74660
74713
  return useCallback110(
74661
- (targets) => refuse(targets) === null,
74662
- [refuse]
74714
+ (targets) => refuse2(targets) === null,
74715
+ [refuse2]
74663
74716
  );
74664
74717
  }
74665
74718
 
@@ -75261,7 +75314,7 @@ function useTimelineDeleteOps({
75261
75314
  }
75262
75315
  return patchTarget;
75263
75316
  });
75264
- const removeResponse = await fetch(
75317
+ const removeResponse = await studioApiFetch(
75265
75318
  buildProjectApiPath(
75266
75319
  pid,
75267
75320
  `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`
@@ -75892,11 +75945,11 @@ function assertAudioGroupEditable(checkEditable, elements) {
75892
75945
  throw new Error("Timeline edit blocked");
75893
75946
  }
75894
75947
  }
75895
- function patchLiveAudioGroupState(iframe, elements, groupId, activeCompPath, restore) {
75948
+ function patchLiveAudioGroupState(iframe, elements, groupId, activeCompPath, restore2) {
75896
75949
  for (const element of elements) {
75897
75950
  const target = findTimelineElementInIframe(iframe, element, activeCompPath);
75898
75951
  if (!target) continue;
75899
- const next = restore ? restore.get(element) ?? null : groupId;
75952
+ const next = restore2 ? restore2.get(element) ?? null : groupId;
75900
75953
  if (next) target.setAttribute(HF_AUDIO_GROUP_ATTR5, next);
75901
75954
  else target.removeAttribute(HF_AUDIO_GROUP_ATTR5);
75902
75955
  }
@@ -76896,9 +76949,9 @@ function useTimelineEditing({
76896
76949
  handleTimelineGroupResize: track2(
76897
76950
  guard((changes) => changes.map((c) => c.element), groupEditing.handleTimelineGroupResize)
76898
76951
  ),
76899
- restoreLiveLanes: (restore) => {
76900
- setElementFxAttribute.restoreLive(restore);
76901
- setAudioGroupAttribute2.restoreLive(restore);
76952
+ restoreLiveLanes: (restore2) => {
76953
+ setElementFxAttribute.restoreLive(restore2);
76954
+ setAudioGroupAttribute2.restoreLive(restore2);
76902
76955
  }
76903
76956
  };
76904
76957
  return useStableHandlers(editing, projectId);
@@ -76958,7 +77011,7 @@ function useAskAgentModal({
76958
77011
  if (!pid) return;
76959
77012
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
76960
77013
  try {
76961
- const response = await fetch(
77014
+ const response = await studioApiFetch(
76962
77015
  buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`)
76963
77016
  );
76964
77017
  if (!response.ok) return;
@@ -78014,7 +78067,7 @@ async function assignGsapTargetAutoIdIfNeeded({
78014
78067
  showToast?.("Couldn't assign element id because the patch contains invalid values", "error");
78015
78068
  return false;
78016
78069
  }
78017
- const res = await fetch(
78070
+ const res = await studioApiFetch(
78018
78071
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
78019
78072
  {
78020
78073
  method: "POST",
@@ -78080,7 +78133,7 @@ function isAtomicElementPatchFile(value) {
78080
78133
  async function patchElementBatches(projectId, batches) {
78081
78134
  const body = JSON.stringify({ batches });
78082
78135
  try {
78083
- const response = await fetch(
78136
+ const response = await studioApiFetch(
78084
78137
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
78085
78138
  {
78086
78139
  method: "POST",
@@ -78120,7 +78173,7 @@ function batchesAreInlineStyleOnly(batches) {
78120
78173
  );
78121
78174
  }
78122
78175
  async function postPatchElement(projectId, targetPath, body, showToast) {
78123
- const response = await fetch(
78176
+ const response = await studioApiFetch(
78124
78177
  buildProjectApiPath(
78125
78178
  projectId,
78126
78179
  `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`
@@ -78418,7 +78471,13 @@ var IMAGE_BACKGROUND_FIT = [
78418
78471
  ["background-repeat", "no-repeat"],
78419
78472
  ["background-size", "contain"]
78420
78473
  ];
78421
- async function commitDomStyles(context, selection, styles) {
78474
+ async function commitDomStyles(context, selection, stylesInEitherCase) {
78475
+ const styles = Object.fromEntries(
78476
+ Object.entries(stylesInEitherCase).map(([property, value]) => [
78477
+ cssPropertyName(property),
78478
+ value
78479
+ ])
78480
+ );
78422
78481
  const entries = Object.entries(styles);
78423
78482
  if (entries.length === 0) return domEditCommitDeclined("no-selection");
78424
78483
  if (entries.some(([property]) => isManualGeometryStyleProperty(property)))
@@ -78855,7 +78914,7 @@ function useDomEditTextCommits({
78855
78914
  persistDomEditOperations
78856
78915
  });
78857
78916
  const handleDomStyleCommitForSelection = useCallback130(
78858
- (selection, property, value) => commitDomStyles(
78917
+ (selection, propertyOrStylesAsOnePatch, value = "") => commitDomStyles(
78859
78918
  {
78860
78919
  activeCompPath,
78861
78920
  previewIframeRef,
@@ -78866,7 +78925,7 @@ function useDomEditTextCommits({
78866
78925
  resync: refreshDomEditSelectionFromPreview
78867
78926
  },
78868
78927
  selection,
78869
- { [property]: value }
78928
+ typeof propertyOrStylesAsOnePatch === "string" ? { [propertyOrStylesAsOnePatch]: value } : propertyOrStylesAsOnePatch
78870
78929
  ),
78871
78930
  [
78872
78931
  activeCompPath,
@@ -78953,24 +79012,24 @@ function useDomEditTextCommits({
78953
79012
  if (element.isConnected && element.innerHTML === html2) element.innerHTML = previousHtml;
78954
79013
  };
78955
79014
  if (latestReadOnlyPreviewRef.current) return putBack();
78956
- const refuse = (reason) => {
79015
+ const refuse2 = (reason) => {
78957
79016
  console.error("[Studio] text edit not saved:", reason, element);
78958
79017
  showToast(`Couldn't save the text edit: ${reason}`, "error");
78959
79018
  putBack();
78960
79019
  };
78961
79020
  const doc = previewIframeRef.current?.contentDocument;
78962
79021
  if (!doc || !element.isConnected || element.ownerDocument !== doc) {
78963
- return refuse("the text's element is gone from the preview");
79022
+ return refuse2("the text's element is gone from the preview");
78964
79023
  }
78965
79024
  const selection = await buildDomSelectionFromTarget(element, {
78966
79025
  exactTarget: true,
78967
79026
  skipSourceProbe: true
78968
79027
  });
78969
79028
  if (selection?.element !== element) {
78970
- return refuse("this text was not found in the composition's source");
79029
+ return refuse2("this text was not found in the composition's source");
78971
79030
  }
78972
79031
  if (!canCommitInlineTextSelection(selection, element)) {
78973
- return refuse("this text can't be edited in place");
79032
+ return refuse2("this text can't be edited in place");
78974
79033
  }
78975
79034
  const isLatestTextCommit = bumpDomEditCommitMapVersion(
78976
79035
  domTextCommitVersionRef.current,
@@ -79229,18 +79288,18 @@ var ELEMENT_OFFSET_REFUSED = {
79229
79288
  anchored: "This layer is anchored from its right or bottom edge. Move it in the Code tab.",
79230
79289
  percent: "This layer's position is set in percent. Move it in the Code tab."
79231
79290
  };
79232
- function gsapOf(el) {
79291
+ function gsapOf2(el) {
79233
79292
  return el.ownerDocument.defaultView?.gsap;
79234
79293
  }
79235
79294
  function settleGsapDraftAtGestureStart(el) {
79236
- const gsap3 = gsapOf(el);
79295
+ const gsap3 = gsapOf2(el);
79237
79296
  const x = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-x") ?? "");
79238
79297
  const y = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
79239
79298
  if (gsap3 && Number.isFinite(x) && Number.isFinite(y)) gsap3.set(el, { x, y });
79240
79299
  }
79241
79300
  function refuseGsapTakeover(el, showToast) {
79242
79301
  if (!gsapHoldsTranslate(el)) return;
79243
- gsapOf(el)?.set(el, { x: 0, y: 0, xPercent: 0, yPercent: 0 });
79302
+ gsapOf2(el)?.set(el, { x: 0, y: 0, xPercent: 0, yPercent: 0 });
79244
79303
  showToast(GSAP_TOOK_OVER, "error");
79245
79304
  throw markStudioSaveErrorAlreadyToasted(new Error(GSAP_TOOK_OVER));
79246
79305
  }
@@ -79299,7 +79358,11 @@ function savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selec
79299
79358
  const before = captureStudioRotation(element);
79300
79359
  const patches = element.hasAttribute(STUDIO_ROTATION_ATTR) ? buildClearRotationPatches(element) : [];
79301
79360
  if (patches.length) clearStudioRotation(element);
79302
- const drawn = applyCssRotation(element, next.angle, patches.length ? void 0 : next.plain);
79361
+ const drawn = applyCssRotation(
79362
+ element,
79363
+ next.angle,
79364
+ patches.length ? void 0 : next.plain ?? void 0
79365
+ );
79303
79366
  patches.push(...drawn);
79304
79367
  const turn = drawn.at(-1);
79305
79368
  return commitPositionPatchToHtml(selection, patches, {
@@ -79331,7 +79394,7 @@ function useDomGeometryCommits({
79331
79394
  [commitPositionPatchToHtml, readOnlyPreview, showToast]
79332
79395
  );
79333
79396
  const handleDomBoxSizeCommit = useCallback131(
79334
- (selection, next, offset, restore, undoKey) => {
79397
+ (selection, next, offset, restore2, undoKey) => {
79335
79398
  if (readOnlyPreview) return Promise.resolve(void 0);
79336
79399
  const element = selection.element;
79337
79400
  const beforeSize = captureStudioBoxSize(element);
@@ -79349,7 +79412,7 @@ function useDomGeometryCommits({
79349
79412
  restoreStudioBoxSize(element, beforeSize);
79350
79413
  if (offset) restoreStudioPathOffset(element, beforeOffset);
79351
79414
  crop?.revert();
79352
- restore?.();
79415
+ restore2?.();
79353
79416
  throw error;
79354
79417
  });
79355
79418
  },
@@ -79672,7 +79735,7 @@ async function commitStructuralMutation(pid, targetPath, route, body, label, dep
79672
79735
  writeFile: deps.writeProjectFile,
79673
79736
  recordEdit: deps.editHistory.recordEdit,
79674
79737
  rewrite: async (originalContent) => {
79675
- const mutateResponse = await fetch(
79738
+ const mutateResponse = await studioApiFetch(
79676
79739
  buildProjectApiPath(pid, `/file-mutations/${route}/${encodeURIComponent(targetPath)}`),
79677
79740
  {
79678
79741
  method: "POST",
@@ -79749,7 +79812,7 @@ function useGroupCommits(params) {
79749
79812
  }
79750
79813
 
79751
79814
  // src/hooks/useGsapScriptCommits.ts
79752
- import { useCallback as useCallback139, useMemo as useMemo72, useRef as useRef148 } from "react";
79815
+ import { useCallback as useCallback139, useMemo as useMemo73, useRef as useRef148 } from "react";
79753
79816
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
79754
79817
 
79755
79818
  // src/hooks/gsapRuntimePatch.ts
@@ -79943,6 +80006,11 @@ function createKeyedSerializer() {
79943
80006
  };
79944
80007
  }
79945
80008
 
80009
+ // src/hooks/gsapScriptCommitTypes.ts
80010
+ function gsapWriteFile(selection, activeCompPath) {
80011
+ return selection.sourceFile || activeCompPath || "index.html";
80012
+ }
80013
+
79946
80014
  // src/hooks/useGsapAnimationOps.ts
79947
80015
  import { useCallback as useCallback134 } from "react";
79948
80016
  function useGsapAnimationOps({
@@ -80862,7 +80930,7 @@ function useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToa
80862
80930
 
80863
80931
  // src/hooks/useGsapScriptCommits.ts
80864
80932
  async function mutateGsapScript(projectId, sourceFile, mutation) {
80865
- const res = await fetch(
80933
+ const res = await studioApiFetch(
80866
80934
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
80867
80935
  {
80868
80936
  method: "POST",
@@ -80876,7 +80944,7 @@ async function mutateGsapScript(projectId, sourceFile, mutation) {
80876
80944
  return result;
80877
80945
  }
80878
80946
  async function mutateGsapScriptBatch(projectId, sourceFile, mutations) {
80879
- const res = await fetch(
80947
+ const res = await studioApiFetch(
80880
80948
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
80881
80949
  {
80882
80950
  method: "POST",
@@ -80981,7 +81049,8 @@ function softReloadOrEscalate(iframe, scriptText, reloadPreview, origin2, author
80981
81049
  }
80982
81050
  function applyPreviewSync(iframe, result, options, reloadPreview, nestedFiles) {
80983
81051
  const patches = instantPatchesFor(options);
80984
- let needsFallback = options.previewFallbackLatch?.pending === true;
81052
+ const writtenWithoutPatch = patches.length === 0;
81053
+ let needsFallback = options.previewFallbackLatch?.pending === true || writtenWithoutPatch;
80985
81054
  if (patches.length > 0) {
80986
81055
  const deferSeek = options.deferPreviewSync === true;
80987
81056
  const missed = patches.find(
@@ -81101,11 +81170,14 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81101
81170
  if (!result) return;
81102
81171
  trackKeyframeCommit(mutations, result, options, calls.map((call) => call.options));
81103
81172
  options.onResult?.(result);
81104
- const instantPatches = calls.map(({ options: callOptions }) => callOptions.instantPatch).filter((patch) => patch !== void 0);
81173
+ const instantPatches = calls.flatMap(
81174
+ ({ options: callOptions }) => callOptions.instantPatch ? [callOptions.instantPatch] : []
81175
+ );
81105
81176
  const { instantPatch: _instantPatch, ...batchOptions } = options;
81106
- await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, instantPatches.length > 0 ? { ...batchOptions, instantPatches } : batchOptions);
81177
+ const allPatched = instantPatches.length === calls.length;
81178
+ await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, allPatched ? { ...batchOptions, instantPatches } : batchOptions);
81107
81179
  }, [showToast, finalizeSuccessfulMutation]);
81108
- const commitMutation = useMemo72(() => {
81180
+ const commitMutation = useMemo73(() => {
81109
81181
  const serializeFile = (file, task) => {
81110
81182
  if (writeProjectFile) {
81111
81183
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -81122,7 +81194,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81122
81194
  };
81123
81195
  const commit = trackedStudioEdit((selection, mutation, options) => {
81124
81196
  if (!activeProjectId) return Promise.resolve();
81125
- const file = selection.sourceFile || activeCompPath || "index.html";
81197
+ const file = gsapWriteFile(selection, activeCompPath);
81126
81198
  return serializeCommit(
81127
81199
  file,
81128
81200
  options.serializeKey,
@@ -81131,7 +81203,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81131
81203
  });
81132
81204
  commit.batch = trackedStudioEdit((calls, options) => {
81133
81205
  if (!activeProjectId) return Promise.resolve();
81134
- const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
81206
+ const file = calls[0] ? gsapWriteFile(calls[0].selection, activeCompPath) : "index.html";
81135
81207
  return serializeCommit(
81136
81208
  file,
81137
81209
  options.serializeKey,
@@ -81170,7 +81242,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81170
81242
  },
81171
81243
  [activeProjectId]
81172
81244
  );
81173
- const sdkDeps = useMemo72(
81245
+ const sdkDeps = useMemo73(
81174
81246
  () => writeProjectFile ? {
81175
81247
  editHistory: { recordEdit: editHistory.recordEdit },
81176
81248
  writeProjectFile,
@@ -81363,13 +81435,13 @@ function useGsapAnimationFetchFallback(projectId) {
81363
81435
 
81364
81436
  // src/hooks/useGsapInteractionFailureTelemetry.ts
81365
81437
  import { useCallback as useCallback141 } from "react";
81366
- function failureToast(error) {
81367
- if (wasAlreadyToasted(error)) return null;
81438
+ function failureToast(error, toast) {
81439
+ if (!toast || wasAlreadyToasted(error)) return null;
81368
81440
  return isGsapEditBlockedError(error) ? error.message : "Failed to save animated edit.";
81369
81441
  }
81370
81442
  function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
81371
81443
  return useCallback141(
81372
- (error, selection, mutationType, label) => {
81444
+ (error, selection, mutationType, label, toast) => {
81373
81445
  const report = isGsapEditBlockedError(error) ? trackStudioEditBlocked : trackStudioSaveFailure;
81374
81446
  report({
81375
81447
  source: "gsap_commit",
@@ -81381,7 +81453,7 @@ function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
81381
81453
  targetSelector: selection?.selector,
81382
81454
  targetSourceFile: selection?.sourceFile
81383
81455
  });
81384
- const message = failureToast(error);
81456
+ const message = failureToast(error, toast !== false);
81385
81457
  if (message) showToast(message, "error");
81386
81458
  },
81387
81459
  [activeCompPath, showToast]
@@ -81835,17 +81907,29 @@ function useDomEditWiring(params) {
81835
81907
 
81836
81908
  // src/hooks/useGsapAwareGroupMove.ts
81837
81909
  import { useCallback as useCallback144 } from "react";
81838
- var groupDragCommitCounter = 0;
81839
- function firstPreflightFailure(results, updates) {
81910
+
81911
+ // src/hooks/gsapGroupPreflight.ts
81912
+ function firstPreflightFailure(results, updates, savedOnElement, activeCompPath) {
81840
81913
  for (const [index, result] of results.entries()) {
81841
81914
  if (result.status !== "rejected") continue;
81842
81915
  const selection = updates[index]?.selection;
81843
81916
  if (selection) return { error: result.reason, selection };
81844
81917
  }
81845
- return null;
81918
+ return secondFile(updates, savedOnElement, activeCompPath);
81919
+ }
81920
+ function secondFile(updates, savedOnElement, activeCompPath) {
81921
+ const scripted = updates.filter(({ selection }) => !savedOnElement.has(selection));
81922
+ const fileOf = (s) => gsapWriteFile(s, activeCompPath);
81923
+ const file = scripted[0] && fileOf(scripted[0].selection);
81924
+ const other = scripted.find(({ selection }) => fileOf(selection) !== file);
81925
+ return other ? { error: new GsapEditBlockedError("mixed-files"), selection: other.selection } : null;
81846
81926
  }
81927
+
81928
+ // src/hooks/useGsapAwareGroupMove.ts
81929
+ var groupDragCommitCounter = 0;
81847
81930
  function useGsapAwareGroupMove({
81848
81931
  gsapCommitMutation,
81932
+ activeCompPath,
81849
81933
  previewIframeRef,
81850
81934
  makeFetchFallback,
81851
81935
  trackGsapInteractionFailure,
@@ -81853,15 +81937,16 @@ function useGsapAwareGroupMove({
81853
81937
  showToast
81854
81938
  }) {
81855
81939
  const handleGsapAwareGroupPathOffsetCommit = useCallback144(
81856
- async (updates) => {
81940
+ async (updates, options = {}) => {
81857
81941
  const writes = observeGsapGesture(gsapCommitMutation);
81858
81942
  const writer = writes.commit;
81859
81943
  if (!writer) return { ok: true, changed: false };
81944
+ const toastRefusal = options.refusalToast !== false;
81860
81945
  const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
81861
81946
  let renderOnCommit = false;
81862
81947
  const previewFallbackLatch = { pending: false };
81863
- const withGroupOptions = (options) => ({
81864
- ...options,
81948
+ const withGroupOptions = (options2) => ({
81949
+ ...options2,
81865
81950
  coalesceKey,
81866
81951
  coalesceMs: Number.POSITIVE_INFINITY,
81867
81952
  deferPreviewSync: !renderOnCommit,
@@ -81882,23 +81967,24 @@ function useGsapAwareGroupMove({
81882
81967
  label: "Move animated layer (group)"
81883
81968
  });
81884
81969
  };
81885
- const coalescedCommit = (selection, mutation, options) => {
81886
- queued.push({ selection, mutation, options: withGroupOptions(options) });
81970
+ const coalescedCommit = (selection, mutation, options2) => {
81971
+ queued.push({ selection, mutation, options: withGroupOptions(options2) });
81887
81972
  return Promise.resolve();
81888
81973
  };
81889
81974
  const preflightAnimations = /* @__PURE__ */ new Map();
81890
81975
  const offsetMembers = /* @__PURE__ */ new Map();
81891
81976
  const preflightResults = await Promise.allSettled(
81892
- updates.map(async ({ selection, plainTranslate }) => {
81893
- if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
81894
- refuseGsapTakeover(selection.element, showToast);
81977
+ updates.map(async ({ selection, next, plainTranslate }) => {
81978
+ if (plainTranslate ?? editsPlainCss(selection.element, "move")) {
81979
+ refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {
81980
+ });
81895
81981
  return void offsetMembers.set(selection, true);
81896
81982
  }
81897
81983
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
81898
81984
  preflightAnimations.set(selection, animations);
81899
81985
  const outcome = await tryGsapDragIntercept(
81900
81986
  selection,
81901
- { x: 0, y: 0 },
81987
+ next,
81902
81988
  animations,
81903
81989
  previewIframeRef.current,
81904
81990
  coalescedCommit,
@@ -81909,13 +81995,19 @@ function useGsapAwareGroupMove({
81909
81995
  assertGsapEditPersisted(outcome);
81910
81996
  })
81911
81997
  );
81912
- const preflightFailure = firstPreflightFailure(preflightResults, updates);
81998
+ const preflightFailure = firstPreflightFailure(
81999
+ preflightResults,
82000
+ updates,
82001
+ offsetMembers,
82002
+ activeCompPath
82003
+ );
81913
82004
  if (preflightFailure) {
81914
82005
  trackGsapInteractionFailure(
81915
82006
  preflightFailure.error,
81916
82007
  preflightFailure.selection,
81917
82008
  "drag",
81918
- "Move animated layer (group)"
82009
+ "Move animated layer (group)",
82010
+ toastRefusal
81919
82011
  );
81920
82012
  throw preflightFailure.error;
81921
82013
  }
@@ -81967,6 +82059,7 @@ function useGsapAwareGroupMove({
81967
82059
  },
81968
82060
  [
81969
82061
  gsapCommitMutation,
82062
+ activeCompPath,
81970
82063
  previewIframeRef,
81971
82064
  makeFetchFallback,
81972
82065
  trackGsapInteractionFailure,
@@ -81980,23 +82073,6 @@ function useGsapAwareGroupMove({
81980
82073
  // src/hooks/useGsapAwareEditing.ts
81981
82074
  import { useCallback as useCallback146 } from "react";
81982
82075
 
81983
- // src/utils/elementGsap.ts
81984
- function gsapOf2(element) {
81985
- return element.ownerDocument.defaultView?.gsap;
81986
- }
81987
- function setElementGsapPosition(element, x, y) {
81988
- const gsap3 = gsapOf2(element);
81989
- if (!gsap3?.set) return false;
81990
- gsap3.set(element, { x, y });
81991
- return true;
81992
- }
81993
- function setElementGsapScale(element, x, y) {
81994
- const gsap3 = gsapOf2(element);
81995
- if (!gsap3?.set) return false;
81996
- gsap3.set(element, { scaleX: x, scaleY: y });
81997
- return true;
81998
- }
81999
-
82000
82076
  // src/hooks/gsapResizePreflight.ts
82001
82077
  function resizeRoute(animations, fetchedAnimations) {
82002
82078
  const allKnownAnimations = [...animations, ...fetchedAnimations];
@@ -82028,16 +82104,8 @@ function preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnim
82028
82104
  }
82029
82105
 
82030
82106
  // src/hooks/gsapResizeIntercept.ts
82031
- var IDENTITY_ONE_PROPS = /* @__PURE__ */ new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
82032
82107
  var SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
82033
- function synthesizeIdentityProps(source) {
82034
- const id = {};
82035
- for (const [k, v] of Object.entries(source)) {
82036
- if (typeof v === "number") id[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
82037
- else id[k] = v;
82038
- }
82039
- return id;
82040
- }
82108
+ var POSITION_XY = /* @__PURE__ */ new Set(["x", "y"]);
82041
82109
  function originalBoxSize(el, measuredAttr, inlineProperty) {
82042
82110
  const measured = Number.parseFloat(el?.getAttribute(measuredAttr) ?? "");
82043
82111
  if (Number.isFinite(measured) && measured > 0) return measured;
@@ -82046,6 +82114,36 @@ function originalBoxSize(el, measuredAttr, inlineProperty) {
82046
82114
  );
82047
82115
  return Number.isFinite(inline) && inline > 0 ? inline : null;
82048
82116
  }
82117
+ function preGestureBoxSize(el) {
82118
+ const width = originalBoxSize(el, STUDIO_ORIGINAL_BOX_WIDTH_ATTR, "width");
82119
+ const height = originalBoxSize(el, STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, "height");
82120
+ return { ...width != null && { width }, ...height != null && { height } };
82121
+ }
82122
+ async function commitSizeAtPlayhead(selection, anim, size, iframe, dragOffset, callbacks) {
82123
+ const selector = selectorFromSelection(selection);
82124
+ const moves = !!dragOffset && (dragOffset.x !== 0 || dragOffset.y !== 0) && !!selector;
82125
+ const anchor2 = moves && animationWritesAnyProperty(anim, POSITION_XY) ? computeDraggedGsapPosition(
82126
+ selection.element,
82127
+ dragOffset,
82128
+ readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 }
82129
+ ) : null;
82130
+ const written = await commitValueAtPlayhead(
82131
+ selection,
82132
+ anim,
82133
+ anchor2 ? { ...size, x: anchor2.newX, y: anchor2.newY } : size,
82134
+ iframe,
82135
+ callbacks,
82136
+ {
82137
+ label: "Resize",
82138
+ backfill: {
82139
+ ...preGestureBoxSize(selection.element),
82140
+ ...anchor2 && { x: anchor2.baseGsapX, y: anchor2.baseGsapY }
82141
+ },
82142
+ ...anchor2 && { beforeReload: () => restoreDragOffset(selection.element) }
82143
+ }
82144
+ );
82145
+ return written.status === "persisted" && anchor2 ? { ...written, ownsDragOffset: true } : written;
82146
+ }
82049
82147
  function tweenUsesScaleLonghands(anim) {
82050
82148
  const isLonghand = (name) => name === "scaleX" || name === "scaleY";
82051
82149
  const inKeyframes = (anim?.keyframes?.keyframes ?? []).some(
@@ -82053,7 +82151,7 @@ function tweenUsesScaleLonghands(anim) {
82053
82151
  );
82054
82152
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
82055
82153
  }
82056
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations) {
82154
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset) {
82057
82155
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
82058
82156
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
82059
82157
  if (outcome.status === "blocked") return outcome;
@@ -82069,7 +82167,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82069
82167
  commitMutation,
82070
82168
  postSplitFetch
82071
82169
  );
82072
- let anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, resizeProperties) ? resolved2.anim : null;
82170
+ const anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, resizeProperties) ? resolved2.anim : null;
82073
82171
  logResize("intercept-enter", {
82074
82172
  hasScaleGroup: resizeGroup === "scale",
82075
82173
  resizeGroup,
@@ -82093,15 +82191,11 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82093
82191
  );
82094
82192
  if (animatedTween) {
82095
82193
  logResize("intercept-route", { route: "keyframed-size", tweenId: animatedTween.id });
82096
- const handled = await commitKeyframedSizeFromResize(
82097
- selection,
82098
- size,
82099
- sel,
82100
- sizeSet,
82101
- animatedTween,
82102
- { commitMutation, fetchAnimations: fetchFallbackAnimations }
82103
- );
82104
- if (handled) return { status: "persisted" };
82194
+ const sized = { width: roundToLayoutPx(size.width), height: roundToLayoutPx(size.height) };
82195
+ return commitSizeAtPlayhead(selection, animatedTween, sized, iframe, dragOffset, {
82196
+ commitMutation,
82197
+ fetchAnimations: fetchFallbackAnimations
82198
+ });
82105
82199
  }
82106
82200
  }
82107
82201
  logResize("intercept-route", { route: "static-size-set", hadSizeSet: !!sizeSet, resizeGroup });
@@ -82117,10 +82211,9 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82117
82211
  }
82118
82212
  const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
82119
82213
  const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
82120
- if (activeKeyframePct != null) setActiveKeyframePct(null);
82121
82214
  const selector = selectorFromSelection(selection);
82122
- const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim, resizeGroup) : {};
82123
82215
  let resizeProps;
82216
+ let resizeBackfill;
82124
82217
  let scaleDraftEl = null;
82125
82218
  let scaleDraftDropPoint = null;
82126
82219
  let committedScale = null;
@@ -82140,6 +82233,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82140
82233
  nonUniformScale = uniformDrift > 0.5;
82141
82234
  useScaleLonghands = nonUniformScale || tweenUsesScaleLonghands(anim);
82142
82235
  resizeProps = useScaleLonghands ? { scaleX: newScaleX, scaleY: newScaleY } : { scale: newScaleX };
82236
+ resizeBackfill = { scaleX: liveScaleX, scaleY: liveScaleY };
82143
82237
  logResize("intercept-route", {
82144
82238
  route: "scale-tween",
82145
82239
  cssW,
@@ -82161,6 +82255,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82161
82255
  width: roundToLayoutPx(size.width),
82162
82256
  height: roundToLayoutPx(size.height)
82163
82257
  };
82258
+ resizeBackfill = preGestureBoxSize(selection.element);
82164
82259
  }
82165
82260
  const finalizeScaleResizeCommit = async () => {
82166
82261
  if (!scaleDraftEl) return false;
@@ -82206,10 +82301,12 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82206
82301
  );
82207
82302
  if (positionTween) {
82208
82303
  logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
82209
- await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, selector, {
82210
- commitMutation,
82211
- fetchAnimations: fetchFallbackAnimations
82212
- });
82304
+ assertGsapEditPersisted(
82305
+ await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, {
82306
+ commitMutation,
82307
+ fetchAnimations: fetchFallbackAnimations
82308
+ })
82309
+ );
82213
82310
  return true;
82214
82311
  }
82215
82312
  const existingSet = findExistingPositionWrite(currentAnimations, selector, selection.element);
@@ -82232,102 +82329,14 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82232
82329
  );
82233
82330
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
82234
82331
  }
82235
- const ct = usePlayerStore.getState().currentTime;
82236
- const ts = resolveTweenStart(anim);
82237
- const td = tweenDuration;
82238
- const outsideRange = ts !== null && td > 0 && (ct < ts - 0.01 || ct > ts + td + 0.01);
82239
- if (!outsideRange) {
82240
- if (anim.hasUnresolvedKeyframes || anim.hasUnresolvedSelector) {
82241
- const newId = await materializeIfDynamic(anim, iframe, commitMutation, selection);
82242
- if (newId) anim = { ...anim, id: newId };
82243
- } else if (!anim.keyframes) {
82244
- const resolvedFromValues = selector ? readAllAnimatedProperties(iframe, selector, anim, resizeGroup) : void 0;
82245
- await commitMutation(
82246
- selection,
82247
- { type: "convert-to-keyframes", animationId: anim.id, resolvedFromValues },
82248
- { label: "Convert to keyframes for resize" }
82249
- );
82250
- if (fetchFallbackAnimations) {
82251
- const fresh = await fetchFallbackAnimations();
82252
- const refreshed = fresh.find(
82253
- (a) => a.targetSelector === anim.targetSelector && a.keyframes
82254
- );
82255
- if (refreshed) anim = refreshed;
82256
- }
82257
- }
82258
- }
82259
- if ((outsideRange || useScaleLonghands) && ts !== null) {
82260
- const kfs = anim.keyframes?.keyframes ?? (() => {
82261
- const fromProps = anim.method === "from" || anim.method === "fromTo" ? { ...anim.properties } : synthesizeIdentityProps(anim.properties);
82262
- const toProps = anim.method === "from" ? synthesizeIdentityProps(anim.properties) : { ...anim.properties };
82263
- return [
82264
- { percentage: 0, properties: fromProps },
82265
- { percentage: 100, properties: toProps }
82266
- ];
82267
- })();
82268
- const newStart = Math.min(ct, ts);
82269
- const newEnd = Math.max(ct, ts + td);
82270
- const newDuration = Math.max(0.01, newEnd - newStart);
82271
- const existingKfs = kfs;
82272
- const remapped = [];
82273
- for (const kf of existingKfs) {
82274
- const absTime = ts + kf.percentage / 100 * td;
82275
- const newPct = Math.round((absTime - newStart) / newDuration * 1e3) / 10;
82276
- const props = { ...kf.properties };
82277
- if (nonUniformScale && "scale" in props) {
82278
- const uniform = props.scale;
82279
- if (typeof uniform === "number") {
82280
- props.scaleX = uniform;
82281
- props.scaleY = uniform;
82282
- }
82283
- delete props.scale;
82284
- }
82285
- for (const k of Object.keys(resizeProps)) {
82286
- if (k in props) continue;
82287
- if (k === "width" || k === "height") continue;
82288
- props[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
82289
- }
82290
- remapped.push({ percentage: newPct, properties: props });
82291
- }
82292
- const targetPct = Math.round((ct - newStart) / newDuration * 1e3) / 10;
82293
- const collidingKf = remapped.find((kf) => Math.abs(kf.percentage - targetPct) < 0.05);
82294
- if (collidingKf) Object.assign(collidingKf.properties, resizeProps);
82295
- else remapped.push({ percentage: targetPct, properties: resizeProps });
82296
- remapped.sort((a, b) => a.percentage - b.percentage);
82297
- await commitMutation(
82298
- selection,
82299
- {
82300
- type: "replace-with-keyframes",
82301
- animationId: anim.id,
82302
- targetSelector: anim.targetSelector,
82303
- position: roundTo3(newStart),
82304
- duration: roundTo3(newDuration),
82305
- keyframes: remapped
82306
- },
82307
- {
82308
- label: outsideRange ? `Resize (extended to ${ct.toFixed(2)}s)` : `Resize (keyframe ${Math.round((ct - newStart) / newDuration * 1e3) / 10}%)`,
82309
- softReload: true,
82310
- ...!collidingKf ? { keyframeAction: "add" } : {}
82311
- }
82312
- );
82313
- return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
82314
- }
82315
- const backfillDefaults = {};
82316
- for (const k of Object.keys(runtimeProps)) {
82317
- if (SIZE_PROPS.has(k)) continue;
82318
- backfillDefaults[k] = IDENTITY_ONE_PROPS.has(k) ? 1 : 0;
82319
- }
82320
- await commitMutation(
82321
- selection,
82322
- {
82323
- type: "add-keyframe",
82324
- animationId: anim.id,
82325
- percentage: pct,
82326
- properties: resizeProps,
82327
- backfillDefaults
82328
- },
82329
- { label: `Resize (keyframe ${pct}%)`, softReload: true }
82330
- );
82332
+ const callbacks = { commitMutation, fetchAnimations: fetchFallbackAnimations };
82333
+ if (resizeGroup === "size")
82334
+ return commitSizeAtPlayhead(selection, anim, resizeProps, iframe, dragOffset, callbacks);
82335
+ const written = await commitValueAtPlayhead(selection, anim, resizeProps, iframe, callbacks, {
82336
+ label: "Resize",
82337
+ backfill: resizeBackfill
82338
+ });
82339
+ if (written.status !== "persisted") return written;
82331
82340
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
82332
82341
  }
82333
82342
 
@@ -82690,6 +82699,7 @@ function useGsapAwareEditing({
82690
82699
  domEditSelection,
82691
82700
  selectedGsapAnimations,
82692
82701
  gsapCommitMutation,
82702
+ activeCompPath,
82693
82703
  previewIframeRef,
82694
82704
  showToast,
82695
82705
  bumpGsapCache,
@@ -82714,7 +82724,7 @@ function useGsapAwareEditing({
82714
82724
  const handleGsapAwarePathOffsetCommit = useCallback146(
82715
82725
  async (selection, next, modifiers) => {
82716
82726
  const writes = observeGsapGesture(gsapCommitMutation);
82717
- if (modifiers?.plainTranslate ?? !gsapWritesPosition(selection.element)) {
82727
+ if (modifiers?.plainTranslate ?? editsPlainCss(selection.element, "move")) {
82718
82728
  const result = await stageElementPositionOffset(selection, next, true).save();
82719
82729
  return writes.finish(result?.changed === true);
82720
82730
  }
@@ -82753,6 +82763,7 @@ function useGsapAwareEditing({
82753
82763
  );
82754
82764
  const handleGsapAwareGroupPathOffsetCommit = useGsapAwareGroupMove({
82755
82765
  gsapCommitMutation,
82766
+ activeCompPath,
82756
82767
  previewIframeRef,
82757
82768
  makeFetchFallback,
82758
82769
  trackGsapInteractionFailure,
@@ -82760,10 +82771,10 @@ function useGsapAwareEditing({
82760
82771
  showToast
82761
82772
  });
82762
82773
  const handleGsapAwareBoxSizeCommit = useCallback146(
82763
- async (selection, next, offset, restore = () => void 0) => {
82774
+ async (selection, next, offset, restore2 = () => void 0, route) => {
82764
82775
  const writes = observeGsapGesture(gsapCommitMutation);
82765
- if (!gsapWritesBox(selection.element)) {
82766
- const result = await handleDomBoxSizeCommit(selection, next, offset, restore);
82776
+ if (route?.plainTranslate ?? editsPlainCss(selection.element, "resize")) {
82777
+ const result = await handleDomBoxSizeCommit(selection, next, offset, restore2);
82767
82778
  return writes.finish(result?.changed === true);
82768
82779
  }
82769
82780
  let targetAnimations;
@@ -82771,7 +82782,7 @@ function useGsapAwareEditing({
82771
82782
  const ownedAnimations = getGsapAnimationsForSelection(selection);
82772
82783
  targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
82773
82784
  } catch (error) {
82774
- restore();
82785
+ restore2();
82775
82786
  trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
82776
82787
  throw error;
82777
82788
  }
@@ -82804,8 +82815,13 @@ function useGsapAwareEditing({
82804
82815
  makeFetchFallback(selection)
82805
82816
  );
82806
82817
  await saveMove(dragOutcome, async () => {
82807
- const plain = !gsapWritesPosition(selection.element);
82808
- anchorMove = stageElementPositionOffset(selection, offset, plain, coalesceKey);
82818
+ const plainAfterSettle = editsPlainCss(selection.element, "move");
82819
+ anchorMove = stageElementPositionOffset(
82820
+ selection,
82821
+ offset,
82822
+ plainAfterSettle,
82823
+ coalesceKey
82824
+ );
82809
82825
  });
82810
82826
  }
82811
82827
  };
@@ -82832,7 +82848,8 @@ function useGsapAwareEditing({
82832
82848
  targetAnimations,
82833
82849
  previewIframeRef.current,
82834
82850
  commitMutation2,
82835
- makeFetchFallback(selection)
82851
+ makeFetchFallback(selection),
82852
+ offset
82836
82853
  );
82837
82854
  assertGsapEditPersisted(outcome);
82838
82855
  if (outcome.status === "element-size") {
@@ -82876,7 +82893,7 @@ function useGsapAwareEditing({
82876
82893
  },
82877
82894
  restore: () => {
82878
82895
  anchorMove?.rollback();
82879
- restore();
82896
+ restore2();
82880
82897
  },
82881
82898
  skipPixelAssert: hasLivePositionTween
82882
82899
  });
@@ -82896,7 +82913,7 @@ function useGsapAwareEditing({
82896
82913
  const handleGsapAwareRotationCommit = useCallback146(
82897
82914
  async (selection, next) => {
82898
82915
  const writes = observeGsapGesture(gsapCommitMutation);
82899
- if (next.plain || !gsapWritesRotation(selection.element)) {
82916
+ if (next.plain === void 0 ? editsPlainCss(selection.element, "rotate") : next.plain) {
82900
82917
  const result = await handleDomRotationCommit(selection, next);
82901
82918
  return writes.finish(result?.changed === true);
82902
82919
  }
@@ -83089,7 +83106,7 @@ function reportSelectionPublishError(error) {
83089
83106
  console.warn("[Studio] Failed to update agent selection context", error);
83090
83107
  }
83091
83108
  function putSelection(projectId, selection, signal) {
83092
- return fetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
83109
+ return studioApiFetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
83093
83110
  method: "PUT",
83094
83111
  headers: { "Content-Type": "application/json" },
83095
83112
  body: JSON.stringify({ selection }),
@@ -83205,7 +83222,7 @@ function useKeyframeEaseCommits({
83205
83222
  }
83206
83223
 
83207
83224
  // src/hooks/useCommitPreflightCapabilities.ts
83208
- import { useEffect as useEffect122, useMemo as useMemo73, useRef as useRef153, useState as useState119 } from "react";
83225
+ import { useEffect as useEffect122, useMemo as useMemo74, useRef as useRef153, useState as useState119 } from "react";
83209
83226
  function runCommitPreflights(selection, fileAnimations, iframe, group) {
83210
83227
  const target = { id: selection.id ?? null, selector: selection.selector ?? null };
83211
83228
  const animations = getAnimationsForElement(fileAnimations, target, selection.element);
@@ -83274,7 +83291,7 @@ function useCommitPreflightCapabilities({
83274
83291
  });
83275
83292
  }
83276
83293
  }, [enabled, projectId, selection, groupSelections, version]);
83277
- return useMemo73(() => {
83294
+ return useMemo74(() => {
83278
83295
  void parseTick;
83279
83296
  if (!enabled || !projectId) return { selection, groupSelections };
83280
83297
  const group = groupSelections.length > 1;
@@ -83322,6 +83339,70 @@ function useLivePreviewIframe2(host) {
83322
83339
  return live;
83323
83340
  }
83324
83341
 
83342
+ // src/components/editor/domGroupMoveBy.ts
83343
+ function toScreen(doc, delta) {
83344
+ const origin2 = toTopScreenPoint(doc, { x: 0, y: 0 });
83345
+ const moved = toTopScreenPoint(doc, delta);
83346
+ if (!origin2 || !moved) throw new Error("The preview has no measurable size.");
83347
+ return { x: moved.x - origin2.x, y: moved.y - origin2.y };
83348
+ }
83349
+ function memberFor(selection, scale) {
83350
+ const { element } = selection;
83351
+ const box = element.getBoundingClientRect();
83352
+ const result = createManualOffsetDragMember({
83353
+ key: selection.id ?? selection.selector ?? selection.label,
83354
+ selection,
83355
+ element,
83356
+ rect: {
83357
+ left: box.left,
83358
+ top: box.top,
83359
+ width: box.width,
83360
+ height: box.height,
83361
+ editScaleX: scale.x,
83362
+ editScaleY: scale.y
83363
+ },
83364
+ gesture: "drag"
83365
+ });
83366
+ if (!result.ok) throw new Error(result.reason);
83367
+ return result.member;
83368
+ }
83369
+ async function moveDomGroupBy(moves, commitGroup) {
83370
+ const moving = moves.filter(({ delta }) => delta.x !== 0 || delta.y !== 0);
83371
+ if (moving.length === 0) return;
83372
+ const selections = moving.map(({ selection }) => selection);
83373
+ if (!canCanvasNudgeTargets(selections)) {
83374
+ const blocked = selections.find((s) => !s.capabilities.canApplyManualOffset);
83375
+ throw new Error(blocked?.capabilities.reasonIfDisabled ?? `${blocked?.label} can't be moved.`);
83376
+ }
83377
+ const screens = moving.map(
83378
+ ({ selection, delta }) => toScreen(selection.element.ownerDocument, delta)
83379
+ );
83380
+ const members = [];
83381
+ try {
83382
+ for (const selection of selections)
83383
+ members.push(memberFor(selection, toScreen(selection.element.ownerDocument, { x: 1, y: 1 })));
83384
+ } catch (error) {
83385
+ restoreManualOffsetDragMembers(members);
83386
+ throw error;
83387
+ }
83388
+ const updates = members.map((member, index) => ({
83389
+ selection: member.selection,
83390
+ next: applyManualOffsetDragCommit(member, screens[index].x, screens[index].y),
83391
+ plainTranslate: member.plainTranslate
83392
+ }));
83393
+ try {
83394
+ await countStudioManualEditSave(members[0].element, () => commitGroup(updates));
83395
+ } catch (error) {
83396
+ for (const member of members) {
83397
+ if (isStudioManualEditGestureCurrent(member.element, member.gestureToken))
83398
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
83399
+ }
83400
+ throw error;
83401
+ } finally {
83402
+ endManualOffsetDragMembers(members);
83403
+ }
83404
+ }
83405
+
83325
83406
  // src/hooks/useDomEditSession.ts
83326
83407
  function useDomEditSession({
83327
83408
  projectId,
@@ -83674,6 +83755,7 @@ function useDomEditSession({
83674
83755
  domEditSelection,
83675
83756
  selectedGsapAnimations,
83676
83757
  gsapCommitMutation,
83758
+ activeCompPath,
83677
83759
  previewIframeRef,
83678
83760
  showToast,
83679
83761
  bumpGsapCache,
@@ -83688,6 +83770,13 @@ function useDomEditSession({
83688
83770
  setArcPath,
83689
83771
  updateArcSegment
83690
83772
  });
83773
+ const handleDomGroupMoveBy = useCallback148(
83774
+ (moves) => moveDomGroupBy(
83775
+ moves,
83776
+ (updates) => handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false })
83777
+ ),
83778
+ [handleGsapAwareGroupPathOffsetCommit]
83779
+ );
83691
83780
  const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } = useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
83692
83781
  const committable = useCommitPreflightCapabilities({
83693
83782
  projectId,
@@ -83725,6 +83814,7 @@ function useDomEditSession({
83725
83814
  handleDomAttributeBatchCommit,
83726
83815
  handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
83727
83816
  handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
83817
+ handleDomGroupMoveBy,
83728
83818
  handleDomZIndexReorderCommit,
83729
83819
  handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
83730
83820
  handleDomRotationCommit: handleGsapAwareRotationCommit,
@@ -83873,7 +83963,7 @@ async function readProjectFileOptional(projectId, path) {
83873
83963
  let res;
83874
83964
  const fetchStarted = performance.now();
83875
83965
  try {
83876
- res = await fetch(
83966
+ res = await studioApiFetch(
83877
83967
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`
83878
83968
  );
83879
83969
  } catch {
@@ -84458,8 +84548,10 @@ function useExternalFileChangeCoordinator({
84458
84548
  },
84459
84549
  [projectId, pendingTimelineEditPathRef, startDrainLoop, onAcceptedPersistedFileChange]
84460
84550
  );
84551
+ const processChangeRef = useRef156(processChange);
84552
+ processChangeRef.current = processChange;
84461
84553
  useEffect127(() => {
84462
- const handler = (delivery) => processChange(decodeFileChange(delivery));
84554
+ const handler = (delivery) => processChangeRef.current(decodeFileChange(delivery));
84463
84555
  const adapter = testHotAdapter();
84464
84556
  if (adapter) {
84465
84557
  adapter.on("hf:file-change", handler);
@@ -84470,7 +84562,7 @@ function useExternalFileChangeCoordinator({
84470
84562
  return () => import.meta.hot?.off?.("hf:file-change", handler);
84471
84563
  }
84472
84564
  return sseFileChangeChannel(handler);
84473
- }, [processChange]);
84565
+ }, []);
84474
84566
  const retry = useCallback150(async () => {
84475
84567
  const current = blockedRef.current;
84476
84568
  if (!current || current.status === "conflict" || current.recovered) return;
@@ -84617,7 +84709,7 @@ function useStudioExternalFileChanges({
84617
84709
  }
84618
84710
 
84619
84711
  // src/hooks/useBlockHandlers.ts
84620
- import { useCallback as useCallback152, useMemo as useMemo74, useRef as useRef157, useState as useState123 } from "react";
84712
+ import { useCallback as useCallback152, useMemo as useMemo75, useRef as useRef157, useState as useState123 } from "react";
84621
84713
 
84622
84714
  // src/utils/blockInstaller.ts
84623
84715
  function getMaxZIndexFromIframe(iframe) {
@@ -84646,7 +84738,7 @@ async function installRegistryItem({
84646
84738
  blockName,
84647
84739
  showToast
84648
84740
  }) {
84649
- const response = await fetch(buildProjectApiPath(projectId, `/registry/install`), {
84741
+ const response = await studioApiFetch(buildProjectApiPath(projectId, `/registry/install`), {
84650
84742
  method: "POST",
84651
84743
  headers: { "Content-Type": "application/json" },
84652
84744
  body: JSON.stringify({ blockName })
@@ -84811,7 +84903,7 @@ function useBlockHandlers({
84811
84903
  setRightPanelTab
84812
84904
  }) {
84813
84905
  const [activeBlockParams, setActiveBlockParams] = useState123(null);
84814
- const blockCtx = useMemo74(
84906
+ const blockCtx = useMemo75(
84815
84907
  () => ({
84816
84908
  activeCompPath: blockCtxDeps.activeCompPath,
84817
84909
  timelineElements: blockCtxDeps.timelineElements,
@@ -85178,7 +85270,7 @@ function dispatchPlainKey(event, key2, cb) {
85178
85270
  }
85179
85271
 
85180
85272
  // src/hooks/useEditHistoryActions.ts
85181
- import { useCallback as useCallback153, useMemo as useMemo75 } from "react";
85273
+ import { useCallback as useCallback153, useMemo as useMemo76 } from "react";
85182
85274
  function useEditHistoryActions({
85183
85275
  editHistory,
85184
85276
  readOptionalProjectFile,
@@ -85205,20 +85297,25 @@ function useEditHistoryActions({
85205
85297
  const noun = direction === "undo" ? "Undo" : "Redo";
85206
85298
  const predicted = editHistory.predict?.(direction) ?? null;
85207
85299
  const predictedShown = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
85208
- const pendingEditShown = !predictedShown && direction === "undo" ? revertNewestStudioPendingEdit() : null;
85209
- const putBack = predictedShown ?? pendingEditShown;
85300
+ const pendingEditShown = !predictedShown && direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
85301
+ const putBack = predictedShown ?? pendingEditShown?.showAgain;
85302
+ const claimedAfter = direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : void 0;
85210
85303
  let result = { ok: false, reason: "failed" };
85211
85304
  let serverSteppedShown = false;
85305
+ let revertIsTheUndo = false;
85212
85306
  try {
85213
85307
  await waitForPendingDomEditSaves();
85308
+ revertIsTheUndo = Boolean(pendingEditShown && !await pendingEditShown.landed());
85309
+ if (revertIsTheUndo) return;
85214
85310
  result = await editHistory[direction]({
85215
85311
  readFile: readHistoryFile,
85216
- serialize: serializeHistoryFiles
85312
+ serialize: serializeHistoryFiles,
85313
+ claimedAfter
85217
85314
  });
85218
85315
  const stepped = Boolean(result.ok && result.label);
85219
85316
  serverSteppedShown = predictedShown ? stepped && result.undoes === predicted?.id : stepped && Boolean(pendingEditShown);
85220
85317
  } finally {
85221
- if (putBack && !serverSteppedShown) putBack();
85318
+ if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
85222
85319
  }
85223
85320
  if (!result.ok && result.reason === "content-mismatch") {
85224
85321
  showToast(
@@ -85233,12 +85330,12 @@ function useEditHistoryActions({
85233
85330
  }
85234
85331
  if (result.ok && result.label) {
85235
85332
  const files = serverSteppedShown && predictedShown ? fromShown(result.files, predicted.files) : result.files;
85236
- const restore = { paths: result.paths, files };
85237
- onAfterUndoRedo?.(restore);
85333
+ const restore2 = { paths: result.paths, files };
85334
+ onAfterUndoRedo?.(restore2);
85238
85335
  if (activeCompPath && result.paths?.includes(activeCompPath)) {
85239
85336
  forceReloadSdkSession?.();
85240
85337
  }
85241
- await syncHistoryPreviewAfterApply(restore);
85338
+ await syncHistoryPreviewAfterApply(restore2);
85242
85339
  showToast(result.label, "info");
85243
85340
  }
85244
85341
  },
@@ -85257,7 +85354,7 @@ function useEditHistoryActions({
85257
85354
  );
85258
85355
  const undo = useCallback153(() => apply("undo"), [apply]);
85259
85356
  const redo = useCallback153(() => apply("redo"), [apply]);
85260
- return useMemo75(() => ({ undo, redo }), [undo, redo]);
85357
+ return useMemo76(() => ({ undo, redo }), [undo, redo]);
85261
85358
  }
85262
85359
  function fromShown(files, shown2) {
85263
85360
  if (!files) return files;
@@ -86252,7 +86349,7 @@ function useCaptionDetection({
86252
86349
  if (!captionSrcPath) return;
86253
86350
  activating = true;
86254
86351
  const srcPath = captionSrcPath;
86255
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`)).then((r) => r.json()).then((data) => {
86352
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`)).then((r) => r.json()).then((data) => {
86256
86353
  if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
86257
86354
  const root = doc.querySelector("[data-composition-id]");
86258
86355
  const w = parseInt(root?.getAttribute("data-width") ?? "1920", 10);
@@ -86365,7 +86462,7 @@ function useConsoleErrorCapture(previewIframe) {
86365
86462
  }
86366
86463
 
86367
86464
  // src/hooks/useFrameCapture.ts
86368
- import { useMemo as useMemo76, useState as useState125, useCallback as useCallback157, useRef as useRef162 } from "react";
86465
+ import { useMemo as useMemo77, useState as useState125, useCallback as useCallback157, useRef as useRef162 } from "react";
86369
86466
  function useFrameCapture({
86370
86467
  projectId,
86371
86468
  activeCompPath,
@@ -86417,7 +86514,10 @@ function useFrameCapture({
86417
86514
  const controller = new AbortController();
86418
86515
  const timeout = setTimeout(() => controller.abort(), 3e4);
86419
86516
  try {
86420
- const response = await fetch(href, { cache: "no-store", signal: controller.signal });
86517
+ const response = await studioApiFetch(href, {
86518
+ cache: "no-store",
86519
+ signal: controller.signal
86520
+ });
86421
86521
  clearTimeout(timeout);
86422
86522
  if (!response.ok) {
86423
86523
  let msg = `Capture failed (${response.status})`;
@@ -86453,7 +86553,7 @@ function useFrameCapture({
86453
86553
  },
86454
86554
  [activeCompPath, playheadTime, projectId, showToast, waitForPendingDomEditSaves]
86455
86555
  );
86456
- const captureFrameHref = useMemo76(
86556
+ const captureFrameHref = useMemo77(
86457
86557
  () => projectId ? buildFrameCaptureUrl({
86458
86558
  projectId,
86459
86559
  compositionPath: activeCompPath,
@@ -86473,7 +86573,7 @@ function useFrameCapture({
86473
86573
  }
86474
86574
 
86475
86575
  // src/hooks/useLintModal.ts
86476
- import { useState as useState126, useCallback as useCallback158, useEffect as useEffect131, useRef as useRef163, useMemo as useMemo77 } from "react";
86576
+ import { useState as useState126, useCallback as useCallback158, useEffect as useEffect131, useRef as useRef163, useMemo as useMemo78 } from "react";
86477
86577
  function parseFinding(f) {
86478
86578
  return {
86479
86579
  severity: f.severity === "error" ? "error" : "warning",
@@ -86484,7 +86584,7 @@ function parseFinding(f) {
86484
86584
  };
86485
86585
  }
86486
86586
  async function fetchLintFindings(projectId) {
86487
- const res = await fetch(buildProjectApiPath(projectId, `/lint`));
86587
+ const res = await studioApiFetch(buildProjectApiPath(projectId, `/lint`));
86488
86588
  const data = await res.json();
86489
86589
  return (data.findings ?? []).map(parseFinding);
86490
86590
  }
@@ -86546,8 +86646,8 @@ function useLintModal(projectId, refreshKey) {
86546
86646
  },
86547
86647
  [backgroundFindings]
86548
86648
  );
86549
- const findingsByElement = useMemo77(() => groupFindings((f) => f.elementId), [groupFindings]);
86550
- const findingsByFile = useMemo77(() => groupFindings((f) => f.file), [groupFindings]);
86649
+ const findingsByElement = useMemo78(() => groupFindings((f) => f.elementId), [groupFindings]);
86650
+ const findingsByFile = useMemo78(() => groupFindings((f) => f.file), [groupFindings]);
86551
86651
  const badgeFindings = lintModal ?? backgroundFindings;
86552
86652
  const lintFindingCount = badgeFindings.length;
86553
86653
  const hasLintError = badgeFindings.some((f) => f.severity === "error");
@@ -87012,16 +87112,16 @@ function useStudioUrlState({
87012
87112
  }
87013
87113
 
87014
87114
  // src/hooks/useEffectiveTimelineDuration.ts
87015
- import { useMemo as useMemo78 } from "react";
87115
+ import { useMemo as useMemo79 } from "react";
87016
87116
  function useEffectiveTimelineDuration(timelineDuration2, timelineElements) {
87017
- return useMemo78(
87117
+ return useMemo79(
87018
87118
  () => getEffectiveTimelineDuration(timelineDuration2, timelineElements),
87019
87119
  [timelineDuration2, timelineElements]
87020
87120
  );
87021
87121
  }
87022
87122
 
87023
87123
  // src/hooks/useStudioContextValue.ts
87024
- import { useCallback as useCallback161, useMemo as useMemo79 } from "react";
87124
+ import { useCallback as useCallback161, useMemo as useMemo80 } from "react";
87025
87125
  function buildStudioContextValue(input) {
87026
87126
  return {
87027
87127
  projectId: input.projectId,
@@ -87049,7 +87149,7 @@ function buildStudioContextValue(input) {
87049
87149
  };
87050
87150
  }
87051
87151
  function useInspectorState(rightPanel, isPlaying, domEditSelection, isGestureRecording) {
87052
- return useMemo79(() => {
87152
+ return useMemo80(() => {
87053
87153
  const inspectorPanelActive = rightPanel === "layers" || rightPanel === "design";
87054
87154
  return {
87055
87155
  inspectorPanelActive,
@@ -87095,7 +87195,7 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
87095
87195
  import { memo as memo44, useContext as useContext15 } from "react";
87096
87196
 
87097
87197
  // src/contexts/PanelLayoutContext.tsx
87098
- import { useContext as useContext14, useMemo as useMemo80 } from "react";
87198
+ import { useContext as useContext14, useMemo as useMemo81 } from "react";
87099
87199
  import { jsx as jsx199 } from "react/jsx-runtime";
87100
87200
  var PanelLayoutContext = createStableContext("PanelLayoutContext", null);
87101
87201
  function usePanelLayoutContext() {
@@ -87107,7 +87207,7 @@ function PanelLayoutProvider({
87107
87207
  value: { rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab },
87108
87208
  children
87109
87209
  }) {
87110
- const stable = useMemo80(
87210
+ const stable = useMemo81(
87111
87211
  () => ({ rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab }),
87112
87212
  [rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab]
87113
87213
  );
@@ -88179,7 +88279,7 @@ function useGestureCommit({
88179
88279
  import { useLayoutEffect as useLayoutEffect13, useRef as useRef168 } from "react";
88180
88280
 
88181
88281
  // src/components/editor/GestureTrailOverlay.tsx
88182
- import { memo as memo45, useMemo as useMemo81 } from "react";
88282
+ import { memo as memo45, useMemo as useMemo82 } from "react";
88183
88283
  import { Fragment as Fragment57, jsx as jsx203, jsxs as jsxs162 } from "react/jsx-runtime";
88184
88284
  var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88185
88285
  samples,
@@ -88191,7 +88291,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88191
88291
  mode,
88192
88292
  accentColor = "#3CE6AC"
88193
88293
  }) {
88194
- const trailPoints = useMemo81(() => {
88294
+ const trailPoints = useMemo82(() => {
88195
88295
  if (!canvasRect) return "";
88196
88296
  if (trail2 && trail2.length > 1) {
88197
88297
  return trail2.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -88199,7 +88299,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88199
88299
  if (samples.length === 0) return "";
88200
88300
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
88201
88301
  }, [samples, trail2, sampleCount, canvasRect?.left, canvasRect?.top]);
88202
- const simplifiedPath = useMemo81(() => {
88302
+ const simplifiedPath = useMemo82(() => {
88203
88303
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
88204
88304
  const pts = [];
88205
88305
  for (const [pct, props] of simplifiedPoints) {
@@ -88211,7 +88311,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88211
88311
  if (pts.length === 0) return "";
88212
88312
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
88213
88313
  }, [simplifiedPoints]);
88214
- const diamondPositions = useMemo81(() => {
88314
+ const diamondPositions = useMemo82(() => {
88215
88315
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
88216
88316
  const pts = [];
88217
88317
  for (const [pct, props] of simplifiedPoints) {
@@ -88319,7 +88419,7 @@ function useGestureTrailOverlay({
88319
88419
  }
88320
88420
 
88321
88421
  // src/hooks/useStudioSidePanels.tsx
88322
- import { useMemo as useMemo87 } from "react";
88422
+ import { useMemo as useMemo88 } from "react";
88323
88423
 
88324
88424
  // src/components/StudioLeftPanels.tsx
88325
88425
  import { memo as memo49, useCallback as useCallback171 } from "react";
@@ -88441,7 +88541,7 @@ function MediaPreview({ projectId, filePath }) {
88441
88541
  }
88442
88542
 
88443
88543
  // src/components/sidebar/AssetsTab.tsx
88444
- import { memo as memo46, useState as useState137, useCallback as useCallback167, useRef as useRef172, useMemo as useMemo83, useEffect as useEffect140 } from "react";
88544
+ import { memo as memo46, useState as useState137, useCallback as useCallback167, useRef as useRef172, useMemo as useMemo84, useEffect as useEffect140 } from "react";
88445
88545
 
88446
88546
  // src/components/ui/SearchInput.tsx
88447
88547
  import { jsx as jsx206, jsxs as jsxs164 } from "react/jsx-runtime";
@@ -89053,7 +89153,7 @@ function AudioRow({
89053
89153
  }
89054
89154
 
89055
89155
  // src/components/sidebar/GlobalAssetsView.tsx
89056
- import { useEffect as useEffect137, useMemo as useMemo82, useState as useState134 } from "react";
89156
+ import { useEffect as useEffect137, useMemo as useMemo83, useState as useState134 } from "react";
89057
89157
  import { jsx as jsx209, jsxs as jsxs167 } from "react/jsx-runtime";
89058
89158
  function globalAssetRows(records, query = "") {
89059
89159
  const q = query.trim().toLowerCase();
@@ -89071,7 +89171,7 @@ function GlobalAssetsView({ searchQuery }) {
89071
89171
  const [records, setRecords] = useState134(null);
89072
89172
  useEffect137(() => {
89073
89173
  let cancelled = false;
89074
- fetch("/api/assets/global").then((r) => r.ok ? r.json() : { assets: [] }).then((d) => {
89174
+ studioApiFetch("/api/assets/global").then((r) => r.ok ? r.json() : { assets: [] }).then((d) => {
89075
89175
  if (!cancelled) setRecords(Array.isArray(d.assets) ? d.assets : []);
89076
89176
  }).catch(() => {
89077
89177
  if (!cancelled) setRecords([]);
@@ -89080,7 +89180,7 @@ function GlobalAssetsView({ searchQuery }) {
89080
89180
  cancelled = true;
89081
89181
  };
89082
89182
  }, []);
89083
- const rows = useMemo82(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
89183
+ const rows = useMemo83(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
89084
89184
  if (records === null) {
89085
89185
  return /* @__PURE__ */ jsx209("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
89086
89186
  }
@@ -89592,7 +89692,7 @@ var AssetsTab = memo46(function AssetsTab2({
89592
89692
  useEffect140(() => {
89593
89693
  if (manifest404Ref.current.has(projectId)) return;
89594
89694
  let cancelled = false;
89595
- fetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`)).then((r) => {
89695
+ studioApiFetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`)).then((r) => {
89596
89696
  if (!r.ok) {
89597
89697
  manifest404Ref.current.add(projectId);
89598
89698
  return "";
@@ -89642,12 +89742,12 @@ var AssetsTab = memo46(function AssetsTab2({
89642
89742
  setTimeout(() => setCopyFeedback(null), copied ? 1500 : 3e3);
89643
89743
  }, []);
89644
89744
  const elements = usePlayerStore((s) => s.elements);
89645
- const usedPaths = useMemo83(() => deriveUsedPaths(elements), [elements]);
89646
- const allMediaAssets = useMemo83(
89745
+ const usedPaths = useMemo84(() => deriveUsedPaths(elements), [elements]);
89746
+ const allMediaAssets = useMemo84(
89647
89747
  () => assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
89648
89748
  [assets]
89649
89749
  );
89650
- const mediaAssets = useMemo83(() => {
89750
+ const mediaAssets = useMemo84(() => {
89651
89751
  const all = filterByUsage(allMediaAssets, usedPaths, usageFilter);
89652
89752
  if (!searchQuery) return all;
89653
89753
  const q = searchQuery.toLowerCase();
@@ -89657,7 +89757,7 @@ var AssetsTab = memo46(function AssetsTab2({
89657
89757
  return rec?.description?.toLowerCase().includes(q);
89658
89758
  });
89659
89759
  }, [allMediaAssets, searchQuery, manifest, usageFilter, usedPaths]);
89660
- const categorized = useMemo83(() => {
89760
+ const categorized = useMemo84(() => {
89661
89761
  const groups = { audio: [], images: [], video: [], fonts: [] };
89662
89762
  for (const a of mediaAssets) {
89663
89763
  const cat = getCategory(a);
@@ -89672,12 +89772,12 @@ var AssetsTab = memo46(function AssetsTab2({
89672
89772
  }
89673
89773
  return groups;
89674
89774
  }, [mediaAssets, usedPaths]);
89675
- const counts = useMemo83(() => {
89775
+ const counts = useMemo84(() => {
89676
89776
  const c = { all: mediaAssets.length };
89677
89777
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
89678
89778
  return c;
89679
89779
  }, [mediaAssets, categorized]);
89680
- const usageCounts = useMemo83(
89780
+ const usageCounts = useMemo84(
89681
89781
  () => countUsage(
89682
89782
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
89683
89783
  usedPaths
@@ -90337,7 +90437,7 @@ function BlockCard({
90337
90437
  }
90338
90438
 
90339
90439
  // src/components/sidebar/CompositionsTab.tsx
90340
- import { memo as memo48, useCallback as useCallback170, useEffect as useEffect143, useLayoutEffect as useLayoutEffect15, useMemo as useMemo84, useRef as useRef175, useState as useState140 } from "react";
90440
+ import { memo as memo48, useCallback as useCallback170, useEffect as useEffect143, useLayoutEffect as useLayoutEffect15, useMemo as useMemo85, useRef as useRef175, useState as useState140 } from "react";
90341
90441
  import { jsx as jsx215, jsxs as jsxs172 } from "react/jsx-runtime";
90342
90442
  var DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
90343
90443
  var CARD_W = 80;
@@ -90479,7 +90579,7 @@ function CompCard({
90479
90579
  const thumbnailUrl2 = compositionCardThumbnailUrl(projectId, comp, contentRevision);
90480
90580
  const [thumbnailBox, nearViewport] = useNearViewport();
90481
90581
  const timelineSessionEpoch = usePlayerStore((state) => state.timelineSessionEpoch);
90482
- const thumbnailRequest = useMemo84(
90582
+ const thumbnailRequest = useMemo85(
90483
90583
  () => previewBooted && nearViewport ? compositionThumbnailRequest(thumbnailUrl2, projectId, {
90484
90584
  sessionEpoch: timelineSessionEpoch,
90485
90585
  rich: true
@@ -94102,7 +94202,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
94102
94202
  }
94103
94203
 
94104
94204
  // src/components/panels/VariablesPanel.tsx
94105
- import { memo as memo58, useCallback as useCallback183, useEffect as useEffect151, useMemo as useMemo85, useState as useState154 } from "react";
94205
+ import { memo as memo58, useCallback as useCallback183, useEffect as useEffect151, useMemo as useMemo86, useState as useState154 } from "react";
94106
94206
 
94107
94207
  // src/hooks/useVariablesPersist.ts
94108
94208
  import { useCallback as useCallback180 } from "react";
@@ -94798,22 +94898,22 @@ var VariablesPanel = memo58(function VariablesPanel2({
94798
94898
  reloadPreview,
94799
94899
  publishSdkSession
94800
94900
  });
94801
- const declarations = useMemo85(
94901
+ const declarations = useMemo86(
94802
94902
  () => sdkSession?.getVariableDeclarations() ?? [],
94803
94903
  // eslint-disable-next-line react-hooks/exhaustive-deps
94804
94904
  [sdkSession, refreshKey, revision]
94805
94905
  );
94806
- const usage = useMemo85(
94906
+ const usage = useMemo86(
94807
94907
  () => sdkSession?.getVariableUsage() ?? null,
94808
94908
  // eslint-disable-next-line react-hooks/exhaustive-deps
94809
94909
  [sdkSession, refreshKey, revision]
94810
94910
  );
94811
- const issues = useMemo85(
94911
+ const issues = useMemo86(
94812
94912
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
94813
94913
  // eslint-disable-next-line react-hooks/exhaustive-deps
94814
94914
  [sdkSession, previewValues, refreshKey, revision]
94815
94915
  );
94816
- const effectiveValues = useMemo85(
94916
+ const effectiveValues = useMemo86(
94817
94917
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
94818
94918
  // eslint-disable-next-line react-hooks/exhaustive-deps
94819
94919
  [sdkSession, previewValues, refreshKey, revision]
@@ -95128,15 +95228,15 @@ function useSlideshowPersist({
95128
95228
  }
95129
95229
 
95130
95230
  // src/hooks/useSlideshowTabState.ts
95131
- import { useMemo as useMemo86 } from "react";
95231
+ import { useMemo as useMemo87 } from "react";
95132
95232
  import { SLIDESHOW_ISLAND_TYPE as SLIDESHOW_ISLAND_TYPE2, slideshowIslandRegex as slideshowIslandRegex2 } from "@hyperframes/core/slideshow";
95133
95233
  function useSlideshowTabState(params) {
95134
95234
  const { editingFileContent, previewIframeRef, refreshKey, slideshowVisible } = params;
95135
- const isSlideshowComposition = useMemo86(() => {
95235
+ const isSlideshowComposition = useMemo87(() => {
95136
95236
  if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE2)) return false;
95137
95237
  return slideshowIslandRegex2("i").test(editingFileContent);
95138
95238
  }, [editingFileContent]);
95139
- const slideshowScenes = useMemo86(() => {
95239
+ const slideshowScenes = useMemo87(() => {
95140
95240
  try {
95141
95241
  const win = previewIframeRef.current?.contentWindow;
95142
95242
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -95313,7 +95413,7 @@ function useRemoveBackground(projectId, refreshFileTree, showToast) {
95313
95413
  return useCallback186(
95314
95414
  // fallow-ignore-next-line complexity
95315
95415
  async (inputPath, options) => {
95316
- const response = await fetch(
95416
+ const response = await studioApiFetch(
95317
95417
  `/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
95318
95418
  {
95319
95419
  method: "POST",
@@ -95751,7 +95851,7 @@ import { Fragment as Fragment66, jsx as jsx236, jsxs as jsxs189 } from "react/js
95751
95851
  function useStudioSidePanels(left, right, projectId) {
95752
95852
  const stableLeft = useStableHandlers(left, projectId);
95753
95853
  const stableRight = useStableHandlers(right, projectId);
95754
- return useMemo87(
95854
+ return useMemo88(
95755
95855
  () => /* @__PURE__ */ jsxs189(Fragment66, { children: [
95756
95856
  /* @__PURE__ */ jsx236(StudioLeftPanels, { ...stableLeft }),
95757
95857
  /* @__PURE__ */ jsx236(StudioRightPanels, { ...stableRight })
@@ -95890,7 +95990,7 @@ async function extendKeyframedTweenToPlayhead(session, sel, anim, currentTime, d
95890
95990
  position: extended.position,
95891
95991
  duration: extended.duration,
95892
95992
  keyframes: extended.keyframes,
95893
- ease: anim.ease
95993
+ ...keyframeEases(anim)
95894
95994
  },
95895
95995
  {
95896
95996
  label: "Add keyframe",
@@ -96770,7 +96870,7 @@ function StudioSplash({ waiting }) {
96770
96870
  import { useEffect as useEffect155, useState as useState156 } from "react";
96771
96871
  async function resolveHashProject(id) {
96772
96872
  try {
96773
- const res = await fetch(`/api/projects/${encodeURIComponent(id)}`);
96873
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(id)}`);
96774
96874
  if (res.ok) return { outcome: "ok", status: res.status };
96775
96875
  return { outcome: res.status === 404 ? "missing" : "unknown", status: res.status };
96776
96876
  } catch {
@@ -96782,7 +96882,7 @@ function replaceProjectHash(projectId) {
96782
96882
  }
96783
96883
  async function firstProjectId() {
96784
96884
  try {
96785
- const res = await fetch("/api/projects");
96885
+ const res = await studioApiFetch("/api/projects");
96786
96886
  const data = await res.json();
96787
96887
  return data.projects?.[0]?.id ?? null;
96788
96888
  } catch {
@@ -96809,7 +96909,7 @@ function useServerConnection() {
96809
96909
  retryTimer = window.setTimeout(tryConnect, 2e3);
96810
96910
  }
96811
96911
  function tryConnect() {
96812
- fetch("/api/projects").then((r) => r.json()).then(async (data) => {
96912
+ studioApiFetch("/api/projects").then((r) => r.json()).then(async (data) => {
96813
96913
  if (cancelled) return;
96814
96914
  if (hashProjectId) {
96815
96915
  const verdict = await resolveHashProject(hashProjectId);
@@ -96953,7 +97053,7 @@ function useCompositionContentLoader({
96953
97053
  if (!projectId) return;
96954
97054
  setActiveCompPath(comp.endsWith(".html") ? comp : null);
96955
97055
  setEditingFile({ path: comp, content: null });
96956
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`)).then(async (r) => {
97056
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`)).then(async (r) => {
96957
97057
  if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
96958
97058
  return r.json();
96959
97059
  }).then((data) => {
@@ -97074,7 +97174,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97074
97174
  setRefreshKey
97075
97175
  });
97076
97176
  const fileManager = useStableHandlers(fileManagerResult, projectId);
97077
- const masterCompPath = useMemo88(
97177
+ const masterCompPath = useMemo89(
97078
97178
  () => resolveMasterCompositionPath(fileManager.compositions),
97079
97179
  [fileManager.compositions]
97080
97180
  );
@@ -97178,7 +97278,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97178
97278
  } = useStableHandlers(blockHandlersResult, projectId);
97179
97279
  const dismissBlockParams = useCallback194(() => setActiveBlockParams(null), [setActiveBlockParams]);
97180
97280
  const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
97181
- const layout = useMemo88(
97281
+ const layout = useMemo89(
97182
97282
  () => ({ ...panelLayout, setRightPanelTab }),
97183
97283
  [panelLayout, setRightPanelTab]
97184
97284
  );
@@ -97227,9 +97327,9 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97227
97327
  handleDuplicate,
97228
97328
  onResetKeyframes: () => resetKeyframesRef.current(),
97229
97329
  onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
97230
- onAfterUndoRedo: (restore) => {
97330
+ onAfterUndoRedo: (restore2) => {
97231
97331
  invalidateGsapCacheRef.current();
97232
- timelineEditing.restoreLiveLanes(restore);
97332
+ timelineEditing.restoreLiveLanes(restore2);
97233
97333
  },
97234
97334
  onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
97235
97335
  onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
@@ -97403,7 +97503,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97403
97503
  handlePreviewIframeRef,
97404
97504
  refreshPreviewDocumentVersion
97405
97505
  });
97406
- const timelineToolbar = useMemo88(
97506
+ const timelineToolbar = useMemo89(
97407
97507
  () => /* @__PURE__ */ jsx244(
97408
97508
  TimelineToolbar,
97409
97509
  {
@@ -97912,7 +98012,7 @@ function findElementBlock(html2, elementId2) {
97912
98012
  }
97913
98013
 
97914
98014
  // src/hooks/useDomStyleCommit.ts
97915
- import { useCallback as useCallback196, useMemo as useMemo89, useRef as useRef189, useState as useState160 } from "react";
98015
+ import { useCallback as useCallback196, useMemo as useMemo90, useRef as useRef189, useState as useState160 } from "react";
97916
98016
  var noop = () => {
97917
98017
  };
97918
98018
  function useDomStyleCommit({
@@ -97928,7 +98028,7 @@ function useDomStyleCommit({
97928
98028
  const [queue2] = useState160(createDomEditSaveQueue);
97929
98029
  useMountEffect(() => () => queue2.destroy());
97930
98030
  const versions = useRef189(/* @__PURE__ */ new Map()).current;
97931
- const editHistory = useMemo89(() => ({ recordEdit }), [recordEdit]);
98031
+ const editHistory = useMemo90(() => ({ recordEdit }), [recordEdit]);
97932
98032
  const persistDomEditOperations = useDomEditPersist({
97933
98033
  activeCompPath,
97934
98034
  previewIframeRef: iframeRef,
@@ -97960,7 +98060,7 @@ function useDomStyleCommit({
97960
98060
  }
97961
98061
 
97962
98062
  // src/hooks/useDomGeometryCommit.ts
97963
- import { useCallback as useCallback197, useMemo as useMemo90, useRef as useRef190, useState as useState161 } from "react";
98063
+ import { useCallback as useCallback197, useMemo as useMemo91, useRef as useRef190, useState as useState161 } from "react";
97964
98064
  var noop2 = () => {
97965
98065
  };
97966
98066
  var NO_SELECTED_ANIMATIONS = [];
@@ -97976,18 +98076,14 @@ function useDomGeometryCommit({
97976
98076
  const projectIdRef = useRef190(projectId);
97977
98077
  projectIdRef.current = projectId;
97978
98078
  const pending2 = useRef190(/* @__PURE__ */ new Set()).current;
97979
- const editHistory = useMemo90(() => ({ recordEdit }), [recordEdit]);
97980
- const reload = useCallback197(
97981
- () => reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload(),
97982
- [reloadPreview, iframeRef]
97983
- );
98079
+ const editHistory = useMemo91(() => ({ recordEdit }), [recordEdit]);
97984
98080
  const { bump: bumpGsapCache } = useGsapCacheVersion();
97985
98081
  const gsap3 = useGsapScriptCommits({
97986
98082
  projectIdRef,
97987
98083
  activeCompPath,
97988
98084
  previewIframeRef: iframeRef,
97989
98085
  editHistory,
97990
- reloadPreview: reload,
98086
+ reloadPreview,
97991
98087
  onCacheInvalidate: bumpGsapCache,
97992
98088
  showToast,
97993
98089
  writeProjectFile
@@ -98033,6 +98129,7 @@ function useDomGeometryCommit({
98033
98129
  domEditSelection: null,
98034
98130
  selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
98035
98131
  gsapCommitMutation: gsap3.commitMutation,
98132
+ activeCompPath,
98036
98133
  previewIframeRef: iframeRef,
98037
98134
  showToast,
98038
98135
  bumpGsapCache,
@@ -98048,10 +98145,10 @@ function useDomGeometryCommit({
98048
98145
  updateArcSegment: gsap3.updateArcSegment
98049
98146
  });
98050
98147
  const saved2 = useCallback197(
98051
- async (commit, restore = noop2) => {
98148
+ async (commit, restore2 = noop2) => {
98052
98149
  const refusal2 = !projectIdRef.current ? "No project is open" : !isPreviewBooted(projectIdRef.current) ? "The preview for this project has not loaded yet" : null;
98053
98150
  if (refusal2) {
98054
- restore();
98151
+ restore2();
98055
98152
  showToast(refusal2, "error");
98056
98153
  throw new Error(refusal2);
98057
98154
  }
@@ -98065,11 +98162,11 @@ function useDomGeometryCommit({
98065
98162
  },
98066
98163
  [pending2, showToast]
98067
98164
  );
98068
- return useMemo90(
98165
+ return useMemo91(
98069
98166
  () => ({
98070
98167
  commitPathOffset: (selection, next, modifiers) => saved2(() => handleGsapAwarePathOffsetCommit(selection, next, modifiers)),
98071
98168
  commitGroupPathOffset: (updates) => saved2(() => handleGsapAwareGroupPathOffsetCommit(updates)),
98072
- commitBoxSize: (selection, next, offset, restore) => saved2(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore), restore),
98169
+ commitBoxSize: (selection, next, offset, restore2, route) => saved2(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore2, route), restore2),
98073
98170
  commitRotation: (selection, next) => saved2(() => handleGsapAwareRotationCommit(selection, next)),
98074
98171
  waitForPendingSaves: () => Promise.allSettled([...pending2]).then(noop2)
98075
98172
  }),
@@ -98093,6 +98190,7 @@ export {
98093
98190
  AudioMeterStrip,
98094
98191
  AudioWaveform,
98095
98192
  Button,
98193
+ ColorField,
98096
98194
  CompositionBreadcrumb,
98097
98195
  CompositionThumbnail,
98098
98196
  ConnectedDomEditOverlay,
@@ -98104,6 +98202,7 @@ export {
98104
98202
  EditorShell,
98105
98203
  ExternalFileConflictBanner,
98106
98204
  FileTree,
98205
+ GradientField,
98107
98206
  GridOverlay,
98108
98207
  HyperframesLoader,
98109
98208
  HyperframesLogo,
@@ -98223,6 +98322,7 @@ export {
98223
98322
  useProjectFileWriter,
98224
98323
  useRenderClipContent,
98225
98324
  useRenderQueue,
98325
+ useThumbnailStill,
98226
98326
  useTimelineContext,
98227
98327
  useTimelineEditing,
98228
98328
  useTimelinePlayer