hyperframes 0.8.115 → 0.8.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/{add-ZSLFODRZ.js → add-5AOKGJRR.js} +16 -16
  2. package/dist/{animatedGifPrep-72A45YER.js → animatedGifPrep-XKCVQ4ZL.js} +7 -7
  3. package/dist/{asset-SGDOHBBN.js → asset-QE74HE7H.js} +2 -2
  4. package/dist/{assetCataloger-Q7BCDARF.js → assetCataloger-3VJRWB3N.js} +3 -3
  5. package/dist/{auth-5JGFN7SV.js → auth-HQE2GKY2.js} +2 -2
  6. package/dist/{autoUpdate-ZVTMYO7Z.js → autoUpdate-MH57H575.js} +2 -2
  7. package/dist/{backgroundChecks-IY3G5G2D.js → backgroundChecks-JP2GHEP4.js} +3 -3
  8. package/dist/backgroundChecksWorker.js +3 -3
  9. package/dist/{batchRender-QVLLL5DN.js → batchRender-KIFBKKVN.js} +4 -4
  10. package/dist/{beats-66KZIB5V.js → beats-V334Q2GV.js} +13 -13
  11. package/dist/{benchmark-AWFKSWQH.js → benchmark-62QZH5YE.js} +13 -13
  12. package/dist/{browser-J54EXMCE.js → browser-EK7OBU6W.js} +10 -10
  13. package/dist/{bundleWithLocalizedFonts-BLLHP2TO.js → bundleWithLocalizedFonts-263BUY7C.js} +2 -2
  14. package/dist/{capture-SLI2I5BS.js → capture-56HMZGJW.js} +10 -10
  15. package/dist/{capture-D4HRSVSK.js → capture-6F3XPKXI.js} +2 -2
  16. package/dist/{catalog-BUMLQU7L.js → catalog-JO7WHBYC.js} +16 -16
  17. package/dist/{check-HEZUK62C.js → check-S4RZIS26.js} +33 -33
  18. package/dist/{checkBrowser-RVJM6IUO.js → checkBrowser-NVWULRE7.js} +32 -32
  19. package/dist/{chunk-MPB7BQT7.js → chunk-23KZNTIV.js} +1 -1
  20. package/dist/{chunk-IOJAVV6J.js → chunk-2E4A277E.js} +1 -1
  21. package/dist/{chunk-CVBWF2MD.js → chunk-2HE5ZOXK.js} +1 -1
  22. package/dist/{chunk-TWJT2RGZ.js → chunk-34AE4KPB.js} +4 -4
  23. package/dist/{chunk-5MXN7WM4.js → chunk-63O3AR5U.js} +3 -3
  24. package/dist/{chunk-WZYHTXZY.js → chunk-6FRHKGRY.js} +1 -1
  25. package/dist/{chunk-AXXK3LWL.js → chunk-6TSA2KDM.js} +1 -1
  26. package/dist/{chunk-YNHP6OFJ.js → chunk-7Z2XNQ7F.js} +2 -2
  27. package/dist/{chunk-XMAL2A7Y.js → chunk-AIYKVE4Y.js} +1 -1
  28. package/dist/{chunk-IEE7GIVX.js → chunk-AKZD7VWZ.js} +2 -2
  29. package/dist/{chunk-ORAISNGN.js → chunk-C47XJXSK.js} +10 -10
  30. package/dist/{chunk-2H37RGJ2.js → chunk-CCWSLTWA.js} +10 -10
  31. package/dist/{chunk-SSZYJCAL.js → chunk-CHWO3UN6.js} +2 -2
  32. package/dist/{chunk-Y2Y4LBJD.js → chunk-CVE7OUZE.js} +3 -3
  33. package/dist/{chunk-3R6P5BIG.js → chunk-DRLB5STV.js} +2 -2
  34. package/dist/{chunk-KLZ7OCHR.js → chunk-ENQL7NCM.js} +3 -3
  35. package/dist/{chunk-T5HXGUM3.js → chunk-EWR3SLQV.js} +1 -1
  36. package/dist/{chunk-SAO3NQPF.js → chunk-EWUI4PU6.js} +1 -1
  37. package/dist/{chunk-2GWVY66H.js → chunk-FERBBWDU.js} +1 -1
  38. package/dist/{chunk-QXNRRMBQ.js → chunk-FFF36PQA.js} +2 -2
  39. package/dist/{chunk-UUDQD62L.js → chunk-FSLYPQ5Z.js} +1 -1
  40. package/dist/{chunk-4P2TOD7N.js → chunk-GNQF4NZW.js} +1 -1
  41. package/dist/{chunk-HOBR4IHC.js → chunk-IGWZCCLO.js} +7 -7
  42. package/dist/{chunk-2DX6JYL2.js → chunk-IX75NQSA.js} +2 -2
  43. package/dist/{chunk-SK53U3PF.js → chunk-JBZYEZPZ.js} +1 -1
  44. package/dist/{chunk-6SEUZGID.js → chunk-JYB7NKO6.js} +3 -3
  45. package/dist/{chunk-HPWPLV5J.js → chunk-KNCLHJB5.js} +3 -3
  46. package/dist/{chunk-M7XTMZF5.js → chunk-MZPMAU25.js} +2 -2
  47. package/dist/{chunk-SC7ICSIU.js → chunk-NCQV2NMY.js} +1 -1
  48. package/dist/{chunk-YJEGT5L6.js → chunk-NITBGIHR.js} +2 -2
  49. package/dist/{chunk-PZTEY73C.js → chunk-NRTZ2JPC.js} +1 -1
  50. package/dist/{chunk-BDNCUMZM.js → chunk-PAZUQZYV.js} +1 -1
  51. package/dist/{chunk-3J4EBX5M.js → chunk-PHBPRHKK.js} +1 -1
  52. package/dist/{chunk-3YTWSGBV.js → chunk-QITH7ALB.js} +4 -4
  53. package/dist/{chunk-XJQ7TRLG.js → chunk-RTLYNXAC.js} +1 -1
  54. package/dist/{chunk-4HIF3LI3.js → chunk-S5Q46UEI.js} +5 -5
  55. package/dist/{chunk-PLP4K6WZ.js → chunk-SXV67HVP.js} +92 -34
  56. package/dist/{chunk-ZBQP54CD.js → chunk-TEYBS6T3.js} +5 -5
  57. package/dist/{chunk-RDMAU7AJ.js → chunk-TRLP3CSL.js} +1 -1
  58. package/dist/{chunk-QMFCL2ZN.js → chunk-TY76XMMP.js} +2 -2
  59. package/dist/{chunk-SOEQWMNS.js → chunk-VJ2RAP65.js} +2 -2
  60. package/dist/{chunk-TMCJPHPP.js → chunk-VLB4NGRR.js} +1 -1
  61. package/dist/{chunk-VDASTWUM.js → chunk-VU5IDWKX.js} +2 -2
  62. package/dist/{chunk-P73WQPSB.js → chunk-WN62HDA7.js} +1 -1
  63. package/dist/{chunk-BQG33YTI.js → chunk-WUUEWXA2.js} +1 -1
  64. package/dist/chunk-WWL6YGYK.js +3541 -0
  65. package/dist/{chunk-74RAXWDF.js → chunk-XTCMIITZ.js} +1 -1
  66. package/dist/{chunk-5YSA3YXO.js → chunk-YX55Y7UO.js} +9 -9
  67. package/dist/{clean-X6Y252ZC.js → clean-DPW2FTMM.js} +17 -17
  68. package/dist/cli.js +50 -50
  69. package/dist/{cloud-EE4GALWR.js → cloud-OAYSRXAJ.js} +4 -4
  70. package/dist/{cloudrun-Y6A42KHE.js → cloudrun-NM2ZMAJV.js} +5 -5
  71. package/dist/{compare-K6XJZ3OC.js → compare-CZC6ROYF.js} +27 -27
  72. package/dist/{compiler-DYACPLUW.js → compiler-3MFRD377.js} +3 -3
  73. package/dist/{component-T6IYSCFP.js → component-6D5RBOWZ.js} +3 -3
  74. package/dist/{compositions-4KUTNS5L.js → compositions-5KCUVODZ.js} +12 -12
  75. package/dist/{delete-C64TWAPE.js → delete-ZGLELTUJ.js} +2 -2
  76. package/dist/{deterministicFonts-A4CNCSAL.js → deterministicFonts-O7BTFDJU.js} +2 -2
  77. package/dist/{dist-HRBNHR4M.js → dist-JAOG6MYW.js} +1 -1
  78. package/dist/{dist-KNGSQALD.js → dist-XSSXGBRH.js} +3 -3
  79. package/dist/{doctor-WJXZK5QN.js → doctor-CICSVC7I.js} +11 -11
  80. package/dist/{events-QRWJBSV4.js → events-V3O6Q5ZH.js} +9 -9
  81. package/dist/{events-FVG3Y6Q4.js → events-Y4NBIJBP.js} +10 -10
  82. package/dist/{feedback-NQHRUOZP.js → feedback-EA3K6QXT.js} +11 -11
  83. package/dist/{figma-MEX4CSIM.js → figma-WOOHCXDA.js} +3 -3
  84. package/dist/fontLocalizeCli.js +10 -10
  85. package/dist/{get-CMJIHGSA.js → get-64SDPINM.js} +2 -2
  86. package/dist/{gpuPolicy-5S4OZDME.js → gpuPolicy-TFCLAD64.js} +7 -7
  87. package/dist/{grade-compare-NIDMIUGZ.js → grade-compare-NXAFWDBR.js} +27 -27
  88. package/dist/{gsapParserAcorn-SZANFUAQ.js → gsapParserAcorn-FLWABPQM.js} +1 -1
  89. package/dist/{help-G6AWAWJW.js → help-6H4ICA27.js} +1 -1
  90. package/dist/{history-ZBOK2JQF.js → history-TPRRROOR.js} +17 -17
  91. package/dist/hyperframe-runtime.js +50 -50
  92. package/dist/hyperframe.manifest.json +1 -1
  93. package/dist/hyperframe.runtime.iife.js +50 -50
  94. package/dist/hyperframes-player.global.js +1 -1
  95. package/dist/{info-2ORQZ6I2.js → info-DDXWWAM3.js} +12 -12
  96. package/dist/{init-3PSKH6UR.js → init-KULZFAS7.js} +23 -23
  97. package/dist/{inspect-NR54RMEE.js → inspect-VEJH72DJ.js} +28 -28
  98. package/dist/{installer-YQUWHYBZ.js → installer-TBL3PE2J.js} +5 -5
  99. package/dist/{keyframes-KT7T4AEV.js → keyframes-W6KIRZ4O.js} +13 -13
  100. package/dist/{lambda-3FF4V3X3.js → lambda-JJXODPS5.js} +6 -6
  101. package/dist/{layout-RB3M37ER.js → layout-KVG7PJ3F.js} +28 -28
  102. package/dist/{lint-HR57M5YP.js → lint-GQ5IVVCN.js} +13 -13
  103. package/dist/{lintProject-TOLC6HDS.js → lintProject-FDJYTOVM.js} +1 -1
  104. package/dist/{list-YDXYHXHF.js → list-QZ7LV6HF.js} +2 -2
  105. package/dist/{login-N6UNRVYW.js → login-526R3KCB.js} +4 -4
  106. package/dist/{media-treatment-UXJOTNRO.js → media-treatment-H7LGSO3I.js} +13 -13
  107. package/dist/{models-Z3XFMGXB.js → models-WOEP555T.js} +8 -8
  108. package/dist/{motionShot-KQMCXJ77.js → motionShot-235LNUY7.js} +12 -12
  109. package/dist/{normalize-audio-XJQMDWRV.js → normalize-audio-GTAESDG6.js} +12 -12
  110. package/dist/{pipeline-U3OISHWV.js → pipeline-NBHOEZPA.js} +7 -7
  111. package/dist/{play-XP77PML5.js → play-SBTJLOWX.js} +22 -22
  112. package/dist/{preflight-CANFVC4Y.js → preflight-KQCXRC6W.js} +8 -8
  113. package/dist/{present-MWGA3O4S.js → present-TXULFA2J.js} +18 -18
  114. package/dist/{preview-X5GWZ3Q6.js → preview-E2U4ER7H.js} +18 -18
  115. package/dist/{producer-B2IHU7MA.js → producer-DR3TOPSC.js} +1 -1
  116. package/dist/{projectConfig-2A64YU5H.js → projectConfig-QBYFCJYE.js} +7 -7
  117. package/dist/{publication-CFYBPLRS.js → publication-TWGRIP5F.js} +3 -3
  118. package/dist/{publish-WE6AS77D.js → publish-JOUHOKGX.js} +9 -9
  119. package/dist/{remove-background-XSDOCVN5.js → remove-background-GQZIZBHM.js} +8 -8
  120. package/dist/{render-5TMRIC73.js → render-2LC2DUR3.js} +13 -13
  121. package/dist/{render-QX6AKB6Y.js → render-JR3YIYZ4.js} +4 -4
  122. package/dist/{render-HVZUCLYW.js → render-UVGS7PLJ.js} +22 -22
  123. package/dist/{render-batch-QJXFVTDE.js → render-batch-MYH6ZGO3.js} +4 -4
  124. package/dist/{renderCancellation-WSSSHRFR.js → renderCancellation-EIBZHTFW.js} +7 -7
  125. package/dist/renderSetupWorker.js +1 -1
  126. package/dist/{resolver-WTXO6WXA.js → resolver-4Z5YISAU.js} +5 -5
  127. package/dist/{sherpa-S6ZBBEFO.js → sherpa-UX3MQXO5.js} +8 -8
  128. package/dist/{skills-CDRSTETB.js → skills-GXETMIED.js} +12 -12
  129. package/dist/{skillsUpdateCheck-FKREJXSF.js → skillsUpdateCheck-XS7OE6FX.js} +3 -3
  130. package/dist/{snapshot-T4PFSXTH.js → snapshot-OAHQ3EVJ.js} +28 -28
  131. package/dist/{src-5BZDQAVN.js → src-AZ5YCYQP.js} +9 -9
  132. package/dist/{src-IZMCUYK7.js → src-X6ACPJHG.js} +6 -6
  133. package/dist/{staticProjectServer-VN4ARAH4.js → staticProjectServer-P4QSLLF6.js} +16 -16
  134. package/dist/{status-OJZWJB53.js → status-KDRWJBPW.js} +7 -7
  135. package/dist/studio/assets/{hyperframes-player-C-9N8nk0.js → hyperframes-player-mGYvPVpw.js} +1 -1
  136. package/dist/studio/assets/{index-BoUM6Xqb.js → index-BWHt0-ir.js} +1 -1
  137. package/dist/studio/assets/{index-CAvZ0AA-.js → index-DYNaXPVf.js} +201 -201
  138. package/dist/studio/assets/{index-CVuorIMv.js → index-Dr3hNpvS.js} +1 -1
  139. package/dist/studio/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
  140. package/dist/studio/chunk-3F47EFCT.js.map +1 -0
  141. package/dist/studio/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
  142. package/dist/studio/index.d.ts +104 -22
  143. package/dist/studio/index.html +1 -1
  144. package/dist/studio/index.js +1131 -1044
  145. package/dist/studio/index.js.map +1 -1
  146. package/dist/{studio-api-CIO2NKK5.js → studio-api-SYQOPCMB.js} +10 -10
  147. package/dist/{studioServer-G5GNYWDI.js → studioServer-ANSX6DY2.js} +42 -42
  148. package/dist/{system-PPNNGVIM.js → system-B6UYANYL.js} +7 -7
  149. package/dist/{telemetry-GMDMISOX.js → telemetry-AZEW2DZP.js} +10 -10
  150. package/dist/{timeline-5LSQX4OS.js → timeline-VIKY65W6.js} +17 -17
  151. package/dist/{tokens-VBXAT7CK.js → tokens-SSGCAC7E.js} +2 -2
  152. package/dist/{transcribe-GYCAIIEQ.js → transcribe-O33POKJY.js} +13 -13
  153. package/dist/{transcribe-VGYAWRXS.js → transcribe-PF6BUHHQ.js} +8 -8
  154. package/dist/{updateCheck-AZEREUQV.js → updateCheck-Y4SUY6VE.js} +2 -2
  155. package/dist/{upgrade-S63QN2LA.js → upgrade-EGIS5OCB.js} +2 -2
  156. package/dist/{urlDownloader-SM6ODN5O.js → urlDownloader-UBY27GFW.js} +6 -6
  157. package/dist/{validate-7MNUK5MH.js → validate-FE74RMPD.js} +17 -17
  158. package/dist/{video-JEOHARTC.js → video-DODRESEY.js} +7 -7
  159. package/package.json +1 -1
  160. package/dist/chunk-RRPJEHHW.js +0 -3541
  161. package/dist/studio/chunk-TXZMMOMA.js.map +0 -1
  162. /package/dist/studio/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
@@ -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
  }
@@ -16929,8 +16967,8 @@ function addTweenTargets(tween, targets) {
16929
16967
  targets.set(el, seen);
16930
16968
  }
16931
16969
  }
16932
- function collectResetTargets(win, doc, targetKeys, reparse = []) {
16933
- const targets = new Map(reparse.map((el) => [el, /* @__PURE__ */ new Set()]));
16970
+ function collectResetTargets(win, doc, targetKeys) {
16971
+ const targets = /* @__PURE__ */ new Map();
16934
16972
  const timelines = win.__timelines ?? {};
16935
16973
  const others = new Set(
16936
16974
  Object.entries(timelines).flatMap(([key2, tl]) => targetKeys.includes(key2) ? [] : [tl])
@@ -17090,7 +17128,7 @@ function timelineKeys(scriptText) {
17090
17128
  return [...scriptText.matchAll(/__timelines\s*\[\s*["'`]([^"'`]+)["'`]\s*\]/g)].map((m) => m[1]).filter((key2) => key2 !== "__proxied");
17091
17129
  }
17092
17130
  function applySoftReload(iframe, scriptText, options = {}) {
17093
- const { onAsyncFailure, currentTimeOverride, authoredHtml, reparse = [] } = options;
17131
+ const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
17094
17132
  if (!iframe || !scriptText) return "cannot-soft-reload";
17095
17133
  const win = iframe.contentWindow;
17096
17134
  const doc = iframe.contentDocument;
@@ -17120,7 +17158,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
17120
17158
  const readAuthoredOpacity = (el) => findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
17121
17159
  const doReload = () => {
17122
17160
  const timelines = win.__timelines;
17123
- const targets = collectResetTargets(win, doc, targetKeys, reparse);
17161
+ const targets = collectResetTargets(win, doc, targetKeys);
17124
17162
  if (timelines) {
17125
17163
  for (const key2 of targetKeys) {
17126
17164
  const tl = timelines[key2];
@@ -17289,7 +17327,7 @@ var GsapPreviewConvergenceError = class extends Error {
17289
17327
  var GsapOwnershipProtocolError = class extends GsapPreviewConvergenceError {
17290
17328
  };
17291
17329
  async function requireGsapOwnershipProtocol(projectId) {
17292
- const response = await fetch(
17330
+ const response = await studioApiFetch(
17293
17331
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-capabilities`
17294
17332
  );
17295
17333
  if (!response.ok) {
@@ -17300,18 +17338,18 @@ async function requireGsapOwnershipProtocol(projectId) {
17300
17338
  throw new GsapOwnershipProtocolError("Invalid GSAP mutation capability response");
17301
17339
  }
17302
17340
  }
17303
- async function rollbackOwnedMutation(projectId, targetPath, expected, restore) {
17341
+ async function rollbackOwnedMutation(projectId, targetPath, expected, restore2) {
17304
17342
  if (targetPath.includes("\0") || targetPath.includes("..")) {
17305
17343
  throw new Error(`Unsafe path: ${targetPath}`);
17306
17344
  }
17307
- const response = await fetch(
17345
+ const response = await studioApiFetch(
17308
17346
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-rollback/${encodeURIComponent(targetPath)}`,
17309
17347
  {
17310
17348
  method: "POST",
17311
17349
  // Deliberately unclaimed: a rollback runs because a mutation did not
17312
17350
  // converge, so let the restored file reload the preview.
17313
17351
  headers: { "Content-Type": "application/json" },
17314
- body: JSON.stringify({ expected, restore })
17352
+ body: JSON.stringify({ expected, restore: restore2 })
17315
17353
  }
17316
17354
  );
17317
17355
  if (!response.ok) {
@@ -17343,7 +17381,7 @@ function readMutationError(value, fallback) {
17343
17381
  async function postGsapMutation(projectId, filePath, mutation, fallback) {
17344
17382
  let response;
17345
17383
  try {
17346
- response = await fetch(
17384
+ response = await studioApiFetch(
17347
17385
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(filePath)}`,
17348
17386
  {
17349
17387
  method: "POST",
@@ -17370,7 +17408,7 @@ async function readFileContent(projectId, targetPath) {
17370
17408
  if (targetPath.includes("\0") || targetPath.includes("..")) {
17371
17409
  throw new Error(`Unsafe path: ${targetPath}`);
17372
17410
  }
17373
- const response = await fetch(
17411
+ const response = await studioApiFetch(
17374
17412
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(targetPath)}`
17375
17413
  );
17376
17414
  if (!response.ok) {
@@ -20480,6 +20518,10 @@ function normalizeDomEditStyleValue(property, value) {
20480
20518
  function isImageBackgroundValue(value) {
20481
20519
  return /^url\(/i.test(value.trim());
20482
20520
  }
20521
+ function cssPropertyName(property) {
20522
+ if (property.startsWith("--")) return property;
20523
+ return property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`).replace(/^ms-/, "-ms-");
20524
+ }
20483
20525
  function isManualGeometryStyleProperty(property) {
20484
20526
  return property === "left" || property === "top" || property === "width" || property === "height";
20485
20527
  }
@@ -20626,7 +20668,7 @@ function isPlainRecord(value) {
20626
20668
  }
20627
20669
  async function resolveAssetHasAudio(projectId, assetPath) {
20628
20670
  try {
20629
- const response = await fetch(mediaMetadataUrl(projectId, assetPath));
20671
+ const response = await studioApiFetch(mediaMetadataUrl(projectId, assetPath));
20630
20672
  if (!response.ok) return null;
20631
20673
  const data = await response.json();
20632
20674
  const metadata = isPlainRecord(data) ? data.metadata : void 0;
@@ -22356,7 +22398,7 @@ function TimelineTrackRow({
22356
22398
  ),
22357
22399
  propertyRows.map((row) => {
22358
22400
  const group = row.propertyGroup;
22359
- const keyframeCount = row.items.filter((item) => item.kind === "keyframe").length;
22401
+ const keyframeCount2 = row.items.filter((item) => item.kind === "keyframe").length;
22360
22402
  const easeCount = row.items.filter((item) => item.kind === "ease").length;
22361
22403
  return (
22362
22404
  // ponytail: aria-owns maps this hidden logical row onto the two visible
@@ -22387,7 +22429,7 @@ function TimelineTrackRow({
22387
22429
  "aria-colindex": 2,
22388
22430
  "aria-owns": timelineLogicalRowCellId(lanesId, row.id, "content"),
22389
22431
  children: [
22390
- keyframeCount,
22432
+ keyframeCount2,
22391
22433
  " keyframes, ",
22392
22434
  easeCount,
22393
22435
  " ease controls"
@@ -24125,7 +24167,7 @@ var peakMapRequests = /* @__PURE__ */ new Map();
24125
24167
  function loadPeakMap(url) {
24126
24168
  const pending2 = peakMapRequests.get(url);
24127
24169
  if (pending2) return pending2;
24128
- const request = fetch(url).then((res) => res.ok ? res.json() : null).then((body) => isPeakMap(body) ? body : null).catch(() => null);
24170
+ const request = studioApiFetch(url).then((res) => res.ok ? res.json() : null).then((body) => isPeakMap(body) ? body : null).catch(() => null);
24129
24171
  peakMapRequests.set(url, request);
24130
24172
  return request;
24131
24173
  }
@@ -24200,7 +24242,7 @@ function isNormalizePlan(value) {
24200
24242
  (key2) => typeof Reflect.get(value, key2) === "number"
24201
24243
  );
24202
24244
  }
24203
- async function requestNormalizePlan(projectId, el, fetchImpl = fetch) {
24245
+ async function requestNormalizePlan(projectId, el, fetchImpl = studioApiFetch) {
24204
24246
  const origin2 = window.location.origin;
24205
24247
  const relative = resolvePreviewRelative(
24206
24248
  resolveMediaPreviewUrl(el.src ?? "", projectId, origin2),
@@ -25257,7 +25299,7 @@ async function measureCarve(doc, voices, strength, bed) {
25257
25299
  const Ctor = window.OfflineAudioContext ?? window.webkitOfflineAudioContext;
25258
25300
  if (!Ctor) return null;
25259
25301
  const decode = async (relative, clock) => {
25260
- const res = await fetch(new URL(relative, doc.baseURI).href);
25302
+ const res = await studioApiFetch(new URL(relative, doc.baseURI).href);
25261
25303
  const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE).decodeAudioData(
25262
25304
  await res.arrayBuffer()
25263
25305
  );
@@ -27147,7 +27189,7 @@ function compositionThumbnailRequest(url, projectId, { sessionEpoch = 0, priorit
27147
27189
  };
27148
27190
  }
27149
27191
  async function loadCompositionImage(url, signal) {
27150
- const response = await fetch(url, { signal });
27192
+ const response = await studioApiFetch(url, { signal });
27151
27193
  if (!response.ok) throw new Error(`Composition thumbnail failed (${response.status})`);
27152
27194
  const blob = await response.blob();
27153
27195
  if (signal.aborted) throw new DOMException("Aborted", "AbortError");
@@ -29921,7 +29963,7 @@ function sceneSwapFor(iframe) {
29921
29963
  );
29922
29964
  cancel?.addEventListener("abort", () => deadline.abort(cancel.reason), { once: true });
29923
29965
  const swapping = (async () => {
29924
- const response = await fetch(url, { signal: deadline.signal });
29966
+ const response = await studioApiFetch(url, { signal: deadline.signal });
29925
29967
  if (!response.ok) throw new Error(`preview request failed with ${response.status}`);
29926
29968
  const html2 = await response.text();
29927
29969
  if (!isCurrent()) throw new Error("superseded by a newer reload");
@@ -31048,7 +31090,9 @@ function useTimelinePlayer({
31048
31090
  const slot = previewGeneration();
31049
31091
  const isCurrent = () => gen === refreshGenRef.current && slot === previewGeneration();
31050
31092
  const swap = sceneSwapFor(iframe);
31051
- if (!swap || isRefreshingRef.current) return reloadWholeFilm(url.toString());
31093
+ const swapWouldReplaceGestureNode = !!iframe.contentDocument && isStudioManualEditGestureLiveIn(iframe.contentDocument);
31094
+ if (!swap || isRefreshingRef.current || swapWouldReplaceGestureNode)
31095
+ return reloadWholeFilm(url.toString());
31052
31096
  swap(url.toString(), isCurrent, cancel.signal).catch((error) => {
31053
31097
  if (!isCurrent()) return;
31054
31098
  logReload("scene-swap-refused", { reason: String(error) });
@@ -31363,7 +31407,7 @@ function NLEProvider({
31363
31407
  setCompIdToSrc(emptyMap);
31364
31408
  setCompositionSourceMap(emptyMap);
31365
31409
  onCompIdToSrcChangeRef.current?.(emptyMap);
31366
- fetch(buildProjectApiPath(projectId, `/files/index.html`), {
31410
+ studioApiFetch(buildProjectApiPath(projectId, `/files/index.html`), {
31367
31411
  signal: controller.signal
31368
31412
  }).then((r) => {
31369
31413
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
@@ -31899,7 +31943,7 @@ function renderPosterForNextOpen(projectId) {
31899
31943
  priority: "overscan",
31900
31944
  discardWhenReleased: true,
31901
31945
  load: async (signal) => {
31902
- await (await fetch(url, { signal })).arrayBuffer();
31946
+ await (await studioApiFetch(url, { signal })).arrayBuffer();
31903
31947
  return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
31904
31948
  }
31905
31949
  };
@@ -35897,7 +35941,7 @@ function OffCanvasIndicators({
35897
35941
  const selectOffCanvas = async () => {
35898
35942
  const el = elements.current.get(r.key);
35899
35943
  if (!el) return;
35900
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-D7VYFJJD.js");
35944
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-OL7GLSS4.js");
35901
35945
  const acp = activeCompositionPathRef.current ?? "index.html";
35902
35946
  const sel = await resolveDomEditSelection2(el, {
35903
35947
  activeCompositionPath: acp,
@@ -36563,6 +36607,13 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
36563
36607
  baseGsapY
36564
36608
  };
36565
36609
  }
36610
+ function restoreDragOffset(element) {
36611
+ const origin2 = (axis) => Number.parseFloat(element.getAttribute(`data-hf-drag-initial-offset-${axis}`) ?? "") || 0;
36612
+ element.style.setProperty("--hf-studio-offset-x", `${origin2("x")}px`);
36613
+ element.style.setProperty("--hf-studio-offset-y", `${origin2("y")}px`);
36614
+ element.removeAttribute("data-hf-drag-initial-offset-x");
36615
+ element.removeAttribute("data-hf-drag-initial-offset-y");
36616
+ }
36566
36617
 
36567
36618
  // src/components/editor/plainTranslate.ts
36568
36619
  var TOKEN = /\s*(?:(\d*\.?\d+(?:e[-+]?\d+)?)(px|%)?|([a-z]+)\(|([-+*/(),]))/giy;
@@ -36723,11 +36774,11 @@ function getRectCenter(element) {
36723
36774
  if (!finiteRect(rect) || rect.width <= 0 && rect.height <= 0) {
36724
36775
  return null;
36725
36776
  }
36726
- let point = {
36727
- x: rect.left + rect.width / 2,
36728
- y: rect.top + rect.height / 2
36729
- };
36730
- let win = element.ownerDocument.defaultView;
36777
+ const point = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
36778
+ return toTopScreenPoint(element.ownerDocument, point);
36779
+ }
36780
+ function toTopScreenPoint(doc, point) {
36781
+ let win = doc.defaultView;
36731
36782
  while (win) {
36732
36783
  const frameElement = getFrameElement(win);
36733
36784
  if (!frameElement) break;
@@ -38427,14 +38478,14 @@ function createDomEditOverlayGestureHandlers(opts) {
38427
38478
  hasMember: !!member,
38428
38479
  inlineStyle: sel.element.getAttribute("style")
38429
38480
  });
38430
- const restore = () => {
38481
+ const restore2 = () => {
38431
38482
  if (!g.manualEditDragToken || !isStudioManualEditGestureCurrent(sel.element, g.manualEditDragToken))
38432
38483
  return;
38433
38484
  restoreStudioBoxSize(sel.element, g.initialBoxSize);
38434
38485
  if (finalOffset) restoreStudioPathOffset(sel.element, g.initialPathOffset);
38435
38486
  };
38436
38487
  void Promise.resolve(
38437
- opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore)
38488
+ opts.onBoxSizeCommitRef.current(sel, finalSize, finalOffset ?? void 0, restore2)
38438
38489
  ).then((result) => trackPreviewEditResult("resize", "drag", result)).catch((error) => {
38439
38490
  logGestureCommitFailure("resize commit failed", error);
38440
38491
  }).finally(() => {
@@ -41580,77 +41631,6 @@ function isDomEditPersistOutcome(value) {
41580
41631
  return typeof Reflect.get(value, "sourceFile") === "string" && typeof Reflect.get(value, "version") === "string" && typeof Reflect.get(value, "changed") === "boolean";
41581
41632
  }
41582
41633
 
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
41634
  // src/utils/sdkOpMapping.ts
41655
41635
  function patchOpsToSdkEditOps(hfId, ops) {
41656
41636
  const result = [];
@@ -42772,7 +42752,7 @@ function useElementLifecycleOps({
42772
42752
  writeFile: writeProjectFile,
42773
42753
  recordEdit: editHistory.recordEdit,
42774
42754
  rewrite: async (originalContent) => {
42775
- const removeResponse = await fetch(
42755
+ const removeResponse = await studioApiFetch(
42776
42756
  buildProjectApiPath(
42777
42757
  pid,
42778
42758
  `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`
@@ -44716,7 +44696,9 @@ import { editabilityForProvenance } from "@hyperframes/core/gsap-parser";
44716
44696
  var GSAP_EDIT_BLOCK_COPY = {
44717
44697
  "no-selector": "This layer needs a stable selector before Studio can save the edit.",
44718
44698
  "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."
44699
+ "source-uneditable": "This animation is computed at runtime. Edit the animation in the Code tab.",
44700
+ "keyframes-uneditable": "Studio can't add this edit to the animation's keyframes. Edit this animation in the Code tab.",
44701
+ "mixed-files": "These layers are animated in different files. Move each file's layers separately."
44720
44702
  };
44721
44703
  var GsapEditBlockedError = class extends Error {
44722
44704
  constructor(reason, detail) {
@@ -44943,62 +44925,6 @@ async function commitStaticGsapSize(selection, size, selector, existingSet, call
44943
44925
  { label: "Resize layer", softReload: true }
44944
44926
  );
44945
44927
  }
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
44928
  async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, iframe, selector, callbacks) {
45003
44929
  const el = selection.element;
45004
44930
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
@@ -45023,7 +44949,6 @@ async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, ifr
45023
44949
  }
45024
44950
  const ts = resolveTweenStart(effectiveAnim);
45025
44951
  const td = resolveTweenDuration(effectiveAnim);
45026
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
45027
44952
  let kfs = effectiveAnim.keyframes?.keyframes ?? [];
45028
44953
  if (kfs.length === 0) {
45029
44954
  const fromProps = effectiveAnim.fromProperties ?? {};
@@ -45055,7 +44980,7 @@ async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, ifr
45055
44980
  position: roundTo3(ts ?? 0),
45056
44981
  duration: roundTo3(td || 1),
45057
44982
  keyframes: shifted,
45058
- ease
44983
+ ...keyframeEases(effectiveAnim)
45059
44984
  },
45060
44985
  { label: "Move animation path", softReload: true, beforeReload: restoreOffset }
45061
44986
  );
@@ -45070,7 +44995,6 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
45070
44995
  }
45071
44996
  const ts = resolveTweenStart(effectiveAnim);
45072
44997
  const td = resolveTweenDuration(effectiveAnim);
45073
- const ease = effectiveAnim.keyframes?.easeEach ?? effectiveAnim.ease;
45074
44998
  const keys = Object.keys(newValues);
45075
44999
  const at = (props, key2) => typeof props[key2] === "number" ? props[key2] : PROPERTY_DEFAULTS[key2] ?? 0;
45076
45000
  const kfs = effectiveAnim.keyframes?.keyframes ?? synthesizeFlatTweenKeyframes(effectiveAnim)?.keyframes;
@@ -45103,7 +45027,7 @@ async function commitWholePropertyOffset(selection, anim, newValues, currentPct,
45103
45027
  position: roundTo3(ts ?? 0),
45104
45028
  duration: roundTo3(td || 1),
45105
45029
  keyframes: shifted,
45106
- ease
45030
+ ...keyframeEases(effectiveAnim)
45107
45031
  },
45108
45032
  { label, softReload: true }
45109
45033
  );
@@ -46261,7 +46185,7 @@ import { useCallback as useCallback68, useMemo as useMemo45 } from "react";
46261
46185
  import { useEffect as useEffect62, useMemo as useMemo44, useRef as useRef78, useState as useState55, useCallback as useCallback67 } from "react";
46262
46186
 
46263
46187
  // src/hooks/gsapRuntimeBridge.ts
46264
- import { isXYPositionWrite } from "@hyperframes/parsers/gsap-constants";
46188
+ import { isXYPositionWrite, PROPERTY_GROUPS } from "@hyperframes/parsers/gsap-constants";
46265
46189
 
46266
46190
  // src/hooks/gsapRuntimeReaders.ts
46267
46191
  import { classifyPropertyGroup as classifyPropertyGroup3 } from "@hyperframes/core/gsap-parser";
@@ -46367,183 +46291,404 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
46367
46291
  return result;
46368
46292
  }
46369
46293
 
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);
46294
+ // src/utils/elementGsap.ts
46295
+ function gsapOf(element) {
46296
+ return element.ownerDocument.defaultView?.gsap;
46381
46297
  }
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
- );
46298
+ function setElementGsapPosition(element, x, y) {
46299
+ const gsap3 = gsapOf(element);
46300
+ if (!gsap3?.set) return false;
46301
+ gsap3.set(element, { x, y });
46302
+ return true;
46418
46303
  }
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 }
46304
+ function setElementGsapScale(element, x, y) {
46305
+ const gsap3 = gsapOf(element);
46306
+ if (!gsap3?.set) return false;
46307
+ gsap3.set(element, { scaleX: x, scaleY: y });
46308
+ return true;
46309
+ }
46310
+ function elementTargets(tween) {
46311
+ return (tween.targets?.() ?? []).filter(
46312
+ (t2) => Boolean(t2?.style && t2.nodeType)
46433
46313
  );
46434
- if (activeKeyframePct != null) {
46435
- setActiveKeyframePct(null);
46436
- parkPlayheadOnKeyframe(anim, pct);
46314
+ }
46315
+
46316
+ // src/hooks/gsapParsedTween.ts
46317
+ var PARSED_NAME = { scale: "scaleX", rotate: "rotation" };
46318
+ function findPropTween(pt, prop) {
46319
+ for (let node = pt; node; node = node._next) {
46320
+ if (node.p === prop && typeof node.s === "number" && typeof node.c === "number") return node;
46321
+ const nested = findPropTween(node.d?._pt, prop);
46322
+ if (nested) return nested;
46437
46323
  }
46324
+ return null;
46438
46325
  }
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;
46326
+ function endsIn(tween, prop) {
46327
+ const pt = findPropTween(tween._pt, PARSED_NAME[prop] ?? prop);
46328
+ if (!pt) return null;
46329
+ const pair = [pt.s, pt.s + pt.c];
46330
+ return tween._from ? [pair[1], pair[0]] : pair;
46331
+ }
46332
+ function findParsedTween(iframe, element, anim) {
46333
+ const win = iframe?.contentWindow;
46334
+ const start = resolveTweenStart(anim);
46335
+ if (!win?.__timelines || start == null) return null;
46336
+ const steps = anim.keyframes?.keyframes.map((kf) => kf.properties) ?? [anim.properties];
46337
+ const props = [...new Set(steps.flatMap((step) => Object.keys(step)))];
46338
+ const keyframed = Boolean(anim.keyframes);
46339
+ for (const timeline of Object.values(win.__timelines)) {
46340
+ for (const tween of timeline?.getChildren?.(true) ?? []) {
46341
+ const targets = tween.targets?.() ?? [];
46342
+ if (!targets.includes(element) && !targets.some((t2) => element.id && t2.id === element.id))
46343
+ continue;
46344
+ if (!startsAt(tween, start)) continue;
46345
+ const vars = tween.vars ?? {};
46346
+ const carries = keyframed ? "keyframes" in vars : props.some((p) => p in vars);
46347
+ if (!carries || !((tween.duration?.() ?? 0) > 0)) continue;
46348
+ parseUnplayed(win, element, tween, props);
46349
+ return tween;
46350
+ }
46351
+ }
46352
+ return null;
46353
+ }
46354
+ function startsAt(tween, start) {
46355
+ const delay2 = Number(tween.vars?.delay) || 0;
46356
+ return Math.abs((tween.startTime?.() ?? Number.NaN) - delay2 - start) <= 1e-3;
46357
+ }
46358
+ function withLiveTiming(anim, tween) {
46359
+ const start = tween?.startTime?.();
46360
+ return {
46361
+ ...anim,
46362
+ ...Number.isFinite(start) && { resolvedStart: start },
46363
+ ...anim.duration == null && tween && { duration: tween.duration?.() }
46364
+ };
46365
+ }
46366
+ function parseUnplayed(win, element, tween, props) {
46367
+ const seek = !isParsed(tween) && seekable(win, tween);
46368
+ if (!seek) return;
46369
+ const { parent, gsap: gsap3 } = seek;
46370
+ const before = layerStates(tween, element);
46371
+ const now2 = parent.time();
46372
+ const start = tween.startTime?.() ?? 0;
46373
+ gsap3.set(element, { clearProps: props.join(",") });
46374
+ for (const p of props) gsap3.getProperty(element, p);
46445
46375
  try {
46446
- el = iframe.contentDocument?.querySelector(selector) ?? null;
46447
- } catch {
46448
- return null;
46376
+ parent.seek(start, true);
46377
+ parent.seek(start + (tween.duration?.() ?? 0), true);
46378
+ } finally {
46379
+ parent.seek(now2, true);
46380
+ restore(before);
46449
46381
  }
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;
46382
+ }
46383
+ function isParsed(tween) {
46384
+ const parts = tween.timeline?.getChildren?.() ?? [];
46385
+ return Boolean(tween._initted) && parts.every((part) => part._initted);
46386
+ }
46387
+ function seekable(win, tween) {
46388
+ const { parent } = tween;
46389
+ const gsap3 = win.gsap;
46390
+ if (!parent?.seek || !parent.time || !gsap3?.getProperty || !gsap3.set) return null;
46454
46391
  return {
46455
- gsapLib: gsap3,
46456
- el,
46457
- mainTl
46458
- };
46392
+ parent: { seek: parent.seek.bind(parent), time: parent.time.bind(parent) },
46393
+ gsap: { getProperty: gsap3.getProperty.bind(gsap3), set: gsap3.set.bind(gsap3) }
46394
+ };
46395
+ }
46396
+ function layerStates(tween, element) {
46397
+ const layers = /* @__PURE__ */ new Set([element]);
46398
+ for (const child of tween.parent?.getChildren?.(true, true, false) ?? [])
46399
+ for (const target of elementTargets(child)) layers.add(target);
46400
+ return [...layers].map((layer) => ({
46401
+ layer,
46402
+ attributes: new Map([...layer.attributes].map((a) => [a.name, a.value])),
46403
+ cache: layer._gsap && { ...layer._gsap }
46404
+ }));
46459
46405
  }
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;
46406
+ function restore(states) {
46407
+ for (const { layer, attributes, cache: cache2 } of states) {
46408
+ restoreAttributes(layer, attributes);
46409
+ if (cache2 && layer._gsap) restoreCache(layer._gsap, cache2);
46410
+ }
46411
+ }
46412
+ function restoreAttributes(layer, attributes) {
46413
+ for (const { name } of [...layer.attributes])
46414
+ if (!attributes.has(name)) layer.removeAttribute(name);
46415
+ for (const [name, value] of attributes)
46416
+ if (layer.getAttribute(name) !== value) layer.setAttribute(name, value);
46417
+ }
46418
+ function restoreCache(live, cache2) {
46419
+ for (const key2 of Object.keys(live)) if (!(key2 in cache2)) delete live[key2];
46420
+ Object.assign(live, cache2);
46421
+ }
46422
+ function parsedImplicitEndValue(tween) {
46423
+ return (prop, end) => {
46424
+ if (!tween) return null;
46425
+ for (const part of partsFrom(tween, end)) {
46426
+ const pair = endsIn(part, prop);
46427
+ if (pair) return end === "start" ? pair[0] : pair[1];
46428
+ if (prop in (part.vars ?? {})) return null;
46478
46429
  }
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);
46430
+ return null;
46431
+ };
46432
+ }
46433
+ function partsFrom(tween, end) {
46434
+ const parts = tween.timeline?.getChildren?.() ?? [];
46435
+ if (parts.length === 0) return [tween];
46436
+ return end === "start" ? parts : [...parts].reverse();
46437
+ }
46438
+ function parsedTweenEase(iframe, tween) {
46439
+ if (!tween) return null;
46440
+ const win = iframe?.contentWindow;
46441
+ const ease = tween.vars?.ease ?? tween.parent?.vars?.defaults?.ease ?? win?.gsap?.defaults?.().ease;
46442
+ if (typeof ease === "string") return ease;
46443
+ const named = (name) => win?.gsap?.parseEase?.(name) === ease;
46444
+ return typeof ease === "function" ? BUILT_IN_EASES.find(named) ?? null : null;
46445
+ }
46446
+ var BUILT_IN_EASES = [
46447
+ "none",
46448
+ ...[
46449
+ "power1",
46450
+ "power2",
46451
+ "power3",
46452
+ "power4",
46453
+ "sine",
46454
+ "expo",
46455
+ "circ",
46456
+ "back",
46457
+ "elastic",
46458
+ "bounce"
46459
+ ].flatMap((e) => [`${e}.in`, `${e}.out`, `${e}.inOut`])
46460
+ ];
46461
+ function withExactStepTimes(anim, tween) {
46462
+ const data = anim.keyframes;
46463
+ const parts = tween?.timeline?.getChildren?.() ?? [];
46464
+ const total = tween?.duration?.() ?? 0;
46465
+ if (data?.format !== "object-array" || parts.length !== data.keyframes.length || !(total > 0))
46466
+ return anim;
46467
+ const ends = parts.map((part) => ((part.startTime?.() ?? 0) + (part.duration?.() ?? 0)) / total);
46468
+ if (ends.some((end) => !Number.isFinite(end))) return anim;
46469
+ const keyframes = data.keyframes.map((kf, i) => ({
46470
+ ...kf,
46471
+ percentage: Math.round(ends[i] * 1e5) / 1e3
46472
+ }));
46473
+ return { ...anim, keyframes: { ...data, keyframes } };
46474
+ }
46475
+
46476
+ // src/hooks/gsapTweenReach.ts
46477
+ function tweenReach(animation, el) {
46478
+ const doc = el?.ownerDocument;
46479
+ if (!el?.isConnected || !doc || animation.hasUnresolvedSelector) return "unknown";
46480
+ let hits;
46481
+ try {
46482
+ hits = [...doc.querySelectorAll(animation.targetSelector)];
46483
+ } catch {
46484
+ return "unknown";
46485
+ }
46486
+ if (!hits.includes(el)) return "elsewhere";
46487
+ return hits.length === 1 ? "own" : "shared";
46488
+ }
46489
+ function tweensForThisElement(selection, animations) {
46490
+ return animations.filter((a) => {
46491
+ const reach = tweenReach(a, selection.element);
46492
+ return reach === "own" || reach === "unknown";
46493
+ });
46494
+ }
46495
+
46496
+ // src/hooks/gsapValueAtPlayhead.ts
46497
+ var PERCENTAGE_SEGMENT_EASE = "power1.inOut";
46498
+ var ARRAY_STEP_EASE = "none";
46499
+ var roundPct2 = (pct) => Math.round(pct * 1e3) / 1e3;
46500
+ var isLinear = (ease) => !ease || ease === "none" || ease === "linear";
46501
+ function refuse(reason) {
46502
+ return { ok: false, reason };
46503
+ }
46504
+ function normalize(edit) {
46505
+ const { anim } = edit;
46506
+ const data = anim.keyframes;
46507
+ if (data) {
46508
+ if (!isLinear(anim.ease) || !isLinear(data.ease)) return { reason: "eased-keyframes" };
46509
+ if (data.format === "simple-array") return { reason: "simple-array-keyframes" };
46510
+ const arrayStep = data.format === "object-array";
46511
+ return {
46512
+ keyframes: data.keyframes.map((kf) => ({
46513
+ ...kf,
46514
+ properties: { ...kf.properties },
46515
+ ...arrayStep ? { ease: kf.ease ?? ARRAY_STEP_EASE } : {}
46516
+ })),
46517
+ ...data.easeEach && !arrayStep ? { easeEach: data.easeEach } : {}
46518
+ };
46519
+ }
46520
+ const easeEach = anim.ease ?? edit.parsedEase;
46521
+ if (!easeEach) return { reason: "unknown-ease" };
46522
+ const to = { percentage: 100, properties: { ...anim.properties } };
46523
+ if (anim.method === "to") return { keyframes: [to], easeEach };
46524
+ if (anim.method === "fromTo") {
46525
+ return { keyframes: [{ percentage: 0, properties: { ...anim.fromProperties } }, to], easeEach };
46526
+ }
46527
+ if (anim.method === "from") {
46528
+ const end = {};
46529
+ for (const prop of Object.keys(anim.properties)) {
46530
+ const value = edit.implicitEndValue(prop, "end");
46531
+ if (value == null) return { reason: "implicit-end-unknown" };
46532
+ end[prop] = value;
46491
46533
  }
46534
+ return {
46535
+ keyframes: [
46536
+ { percentage: 0, properties: { ...anim.properties } },
46537
+ { percentage: 100, properties: end }
46538
+ ],
46539
+ easeEach
46540
+ };
46492
46541
  }
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;
46542
+ return { reason: "not-a-tween" };
46543
+ }
46544
+ function valueAt(keyframes, prop, side) {
46545
+ const carrying = keyframes.filter((kf2) => kf2.properties[prop] != null);
46546
+ const kf = side === "first" ? carrying[0] : carrying.at(-1);
46547
+ return kf ? { percentage: kf.percentage, value: kf.properties[prop] } : null;
46548
+ }
46549
+ function upsert(keyframes, percentage, properties, ease) {
46550
+ const hit = keyframes.find((kf) => Math.abs(kf.percentage - percentage) <= KEYFRAME_PCT_MATCH);
46551
+ if (hit) {
46552
+ Object.assign(hit.properties, properties);
46553
+ return hit;
46519
46554
  }
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",
46555
+ const added = { percentage, properties, ...ease ? { ease } : {} };
46556
+ keyframes.push(added);
46557
+ return added;
46558
+ }
46559
+ function heldEnds(norm, end, implicit, backfilled) {
46560
+ const props = new Set(norm.keyframes.flatMap((kf) => Object.keys(kf.properties)));
46561
+ const held = {};
46562
+ for (const prop of props) {
46563
+ const known = valueAt(norm.keyframes, prop, end === "start" ? "first" : "last");
46564
+ const value = known && (end === "end" || known.percentage <= 0) ? known.value : backfilled[prop] ?? implicit(prop, "start");
46565
+ if (value == null) return null;
46566
+ held[prop] = value;
46567
+ }
46568
+ return held;
46569
+ }
46570
+ function planValueAtPlayhead(edit) {
46571
+ const { anim, values } = edit;
46572
+ const start = resolveTweenStart(anim);
46573
+ const duration = resolveTweenDuration(anim);
46574
+ if (start == null || !(duration > 0)) return refuse("no-timing");
46575
+ const norm = normalize(edit);
46576
+ if ("reason" in norm) return refuse(norm.reason);
46577
+ const keyframes = norm.keyframes;
46578
+ const authored = keyframes.length;
46579
+ if ("scaleX" in values || "scaleY" in values) {
46580
+ for (const kf of keyframes) {
46581
+ const { scale, ...rest } = kf.properties;
46582
+ if (scale != null) kf.properties = { scaleX: scale, scaleY: scale, ...rest };
46583
+ }
46584
+ }
46585
+ const backfilled = {};
46586
+ for (const [prop, value] of Object.entries(edit.backfill ?? {})) {
46587
+ if (!(prop in values) || keyframes.some((kf) => kf.properties[prop] != null)) continue;
46588
+ if (edit.holdFromStart && !keyframes.some((kf) => kf.percentage <= 0))
46589
+ keyframes.unshift({ percentage: 0, properties: {} });
46590
+ for (const kf of keyframes) kf.properties[prop] = value;
46591
+ backfilled[prop] = value;
46592
+ }
46593
+ if (Object.keys(values).some((prop) => !keyframes.some((kf) => kf.properties[prop] != null)))
46594
+ return refuse("implicit-end-unknown");
46595
+ let position = start;
46596
+ let span = duration;
46597
+ const pct = "percentage" in edit.at ? edit.at.percentage : roundPct2((edit.at.time - start) / duration * 100);
46598
+ if (pct >= -KEYFRAME_PCT_MATCH && pct <= 100 + KEYFRAME_PCT_MATCH) {
46599
+ const at = Math.min(100, Math.max(0, pct));
46600
+ const next = [...keyframes].sort((a, b) => a.percentage - b.percentage).find((kf) => kf.percentage > at + KEYFRAME_PCT_MATCH);
46601
+ upsert(keyframes, at, { ...values }, next?.ease);
46602
+ } else {
46603
+ const time = edit.at.time;
46604
+ const before = time < start;
46605
+ const held = heldEnds(norm, before ? "start" : "end", edit.implicitEndValue, backfilled);
46606
+ if (!held) return refuse("implicit-end-unknown");
46607
+ position = before ? time : start;
46608
+ span = before ? start + duration - time : time - start;
46609
+ for (const kf of keyframes) {
46610
+ kf.percentage = roundPct2(
46611
+ (start + kf.percentage / 100 * duration - position) / span * 100
46612
+ );
46613
+ }
46614
+ const oldEdge = roundPct2(((before ? start : start + duration) - position) / span * 100);
46615
+ const linear = isLinear(norm.easeEach ?? PERCENTAGE_SEGMENT_EASE) ? void 0 : "none";
46616
+ const edge = upsert(keyframes, oldEdge, held, linear);
46617
+ if (before && linear) edge.ease ??= linear;
46618
+ upsert(keyframes, before ? 0 : 100, { ...held, ...values }, before ? void 0 : linear);
46619
+ }
46620
+ keyframes.sort((a, b) => a.percentage - b.percentage);
46621
+ return {
46622
+ ok: true,
46623
+ mutation: {
46624
+ type: "replace-with-keyframes",
46537
46625
  animationId: anim.id,
46538
- percentage: pct,
46539
- properties,
46540
- ...backfillDefaults ? { backfillDefaults } : {}
46541
- },
46542
- { label: `Move layer (keyframe ${pct}%)`, softReload: true, beforeReload, coalesceKey }
46626
+ targetSelector: anim.targetSelector,
46627
+ position: roundTo3(position),
46628
+ duration: roundTo3(span),
46629
+ keyframes,
46630
+ ...norm.easeEach ? { easeEach: norm.easeEach } : {}
46631
+ },
46632
+ added: keyframes.length > authored
46633
+ };
46634
+ }
46635
+ function planValueEdit(selection, anim, values, iframe, { backfill, holdFromStart } = {}) {
46636
+ if (tweenReach(anim, selection.element) === "shared") return refuse("shared-tween");
46637
+ const { activeKeyframePct, currentTime } = usePlayerStore.getState();
46638
+ const tween = findParsedTween(iframe, selection.element, anim);
46639
+ const timed = withExactStepTimes(anim, tween);
46640
+ return planValueAtPlayhead({
46641
+ anim: withLiveTiming(timed, tween),
46642
+ at: activeKeyframePct != null ? { percentage: activeKeyframePct } : { time: currentTime },
46643
+ values,
46644
+ backfill,
46645
+ holdFromStart,
46646
+ implicitEndValue: parsedImplicitEndValue(tween),
46647
+ parsedEase: parsedTweenEase(iframe, tween)
46648
+ });
46649
+ }
46650
+ async function commitValueAtPlayhead(selection, anim, values, iframe, callbacks, options) {
46651
+ await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
46652
+ const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46653
+ const plan = planValueEdit(selection, anim, values, iframe, options);
46654
+ if (!plan.ok) return { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
46655
+ await callbacks.commitMutation(selection, plan.mutation, {
46656
+ label: options.label,
46657
+ softReload: true,
46658
+ beforeReload: options.beforeReload,
46659
+ ...plan.added && { keyframeAction: "add" }
46660
+ });
46661
+ if (activeKeyframePct != null) {
46662
+ setActiveKeyframePct(null);
46663
+ parkPlayheadOnKeyframe(anim, activeKeyframePct);
46664
+ }
46665
+ return { status: "persisted" };
46666
+ }
46667
+
46668
+ // src/hooks/gsapDragPositionCommit.ts
46669
+ function buildTemporalArcKeyframes(anim, percentage, properties) {
46670
+ return [
46671
+ ...(anim.keyframes?.keyframes ?? []).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
46672
+ percentage: keyframe.percentage,
46673
+ properties: { ...keyframe.properties },
46674
+ ...keyframe.ease ? { ease: keyframe.ease } : {}
46675
+ })),
46676
+ { percentage, properties }
46677
+ ].sort((a, b) => a.percentage - b.percentage);
46678
+ }
46679
+ function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, iframe) {
46680
+ if (anim.arcPath?.enabled) return { status: "persisted" };
46681
+ const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
46682
+ selection.element,
46683
+ studioOffset,
46684
+ gsapPos
46543
46685
  );
46544
- if (editedSelected) parkPlayheadOnKeyframe(anim, pct);
46686
+ const plan = planValueEdit(selection, anim, { x: newX, y: newY }, iframe, {
46687
+ backfill: { x: baseGsapX, y: baseGsapY }
46688
+ });
46689
+ return plan.ok ? { status: "persisted" } : { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
46545
46690
  }
46546
- async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos, iframe, selector, callbacks) {
46691
+ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos, iframe, callbacks) {
46547
46692
  const el = selection.element;
46548
46693
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
46549
46694
  el,
@@ -46558,8 +46703,6 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46558
46703
  el.removeAttribute("data-hf-drag-initial-offset-x");
46559
46704
  el.removeAttribute("data-hf-drag-initial-offset-y");
46560
46705
  };
46561
- const backfillDefaults = { x: baseGsapX, y: baseGsapY };
46562
- const ct = usePlayerStore.getState().currentTime;
46563
46706
  if (anim.arcPath?.enabled) {
46564
46707
  const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
46565
46708
  const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
@@ -46581,11 +46724,12 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46581
46724
  );
46582
46725
  setActiveKeyframePct(null);
46583
46726
  parkPlayheadOnKeyframe(anim, pct);
46584
- return;
46727
+ return { status: "persisted" };
46585
46728
  }
46586
46729
  const tweenStart = resolveTweenStart(anim);
46587
46730
  const tweenDuration = resolveEditableTweenDuration(anim, selection);
46588
- if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2) return;
46731
+ if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
46732
+ return { status: "persisted" };
46589
46733
  const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
46590
46734
  await callbacks.commitMutation(
46591
46735
  selection,
@@ -46605,171 +46749,12 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46605
46749
  beforeReload: restoreOffset
46606
46750
  }
46607
46751
  );
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";
46752
+ return { status: "persisted" };
46765
46753
  }
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";
46754
+ return commitValueAtPlayhead(selection, anim, { x: newX, y: newY }, iframe, callbacks, {
46755
+ label: "Move layer",
46756
+ backfill: { x: baseGsapX, y: baseGsapY },
46757
+ beforeReload: restoreOffset
46773
46758
  });
46774
46759
  }
46775
46760
 
@@ -46795,40 +46780,59 @@ function animHasPosition(anim) {
46795
46780
  function findGsapPositionAnimation(animations, selector) {
46796
46781
  if (animations.length === 0) return null;
46797
46782
  const currentTime = usePlayerStore.getState().currentTime;
46798
- const scored = animations.filter((a) => animHasPosition(a) || a.keyframes || animations.length === 1).map((a) => {
46783
+ const scored = animations.filter(animHasPosition).map((a) => {
46799
46784
  let score = 0;
46800
- if (animHasPosition(a)) score += 10;
46801
46785
  if (a.keyframes) score += 5;
46802
46786
  if (selector && a.targetSelector === selector) score += 8;
46803
46787
  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 };
46788
+ return { anim: a, score: score + playheadProximity(a, currentTime) };
46812
46789
  });
46813
46790
  scored.sort((a, b) => b.score - a.score);
46814
- return scored[0]?.anim ?? animations[0];
46791
+ return scored[0]?.anim ?? null;
46792
+ }
46793
+ function playheadProximity(a, currentTime) {
46794
+ const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
46795
+ const dur = a.duration ?? 0;
46796
+ if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
46797
+ return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
46798
+ }
46799
+ function statesValueAt(anim, time) {
46800
+ const duration = resolveTweenDuration(anim);
46801
+ if (!(duration > 0)) return false;
46802
+ const pct = (time - (resolveTweenStart(anim) ?? 0)) / duration * 100;
46803
+ const near3 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
46804
+ if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near3(kf.percentage));
46805
+ if (anim.method === "from") return near3(0);
46806
+ return near3(100) || anim.method === "fromTo" && near3(0);
46815
46807
  }
46816
46808
  function pickClosestToPlayhead(anims) {
46817
46809
  if (anims.length <= 1) return anims[0] ?? null;
46818
46810
  const ct = usePlayerStore.getState().currentTime;
46819
46811
  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));
46812
+ const dist = playheadDistance(a, ct);
46813
+ const bestDist = playheadDistance(best, ct);
46826
46814
  if (dist < bestDist) return a;
46827
- if (dist === bestDist && (a.keyframes?.keyframes.length ?? 0) > (best.keyframes?.keyframes.length ?? 0))
46828
- return a;
46815
+ const stated = dist === 0 && bestDist === 0 ? statedValueWinner(a, best, ct) : null;
46816
+ if (stated) return stated;
46817
+ if (dist === bestDist && keyframeCount(a) > keyframeCount(best)) return a;
46829
46818
  return best;
46830
46819
  });
46831
46820
  }
46821
+ function playheadDistance(anim, time) {
46822
+ const s = resolveTweenStart(anim) ?? 0;
46823
+ const e = s + resolveTweenDuration(anim);
46824
+ return time >= s && time <= e ? 0 : Math.min(Math.abs(time - s), Math.abs(time - e));
46825
+ }
46826
+ function statedValueWinner(a, b, time) {
46827
+ const [mine, theirs] = [statesValueAt(a, time), statesValueAt(b, time)];
46828
+ if (mine !== theirs) return mine ? a : b;
46829
+ const [s, bS] = [resolveTweenStart(a) ?? 0, resolveTweenStart(b) ?? 0];
46830
+ if (mine && s !== bS) return s > bS ? a : b;
46831
+ return null;
46832
+ }
46833
+ function keyframeCount(anim) {
46834
+ return anim.keyframes?.keyframes.length ?? 0;
46835
+ }
46832
46836
 
46833
46837
  // src/hooks/gsapElementMatch.ts
46834
46838
  function getAnimationsForElement(animations, target, element) {
@@ -46858,40 +46862,19 @@ function getAnimationsForElement(animations, target, element) {
46858
46862
  // src/hooks/gsapRuntimeBridge.ts
46859
46863
  var POSITION_CHANNEL_SET = new Set(POSITION_CHANNELS);
46860
46864
  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;
46865
+ async function resolveGroupTween(group, animations, _selection, _commitMutation, fetchFallbackAnimations) {
46866
+ const inGroup = (list) => {
46867
+ const tagged = list.filter((a) => a.propertyGroup === group);
46868
+ const props = new Set(PROPERTY_GROUPS[group]);
46869
+ const mixed = list.filter((a) => !a.propertyGroup && animationWritesAnyProperty(a, props));
46870
+ return pickClosestToPlayhead(tagged.length > 0 ? tagged : mixed);
46871
+ };
46872
+ const anim = inGroup(animations);
46873
+ if (anim) return { anim, animations };
46874
+ if (!fetchFallbackAnimations) return null;
46875
+ const fresh = await fetchFallbackAnimations();
46876
+ const freshAnim = inGroup(fresh);
46877
+ return freshAnim ? { anim: freshAnim, animations: fresh } : null;
46895
46878
  }
46896
46879
  var writesPosition = (a) => animationWritesAnyProperty(a, POSITION_CHANNEL_SET);
46897
46880
  function positionedOnlyBySharedTween(selection, animations) {
@@ -46939,6 +46922,36 @@ function oneUndoStep(commit) {
46939
46922
  const coalesceKey = `gsap:drag:${++dragGestureCounter}`;
46940
46923
  return (selection, mutation, options) => commit(selection, mutation, { ...options, coalesceKey, coalesceMs: Number.POSITIVE_INFINITY });
46941
46924
  }
46925
+ var isPositionWriteOf = (selector) => (a) => a.targetSelector === selector && isXYPositionWrite(a);
46926
+ function positionWriteKeeper(animations, selector) {
46927
+ const dupes = animations.filter(isPositionWriteOf(selector));
46928
+ if (dupes.length < 2 || dupes.filter((a) => !isInstantHold(a)).length > 1) return null;
46929
+ return dupes.find((a) => a.keyframes) ?? dupes.find((a) => (a.duration ?? 0) > 0) ?? dupes[0];
46930
+ }
46931
+ function dragRoute(posAnim, iframe, selector, altKey) {
46932
+ const hasNonHold = hasNonHoldTweenForElement(iframe, selector, void 0, POSITION_CHANNELS);
46933
+ const hasKeyframedPosTween = !!posAnim?.keyframes && resolveTweenDuration(posAnim) > 0;
46934
+ if (!hasNonHold && !hasKeyframedPosTween) return "static";
46935
+ return altKey || !usePlayerStore.getState().autoKeyframeEnabled ? "whole-path" : "at-playhead";
46936
+ }
46937
+ async function planDrag(selection, offset, allAnimations, iframe, options) {
46938
+ const selector = selectorFromSelection(selection);
46939
+ if (!selector) return { status: "blocked", reason: "no-selector" };
46940
+ const own = tweensForThisElement(selection, allAnimations);
46941
+ const keeper = positionWriteKeeper(own, selector);
46942
+ const animations = keeper ? own.filter((a) => a === keeper || !isPositionWriteOf(selector)(a)) : own;
46943
+ const resolved2 = await resolveGroupTween("position", animations, selection, async () => {
46944
+ });
46945
+ const posAnim = resolved2?.anim ?? findGsapPositionAnimation(animations, selector);
46946
+ const route = dragRoute(posAnim, iframe, selector, options.altKey);
46947
+ if (route === "static") return { status: "persisted" };
46948
+ if (!posAnim) {
46949
+ return { status: "blocked", reason: "source-uneditable", detail: "no-position-tween" };
46950
+ }
46951
+ if (route === "whole-path") return { status: "persisted" };
46952
+ const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
46953
+ return gsapPositionFromDragOutcome(selection, posAnim, offset, gsapPos, iframe);
46954
+ }
46942
46955
  async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, gestureCommit, fetchAllAnimations, options) {
46943
46956
  if (!options?.preflightPassed) {
46944
46957
  const preflight = await preflightGsapDragIntercept(
@@ -46948,7 +46961,10 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46948
46961
  fetchAllAnimations,
46949
46962
  options?.group
46950
46963
  );
46951
- if (preflight.status !== "persisted" || options?.preflightOnly) return preflight;
46964
+ if (preflight.status !== "persisted") return preflight;
46965
+ if (options?.preflightOnly) {
46966
+ return options.group ? planDrag(selection, offset, allAnimations, iframe, options) : preflight;
46967
+ }
46952
46968
  }
46953
46969
  const animations = tweensForThisElement(selection, allAnimations);
46954
46970
  const fetchFallbackAnimations = fetchAllAnimations && (async () => tweensForThisElement(selection, await fetchAllAnimations()));
@@ -46956,12 +46972,10 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46956
46972
  if (!selector) return { status: "blocked", reason: "no-selector" };
46957
46973
  const commitMutation = oneUndoStep(gestureCommit);
46958
46974
  let workingAnimations = animations;
46959
- const isPosWrite = (a) => a.targetSelector === selector && isXYPositionWrite(a);
46960
- if (animations.filter(isPosWrite).length > 1 && fetchFallbackAnimations) {
46975
+ if (animations.filter(isPositionWriteOf(selector)).length > 1 && fetchFallbackAnimations) {
46961
46976
  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];
46977
+ const keeper = positionWriteKeeper(fresh, selector);
46978
+ if (keeper) {
46965
46979
  await commitMutation(
46966
46980
  selection,
46967
46981
  {
@@ -46994,9 +47008,8 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
46994
47008
  }
46995
47009
  }
46996
47010
  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) {
47011
+ const route = dragRoute(posAnim, iframe, selector, options?.altKey);
47012
+ if (route === "static") {
47000
47013
  const existingSet = posAnim && isInstantHold(posAnim) && posAnim.targetSelector === selector ? posAnim : findExistingPositionWrite(resolvedAnimations, selector, selection.element);
47001
47014
  await commitStaticGsapPosition(selection, offset, gsapPos, selector, existingSet, {
47002
47015
  commitMutation,
@@ -47009,19 +47022,20 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
47009
47022
  }
47010
47023
  if (fetchFallbackAnimations) {
47011
47024
  const fresh = await fetchFallbackAnimations();
47012
- const freshMatch = fresh.find(
47013
- (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
47025
+ const freshMatch = fresh.find((a) => a.id === posAnim.id) ?? pickClosestToPlayhead(
47026
+ fresh.filter(
47027
+ (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
47028
+ )
47014
47029
  );
47015
47030
  if (freshMatch && freshMatch.id !== posAnim.id) {
47016
47031
  posAnim = freshMatch;
47017
47032
  }
47018
47033
  }
47019
47034
  const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations };
47020
- const autoKeyframeEnabled = usePlayerStore.getState().autoKeyframeEnabled;
47021
- if (options?.altKey || !autoKeyframeEnabled) {
47035
+ if (route === "whole-path") {
47022
47036
  await commitWholePathOffset(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
47023
47037
  } else {
47024
- await commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
47038
+ return commitGsapPositionFromDrag(selection, posAnim, offset, gsapPos, iframe, cbs);
47025
47039
  }
47026
47040
  return { status: "persisted" };
47027
47041
  }
@@ -47065,7 +47079,9 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
47065
47079
  const resolvedAnimations = resolved2?.animations ?? workingAnimations;
47066
47080
  let anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, ROTATION_CHANNEL_SET) ? resolved2.anim : null;
47067
47081
  if (!anim) {
47068
- anim = workingAnimations.find((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET)) ?? null;
47082
+ anim = pickClosestToPlayhead(
47083
+ workingAnimations.filter((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET))
47084
+ );
47069
47085
  }
47070
47086
  const newRotation = roundTo3(angle);
47071
47087
  if (!anim || isInstantHold(anim)) {
@@ -47089,35 +47105,14 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
47089
47105
  );
47090
47106
  return { status: "persisted" };
47091
47107
  }
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(
47108
+ return commitValueAtPlayhead(
47110
47109
  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 }
47110
+ anim,
47111
+ { rotation: newRotation },
47112
+ iframe,
47113
+ { commitMutation, fetchAnimations: fetchFallbackAnimations },
47114
+ { label: "Rotate", backfill: { rotation: newRotation }, holdFromStart: true }
47119
47115
  );
47120
- return { status: "persisted" };
47121
47116
  }
47122
47117
 
47123
47118
  // src/hooks/keyframeCacheAstLoad.ts
@@ -47141,7 +47136,7 @@ function fetchParsedAnimations(projectId, sourceFile, options = {}) {
47141
47136
  async function requestParsedAnimations(projectId, sourceFile) {
47142
47137
  if (!isPreviewBooted(projectId) && !await whenPreviewBooted(projectId)) return null;
47143
47138
  try {
47144
- const res = await fetch(
47139
+ const res = await studioApiFetch(
47145
47140
  `/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
47146
47141
  // Always revalidate; an unchanged file answers 304. No per-call timestamp
47147
47142
  // (a deterministic-render no-no).
@@ -47433,8 +47428,8 @@ var MIN_TWEEN_DURATION = 0.01;
47433
47428
  var round33 = (n) => Math.round(n * 1e3) / 1e3;
47434
47429
  var clamp5 = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
47435
47430
  function resolveFlatTweenBoundaryRetime(opts) {
47436
- const { keyframeCount, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
47437
- if (keyframeCount > 0) return null;
47431
+ const { keyframeCount: keyframeCount2, draggedTweenPct, tweenStart, tweenEnd, dropAbsTime } = opts;
47432
+ if (keyframeCount2 > 0) return null;
47438
47433
  if (draggedTweenPct !== 0 && draggedTweenPct !== 100) return { kind: "noop" };
47439
47434
  const draggedTime = draggedTweenPct === 0 ? tweenStart : tweenEnd;
47440
47435
  if (Math.abs(dropAbsTime - draggedTime) <= EPSILON_TIME) return { kind: "noop" };
@@ -49368,7 +49363,7 @@ function StudioAgentTools() {
49368
49363
  // fetches the URL itself.
49369
49364
  probeFrame: async (url) => {
49370
49365
  try {
49371
- const response = await fetch(url, { method: "HEAD" });
49366
+ const response = await studioApiFetch(url, { method: "HEAD" });
49372
49367
  return { ok: response.ok, status: response.status };
49373
49368
  } catch {
49374
49369
  return { ok: false, status: 0 };
@@ -50779,6 +50774,23 @@ function useRenderClipContent({
50779
50774
  );
50780
50775
  }
50781
50776
 
50777
+ // src/hooks/useThumbnailStill.ts
50778
+ import { useMemo as useMemo49 } from "react";
50779
+ var LOADING = Object.freeze({ status: "loading" });
50780
+ var FAILED = Object.freeze({ status: "failed" });
50781
+ function useThumbnailStill(url, projectId, { sessionEpoch = 0, priority = "visible" } = {}) {
50782
+ const request = useMemo49(
50783
+ () => url ? compositionThumbnailRequest(url, projectId, { sessionEpoch, priority }) : null,
50784
+ [priority, projectId, sessionEpoch, url]
50785
+ );
50786
+ const snapshot3 = useThumbnailLease(request);
50787
+ const ready = snapshot3.status === "ready" && snapshot3.value.kind === "image" ? snapshot3.value.url : null;
50788
+ return useMemo49(
50789
+ () => ready ? { status: "ready", url: ready } : snapshot3.status === "error" ? FAILED : LOADING,
50790
+ [ready, snapshot3.status]
50791
+ );
50792
+ }
50793
+
50782
50794
  // src/components/editor/SourceEditor.tsx
50783
50795
  import { useRef as useRef82, useCallback as useCallback76, useEffect as useEffect68, memo as memo35 } from "react";
50784
50796
  import {
@@ -51002,13 +51014,13 @@ var SourceEditor = memo35(function SourceEditor2({
51002
51014
  });
51003
51015
 
51004
51016
  // src/components/editor/PropertyPanel.tsx
51005
- import { memo as memo39, useMemo as useMemo64, useRef as useRef113, useState as useState101 } from "react";
51017
+ import { memo as memo39, useMemo as useMemo65, useRef as useRef113, useState as useState101 } from "react";
51006
51018
 
51007
51019
  // src/components/editor/InspectorHeaderActions.tsx
51008
51020
  import { Eye as Eye3, EyeSlash as EyeSlash2 } from "@phosphor-icons/react";
51009
51021
 
51010
51022
  // src/contexts/DesignPanelInputContext.tsx
51011
- import { useCallback as useCallback77, useContext as useContext12, useMemo as useMemo49 } from "react";
51023
+ import { useCallback as useCallback77, useContext as useContext12, useMemo as useMemo50 } from "react";
51012
51024
 
51013
51025
  // src/utils/designInputTracking.ts
51014
51026
  var COALESCE_WINDOW_MS = 600;
@@ -51051,7 +51063,7 @@ function DesignPanelInputProvider({
51051
51063
  children
51052
51064
  }) {
51053
51065
  const parent = useContext12(DesignPanelInputContext);
51054
- const value = useMemo49(
51066
+ const value = useMemo50(
51055
51067
  () => ({ ui: ui ?? parent.ui, section: section ?? parent.section }),
51056
51068
  [ui, section, parent.ui, parent.section]
51057
51069
  );
@@ -52716,7 +52728,7 @@ function MediaSection({
52716
52728
  import { isHfColorGradingActive as isHfColorGradingActive2 } from "@hyperframes/core/color-grading";
52717
52729
 
52718
52730
  // src/components/editor/propertyPanelColorGradingControls.tsx
52719
- import { useMemo as useMemo50, useRef as useRef87, useState as useState63 } from "react";
52731
+ import { useMemo as useMemo51, useRef as useRef87, useState as useState63 } from "react";
52720
52732
  import {
52721
52733
  HF_COLOR_GRADING_PRESETS,
52722
52734
  normalizeHfColorGrading as normalizeHfColorGrading2
@@ -53160,7 +53172,7 @@ function ColorGradingControls({
53160
53172
  const [detailSettings, setDetailSettings] = useState63(null);
53161
53173
  const [lutImporting, setLutImporting] = useState63(false);
53162
53174
  const [lutImportError, setLutImportError] = useState63(null);
53163
- const lutAssets = useMemo50(
53175
+ const lutAssets = useMemo51(
53164
53176
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
53165
53177
  [assets]
53166
53178
  );
@@ -53440,7 +53452,7 @@ function ColorGradingControls({
53440
53452
  }
53441
53453
 
53442
53454
  // src/components/editor/useColorGradingController.ts
53443
- import { useCallback as useCallback82, useEffect as useEffect75, useMemo as useMemo51, useRef as useRef89, useState as useState65 } from "react";
53455
+ import { useCallback as useCallback82, useEffect as useEffect75, useMemo as useMemo52, useRef as useRef89, useState as useState65 } from "react";
53444
53456
  import {
53445
53457
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR3,
53446
53458
  hasHfColorGradingAuthoredValues,
@@ -53803,7 +53815,7 @@ function useColorGradingController({
53803
53815
  state: "pending",
53804
53816
  message: "Waiting for runtime"
53805
53817
  }));
53806
- const selectedAssetPath = useMemo51(
53818
+ const selectedAssetPath = useMemo52(
53807
53819
  () => selectedMediaAssetPath(element, projectId),
53808
53820
  [element, projectId]
53809
53821
  );
@@ -53850,7 +53862,7 @@ function useColorGradingController({
53850
53862
  trackStudioPendingEdit(onSetAttributeLive(COLOR_GRADING_DATA_KEY, value));
53851
53863
  };
53852
53864
  }, [identityKey]);
53853
- const target = useMemo51(
53865
+ const target = useMemo52(
53854
53866
  () => ({
53855
53867
  id: element.id ?? null,
53856
53868
  hfId: element.hfId ?? null,
@@ -53871,7 +53883,7 @@ function useColorGradingController({
53871
53883
  return;
53872
53884
  }
53873
53885
  const controller = new AbortController();
53874
- fetch(cacheKey, { signal: controller.signal }).then(async (response) => {
53886
+ studioApiFetch(cacheKey, { signal: controller.signal }).then(async (response) => {
53875
53887
  if (!response.ok) return { ok: false };
53876
53888
  const data = await response.json();
53877
53889
  return { ok: true, metadata: data?.metadata ?? null };
@@ -54830,7 +54842,7 @@ function ColorField({
54830
54842
  }
54831
54843
 
54832
54844
  // src/components/editor/propertyPanelFont.tsx
54833
- import { useEffect as useEffect77, useMemo as useMemo52, useRef as useRef91, useState as useState67, useSyncExternalStore as useSyncExternalStore8 } from "react";
54845
+ import { useEffect as useEffect77, useMemo as useMemo53, useRef as useRef91, useState as useState67, useSyncExternalStore as useSyncExternalStore8 } from "react";
54834
54846
 
54835
54847
  // src/components/editor/fontAssets.ts
54836
54848
  var FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i;
@@ -54945,7 +54957,7 @@ var fontLists = {
54945
54957
  var fontListsRequest = null;
54946
54958
  var fontListListeners = /* @__PURE__ */ new Set();
54947
54959
  async function fetchFontList(url) {
54948
- const data = await (await fetch(url)).json();
54960
+ const data = await (await studioApiFetch(url)).json();
54949
54961
  if (!Array.isArray(data.fonts)) throw new Error(`${url} returned no font list`);
54950
54962
  return data.fonts;
54951
54963
  }
@@ -55076,13 +55088,13 @@ function FontFamilyField({
55076
55088
  setImportingFonts(false);
55077
55089
  }
55078
55090
  };
55079
- const projectFontAssets = useMemo52(
55091
+ const projectFontAssets = useMemo53(
55080
55092
  () => uniqueFontOptions(
55081
55093
  importedFonts.map((f) => ({ family: f.family, source: "Imported" }))
55082
55094
  ),
55083
55095
  [importedFonts]
55084
55096
  );
55085
- const options = useMemo52(() => {
55097
+ const options = useMemo53(() => {
55086
55098
  if (!open) return [];
55087
55099
  const documentFonts = collectDocumentFontFamilies();
55088
55100
  return sortFontOptions(
@@ -55098,7 +55110,7 @@ function FontFamilyField({
55098
55110
  ])
55099
55111
  );
55100
55112
  }, [open, currentFamily, lists, localFonts, projectFontAssets]);
55101
- const filteredOptions = useMemo52(() => {
55113
+ const filteredOptions = useMemo53(() => {
55102
55114
  const matches = options.filter((o) => fontMatchesQuery(o.family, query));
55103
55115
  if (query.trim()) return matches.slice(0, 200);
55104
55116
  const bySource = /* @__PURE__ */ new Map();
@@ -55128,7 +55140,7 @@ function FontFamilyField({
55128
55140
  };
55129
55141
  const importSystemFont = async (family) => {
55130
55142
  if (!onImportFonts) return null;
55131
- const response = await fetch(`/api/fonts/file?family=${encodeURIComponent(family)}`);
55143
+ const response = await studioApiFetch(`/api/fonts/file?family=${encodeURIComponent(family)}`);
55132
55144
  if (!response.ok) return null;
55133
55145
  const blob = await response.blob();
55134
55146
  const ext2 = response.headers.get("Content-Disposition")?.match(/\.(\w+)"?$/)?.[1] ?? "ttf";
@@ -55334,7 +55346,7 @@ function FontFamilyField({
55334
55346
  import { useEffect as useEffect79 } from "react";
55335
55347
 
55336
55348
  // src/components/panels/VariablesBindElement.tsx
55337
- import { useMemo as useMemo53, useState as useState69 } from "react";
55349
+ import { useMemo as useMemo54, useState as useState69 } from "react";
55338
55350
 
55339
55351
  // src/components/panels/VariablesValueControls.tsx
55340
55352
  import { useState as useState68 } from "react";
@@ -55616,7 +55628,7 @@ function VariablesBindElement({
55616
55628
  }) {
55617
55629
  const [activeKey, setActiveKey] = useState69(null);
55618
55630
  const [idDraft, setIdDraft] = useState69("");
55619
- const actions = useMemo53(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
55631
+ const actions = useMemo54(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
55620
55632
  if (actions.length === 0) return null;
55621
55633
  const active = actions.find((a) => a.key === activeKey) ?? null;
55622
55634
  const trimmedId = sanitizeId(idDraft);
@@ -55727,7 +55739,7 @@ function uniqueId(base2, existing) {
55727
55739
  }
55728
55740
 
55729
55741
  // src/contexts/VariablePromoteContext.tsx
55730
- import { useContext as useContext13, useEffect as useEffect78, useMemo as useMemo54, useState as useState70 } from "react";
55742
+ import { useContext as useContext13, useEffect as useEffect78, useMemo as useMemo55, useState as useState70 } from "react";
55731
55743
  import { jsx as jsx129 } from "react/jsx-runtime";
55732
55744
  var VariablePromoteContext = createStableContext(
55733
55745
  "VariablePromoteContext",
@@ -55750,15 +55762,15 @@ function VariablePromoteProvider({
55750
55762
  if (!session) return;
55751
55763
  return session.on("change", () => setRevision((r) => r + 1));
55752
55764
  }, [session]);
55753
- const actions = useMemo54(() => {
55765
+ const actions = useMemo55(() => {
55754
55766
  if (!session || !selection) return [];
55755
55767
  return buildBindActions(selection, session);
55756
55768
  }, [session, selection, revision]);
55757
- const declarations = useMemo54(() => {
55769
+ const declarations = useMemo55(() => {
55758
55770
  if (!session) return [];
55759
55771
  return session.getVariableDeclarations();
55760
55772
  }, [session, revision]);
55761
- const value = useMemo54(
55773
+ const value = useMemo55(
55762
55774
  () => ({ session, selection, actions, declarations, persist: persist2, onPersistError }),
55763
55775
  [session, selection, actions, declarations, persist2, onPersistError]
55764
55776
  );
@@ -55767,7 +55779,7 @@ function VariablePromoteProvider({
55767
55779
  function useVariablePromoteChannel(channel) {
55768
55780
  const ctx3 = useContext13(VariablePromoteContext);
55769
55781
  const key2 = channel.kind === "style" ? `style:${channel.prop}` : channel.kind;
55770
- return useMemo54(() => {
55782
+ return useMemo55(() => {
55771
55783
  if (!ctx3 || !ctx3.session || !ctx3.selection?.hfId) return null;
55772
55784
  const { session, selection, actions, declarations, persist: persist2, onPersistError } = ctx3;
55773
55785
  const hfId = selection.hfId;
@@ -56191,7 +56203,7 @@ function insertGradientStop(model, position) {
56191
56203
  }
56192
56204
 
56193
56205
  // src/components/editor/propertyPanelFill.tsx
56194
- import { useMemo as useMemo55, useRef as useRef92, useState as useState71 } from "react";
56206
+ import { useMemo as useMemo56, useRef as useRef92, useState as useState71 } from "react";
56195
56207
 
56196
56208
  // src/components/icons/ReverseGradientIcon.tsx
56197
56209
  import { jsx as jsx131, jsxs as jsxs97 } from "react/jsx-runtime";
@@ -56266,8 +56278,8 @@ function ImageFillField({
56266
56278
  const fileInputRef = useRef92(null);
56267
56279
  const [uploading, setUploading] = useState71(false);
56268
56280
  const [uploadError, setUploadError] = useState71(null);
56269
- const imageAssets = useMemo55(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
56270
- const selectedAsset = useMemo55(
56281
+ const imageAssets = useMemo56(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
56282
+ const selectedAsset = useMemo56(
56271
56283
  () => resolveSelectedAsset(value, sourceFile, imageAssets),
56272
56284
  [imageAssets, sourceFile, value]
56273
56285
  );
@@ -57734,7 +57746,7 @@ import { useCallback as useCallback87, useState as useState76 } from "react";
57734
57746
  import { useShallow } from "zustand/react/shallow";
57735
57747
 
57736
57748
  // 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";
57749
+ import { memo as memo37, useCallback as useCallback86, useEffect as useEffect82, useMemo as useMemo57, useRef as useRef95, useState as useState75 } from "react";
57738
57750
  import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
57739
57751
 
57740
57752
  // src/components/editor/gsapAnimationHelpers.ts
@@ -58275,21 +58287,21 @@ var AnimationCard = memo37(function AnimationCard2({
58275
58287
  segment?.scrollIntoView({ block: "nearest", behavior: "smooth" });
58276
58288
  pendingAutoScrollRef.current = false;
58277
58289
  }, [expandedKfPct]);
58278
- const usedProps = useMemo56(
58290
+ const usedProps = useMemo57(
58279
58291
  () => new Set(Object.keys(animation.properties)),
58280
58292
  [animation.properties]
58281
58293
  );
58282
- const availableProps = useMemo56(
58294
+ const availableProps = useMemo57(
58283
58295
  () => SUPPORTED_PROPS.filter(
58284
58296
  (p) => !usedProps.has(p) && (animation.method === "set" || !BOOLEAN_PROPS.has(p))
58285
58297
  ),
58286
58298
  [usedProps, animation.method]
58287
58299
  );
58288
- const usedFromProps = useMemo56(
58300
+ const usedFromProps = useMemo57(
58289
58301
  () => new Set(Object.keys(animation.fromProperties ?? {})),
58290
58302
  [animation.fromProperties]
58291
58303
  );
58292
- const availableFromProps = useMemo56(
58304
+ const availableFromProps = useMemo57(
58293
58305
  () => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p) && !BOOLEAN_PROPS.has(p)),
58294
58306
  [usedFromProps]
58295
58307
  );
@@ -58336,7 +58348,7 @@ var AnimationCard = memo37(function AnimationCard2({
58336
58348
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
58337
58349
  const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
58338
58350
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
58339
- const summary = useMemo56(() => buildTweenSummary(animation), [animation]);
58351
+ const summary = useMemo57(() => buildTweenSummary(animation), [animation]);
58340
58352
  const setKeys = Object.keys(animation.properties);
58341
58353
  if (animation.method === "set" && // `every` is vacuously true on an empty bag — require at least one key so a
58342
58354
  // property-less set doesn't masquerade as a position row.
@@ -59484,7 +59496,7 @@ function PropertyPanel3dTransform({
59484
59496
  }
59485
59497
 
59486
59498
  // src/components/editor/PropertyPanelFlat.tsx
59487
- import { useEffect as useEffect103, useMemo as useMemo63, useRef as useRef112, useState as useState100 } from "react";
59499
+ import { useEffect as useEffect103, useMemo as useMemo64, useRef as useRef112, useState as useState100 } from "react";
59488
59500
  import { useShallow as useShallow2 } from "zustand/react/shallow";
59489
59501
 
59490
59502
  // src/components/editor/audioFxSummary.ts
@@ -61654,7 +61666,7 @@ function motionSectionLabel(args) {
61654
61666
  }
61655
61667
 
61656
61668
  // src/components/editor/propertyPanelAudioFxGroup.tsx
61657
- import { useMemo as useMemo59, useState as useState88 } from "react";
61669
+ import { useMemo as useMemo60, useState as useState88 } from "react";
61658
61670
  import {
61659
61671
  HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR11,
61660
61672
  HF_AUDIO_FX_DATA_KEY as HF_AUDIO_FX_DATA_KEY2,
@@ -61674,7 +61686,7 @@ import {
61674
61686
  } from "@hyperframes/core/audio-automation";
61675
61687
 
61676
61688
  // src/components/editor/propertyPanelFxSection.tsx
61677
- import { useCallback as useCallback90, useEffect as useEffect90, useMemo as useMemo58, useRef as useRef100, useState as useState86 } from "react";
61689
+ import { useCallback as useCallback90, useEffect as useEffect90, useMemo as useMemo59, useRef as useRef100, useState as useState86 } from "react";
61678
61690
 
61679
61691
  // src/components/editor/audioFxRevealTarget.ts
61680
61692
  import { parseAutomationTarget as parseAutomationTarget2 } from "@hyperframes/core/audio-automation";
@@ -62632,7 +62644,7 @@ function FxEqModule({
62632
62644
  import { getAudioFxPreset as getAudioFxPreset4 } from "@hyperframes/core/audio-fx-presets";
62633
62645
 
62634
62646
  // src/components/editor/propertyPanelFxNodeRow.tsx
62635
- import { useMemo as useMemo57, useState as useState85 } from "react";
62647
+ import { useMemo as useMemo58, useState as useState85 } from "react";
62636
62648
  import {
62637
62649
  defaultAudioFxParams as defaultAudioFxParams4,
62638
62650
  getAudioFxDef as getAudioFxDef4
@@ -63069,11 +63081,11 @@ function FxNodeRow({
63069
63081
  }) {
63070
63082
  const trackInput = useTrackDesignInput();
63071
63083
  const registryDef = getAudioFxDef4(node.type);
63072
- const def = useMemo57(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
63084
+ const def = useMemo58(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
63073
63085
  const primary = registryDef ? primaryParamOf(registryDef) : null;
63074
63086
  const profile = getAudioFxProfile2(node.type);
63075
- const derived = useMemo57(() => profileParam(node.type), [node.type]);
63076
- const onlyPrimary = useMemo57(
63087
+ const derived = useMemo58(() => profileParam(node.type), [node.type]);
63088
+ const onlyPrimary = useMemo58(
63077
63089
  () => def && primary ? { ...def, params: def.params.filter((p) => p.key === primary) } : def,
63078
63090
  [def, primary]
63079
63091
  );
@@ -63745,12 +63757,12 @@ function FxSection({
63745
63757
  [chain.nodes, mutate, onRemoveNodeAutomation, trackKind]
63746
63758
  );
63747
63759
  const [carveOpen, setCarveOpen] = useState86(true);
63748
- const carveNodes = useMemo58(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
63749
- const handBuilt = useMemo58(
63760
+ const carveNodes = useMemo59(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
63761
+ const handBuilt = useMemo59(
63750
63762
  () => chain.nodes.map((node, i) => ({ node, i })).filter(({ node }) => !node.fromCarve && !node.fromEq),
63751
63763
  [chain.nodes]
63752
63764
  );
63753
- const runs = useMemo58(() => {
63765
+ const runs = useMemo59(() => {
63754
63766
  const out = [];
63755
63767
  for (const item of handBuilt) {
63756
63768
  const preset = item.node.fromPreset;
@@ -63761,8 +63773,8 @@ function FxSection({
63761
63773
  return out;
63762
63774
  }, [handBuilt]);
63763
63775
  const [collapsedRuns, setCollapsedRuns] = useState86(/* @__PURE__ */ new Set());
63764
- const eqIds = useMemo58(() => audioEqIds(chain), [chain]);
63765
- const positions = useMemo58(() => {
63776
+ const eqIds = useMemo59(() => audioEqIds(chain), [chain]);
63777
+ const positions = useMemo59(() => {
63766
63778
  const map = /* @__PURE__ */ new Map();
63767
63779
  let at = (showCarve ? 1 : 0) + eqIds.length;
63768
63780
  for (const { i } of handBuilt) map.set(i, ++at);
@@ -64192,7 +64204,7 @@ function useFxLevelling(element, chain, automation, onSetAttributeQuiet, onSetAt
64192
64204
  if (cached5?.src === src) return cached5;
64193
64205
  const Ctor = window.OfflineAudioContext ?? window.webkitOfflineAudioContext;
64194
64206
  if (!Ctor) return null;
64195
- const res = await fetch(new URL(src, doc.baseURI).href);
64207
+ const res = await studioApiFetch(new URL(src, doc.baseURI).href);
64196
64208
  const buffer = await new Ctor(1, 1, DECODE_SAMPLE_RATE2).decodeAudioData(
64197
64209
  await res.arrayBuffer()
64198
64210
  );
@@ -64391,7 +64403,7 @@ function AudioFxGroup({
64391
64403
  const removeNodeAutomation = (nodeId) => removeNodesAutomation([nodeId]);
64392
64404
  const [analysing, setAnalysing] = useState88(false);
64393
64405
  const storeElements = usePlayerStore((s) => s.elements);
64394
- const signalPath = useMemo59(() => {
64406
+ const signalPath = useMemo60(() => {
64395
64407
  const doc = element.element?.ownerDocument;
64396
64408
  return audioFxSignalPath(
64397
64409
  element.tagName?.toLowerCase(),
@@ -65064,7 +65076,7 @@ function createGsapLivePreview(iframeRef) {
65064
65076
  }
65065
65077
 
65066
65078
  // src/components/editor/propertyPanelFlatColorGradingSection.tsx
65067
- import { useEffect as useEffect100, useMemo as useMemo61, useRef as useRef111, useState as useState96 } from "react";
65079
+ import { useEffect as useEffect100, useMemo as useMemo62, useRef as useRef111, useState as useState96 } from "react";
65068
65080
  import {
65069
65081
  HF_COLOR_GRADING_GRADE_PRESETS,
65070
65082
  normalizeHfColorGrading as normalizeHfColorGrading6,
@@ -65099,7 +65111,7 @@ function presetPreviewHandlers({
65099
65111
  import { useState as useState92 } from "react";
65100
65112
 
65101
65113
  // src/components/editor/propertyPanelColorCurveGraph.tsx
65102
- import { useMemo as useMemo60, useRef as useRef104 } from "react";
65114
+ import { useMemo as useMemo61, useRef as useRef104 } from "react";
65103
65115
  import {
65104
65116
  compileHfColorCurve,
65105
65117
  compileHfHueCurve,
@@ -65287,8 +65299,8 @@ function CurveGraph({
65287
65299
  onCancel
65288
65300
  }) {
65289
65301
  const pointerRef = useRef104(null);
65290
- const samples = useMemo60(() => samplesFor(points, tab), [points, tab]);
65291
- const path = useMemo60(() => curvePath(samples, tab), [samples, tab]);
65302
+ const samples = useMemo61(() => samplesFor(points, tab), [points, tab]);
65303
+ const path = useMemo61(() => curvePath(samples, tab), [samples, tab]);
65292
65304
  const selectRelativePoint = (offset) => {
65293
65305
  if (points.length === 0) return;
65294
65306
  const current = selectedIndex ?? (offset > 0 ? -1 : 0);
@@ -67078,14 +67090,14 @@ function FlatColorGradingSection({
67078
67090
  const lutInputRef = useRef111(null);
67079
67091
  const [lutOpen, setLutOpen] = useState96(false);
67080
67092
  const [detailSettingsOpen, setDetailSettingsOpen] = useState96(null);
67081
- const lutAssets = useMemo61(
67093
+ const lutAssets = useMemo62(
67082
67094
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
67083
67095
  [assets]
67084
67096
  );
67085
67097
  const lut = grading.lut;
67086
67098
  const selectedLutName = lut?.src ? lut.src.split("/").pop() ?? lut.src : null;
67087
- const resolvedGrading = useMemo61(() => resolveColorGrading(grading), [grading]);
67088
- const secondaryInputGrading = useMemo61(
67099
+ const resolvedGrading = useMemo62(() => resolveColorGrading(grading), [grading]);
67100
+ const secondaryInputGrading = useMemo62(
67089
67101
  () => resolveColorGrading({
67090
67102
  intensity: 1,
67091
67103
  adjust: resolvedGrading.adjust,
@@ -67103,7 +67115,7 @@ function FlatColorGradingSection({
67103
67115
  ]
67104
67116
  );
67105
67117
  const actions = createColorGradingActions(grading, onCommitColorGrading);
67106
- const scopesRefreshKey = useMemo61(() => serializeHfColorGrading2(grading), [grading]);
67118
+ const scopesRefreshKey = useMemo62(() => serializeHfColorGrading2(grading), [grading]);
67107
67119
  useEffect100(() => {
67108
67120
  if (presetPreviews.status === "idle") onRequestPresetPreviews();
67109
67121
  }, [onRequestPresetPreviews, presetPreviews.status]);
@@ -68233,7 +68245,7 @@ function FlatEffectsSection({
68233
68245
  import { useState as useState99 } from "react";
68234
68246
 
68235
68247
  // src/hooks/useBlockCatalog.ts
68236
- import { useState as useState98, useEffect as useEffect102, useMemo as useMemo62 } from "react";
68248
+ import { useState as useState98, useEffect as useEffect102, useMemo as useMemo63 } from "react";
68237
68249
 
68238
68250
  // src/utils/blockCategories.ts
68239
68251
  import {
@@ -68291,7 +68303,7 @@ function getCategoryColors(category) {
68291
68303
  var catalogCache = null;
68292
68304
  var catalogRequest = null;
68293
68305
  function loadCatalog() {
68294
- catalogRequest ??= fetch("/api/registry/blocks").then((response) => {
68306
+ catalogRequest ??= studioApiFetch("/api/registry/blocks").then((response) => {
68295
68307
  if (!response.ok) throw new Error("Failed to load catalog");
68296
68308
  return response.json();
68297
68309
  }).then((items) => {
@@ -68332,7 +68344,7 @@ function useBlockCatalog() {
68332
68344
  cancelled = true;
68333
68345
  };
68334
68346
  }, []);
68335
- const filteredBlocks = useMemo62(() => {
68347
+ const filteredBlocks = useMemo63(() => {
68336
68348
  let result = blocks;
68337
68349
  if (category) {
68338
68350
  result = result.filter((b) => b.category === category);
@@ -68648,7 +68660,7 @@ function PropertyPanelFlat({
68648
68660
  onSetAttributeQuiet ?? onSetAttributeLive,
68649
68661
  linkedSpeed
68650
68662
  );
68651
- const audioGroupLabel = useMemo63(() => {
68663
+ const audioGroupLabel = useMemo64(() => {
68652
68664
  const doc = element.element?.ownerDocument;
68653
68665
  const id = element.id;
68654
68666
  if (!doc || !id) return void 0;
@@ -69214,12 +69226,12 @@ var PropertyPanel = memo39(function PropertyPanel2(props) {
69214
69226
  const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
69215
69227
  const iframeRef = previewIframeRef ?? { current: null };
69216
69228
  const gsapAnimIdForMemo = element ? gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null : null;
69217
- const gsapRuntimeValues = useMemo64(
69229
+ const gsapRuntimeValues = useMemo65(
69218
69230
  () => element ? readGsapRuntimeValuesForPanel(gsapAnimIdForMemo, gsapAnimations, element, iframeRef) : null,
69219
69231
  // eslint-disable-next-line react-hooks/exhaustive-deps -- iframeRef is stable; currentTime drives re-reads during playback
69220
69232
  [gsapAnimIdForMemo, gsapAnimations, element, currentTime]
69221
69233
  );
69222
- const gsapBorderRadius = useMemo64(
69234
+ const gsapBorderRadius = useMemo65(
69223
69235
  () => element ? readGsapBorderRadiusForPanel(gsapRuntimeValues, gsapAnimations, element, iframeRef) : null,
69224
69236
  // eslint-disable-next-line react-hooks/exhaustive-deps
69225
69237
  [gsapRuntimeValues, gsapAnimations, element, currentTime]
@@ -69596,11 +69608,11 @@ var PropertyPanel = memo39(function PropertyPanel2(props) {
69596
69608
  });
69597
69609
 
69598
69610
  // src/components/editor/FileTree.tsx
69599
- import { memo as memo41, useState as useState103, useCallback as useCallback92, useMemo as useMemo66, useRef as useRef115 } from "react";
69611
+ import { memo as memo41, useState as useState103, useCallback as useCallback92, useMemo as useMemo67, useRef as useRef115 } from "react";
69600
69612
  import { Plus as Plus2, FolderSimplePlus as FolderSimplePlus2 } from "@phosphor-icons/react";
69601
69613
 
69602
69614
  // 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";
69615
+ import { memo as memo40, useState as useState102, useCallback as useCallback91, useMemo as useMemo66, useRef as useRef114, useEffect as useEffect104 } from "react";
69604
69616
  import {
69605
69617
  PencilSimple,
69606
69618
  Copy,
@@ -70103,7 +70115,7 @@ var TreeFolder = memo40(function TreeFolder2({
70103
70115
  }) {
70104
70116
  const [isOpen, setIsOpen] = useState102(defaultOpen);
70105
70117
  const toggle = useCallback91(() => setIsOpen((v) => !v), []);
70106
- const children = useMemo65(() => sortChildren(node.children), [node.children]);
70118
+ const children = useMemo66(() => sortChildren(node.children), [node.children]);
70107
70119
  const Chevron = isOpen ? ChevronDown : ChevronRight;
70108
70120
  const isDragOver = dragOverFolder === node.fullPath;
70109
70121
  const isRenaming = inlineInput?.mode === "rename" && inlineInput.originalPath === node.fullPath;
@@ -70234,8 +70246,8 @@ var FileTree = memo41(function FileTree2({
70234
70246
  onImportFiles,
70235
70247
  lintFindingsByFile
70236
70248
  }) {
70237
- const tree = useMemo66(() => buildTree(files), [files]);
70238
- const children = useMemo66(() => sortChildren(tree.children), [tree]);
70249
+ const tree = useMemo67(() => buildTree(files), [files]);
70250
+ const children = useMemo67(() => sortChildren(tree.children), [tree]);
70239
70251
  const [contextMenu, setContextMenu] = useState103(null);
70240
70252
  const [inlineInput, setInlineInput] = useState103(null);
70241
70253
  const [deleteTarget, setDeleteTarget] = useState103(null);
@@ -70502,7 +70514,7 @@ var FileTree = memo41(function FileTree2({
70502
70514
  });
70503
70515
 
70504
70516
  // src/App.tsx
70505
- import { useState as useState158, useCallback as useCallback194, useRef as useRef187, useMemo as useMemo88 } from "react";
70517
+ import { useState as useState158, useCallback as useCallback194, useRef as useRef187, useMemo as useMemo89 } from "react";
70506
70518
 
70507
70519
  // src/hooks/useStableHandlers.ts
70508
70520
  import { useRef as useRef116 } from "react";
@@ -70534,9 +70546,9 @@ function useStableHandlers(value, scope) {
70534
70546
  }
70535
70547
 
70536
70548
  // src/hooks/useShellSlices.ts
70537
- import { useMemo as useMemo67 } from "react";
70549
+ import { useMemo as useMemo68 } from "react";
70538
70550
  function useHistoryFlags({ canUndo, canRedo, undoLabel, redoLabel }) {
70539
- return useMemo67(
70551
+ return useMemo68(
70540
70552
  () => ({ canUndo, canRedo, undoLabel, redoLabel }),
70541
70553
  [canUndo, canRedo, undoLabel, redoLabel]
70542
70554
  );
@@ -70552,7 +70564,7 @@ function useToolbarSession(session) {
70552
70564
  handleGsapConvertToKeyframes,
70553
70565
  handleGsapRemoveKeyframe
70554
70566
  } = session;
70555
- return useMemo67(
70567
+ return useMemo68(
70556
70568
  () => ({
70557
70569
  domEditSelection,
70558
70570
  selectedGsapAnimations,
@@ -70625,7 +70637,7 @@ function useRightPanelIntent() {
70625
70637
  }
70626
70638
 
70627
70639
  // src/components/renders/useRenderQueue.ts
70628
- import { useState as useState105, useEffect as useEffect107, useCallback as useCallback95, useRef as useRef119, useMemo as useMemo68 } from "react";
70640
+ import { useState as useState105, useEffect as useEffect107, useCallback as useCallback95, useRef as useRef119, useMemo as useMemo69 } from "react";
70629
70641
 
70630
70642
  // src/components/renders/serverError.ts
70631
70643
  async function readServerError(res) {
@@ -70665,7 +70677,7 @@ function parseStatus(body) {
70665
70677
  }
70666
70678
  async function probe() {
70667
70679
  try {
70668
- const res = await fetch("/api/environment/ffmpeg");
70680
+ const res = await studioApiFetch("/api/environment/ffmpeg");
70669
70681
  return res.ok ? parseStatus(await res.json()) : null;
70670
70682
  } catch {
70671
70683
  return null;
@@ -70889,7 +70901,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
70889
70901
  const loadRenders = useCallback95(async () => {
70890
70902
  if (!projectId) return;
70891
70903
  try {
70892
- const res = await fetch(buildProjectApiPath(projectId, `/renders`));
70904
+ const res = await studioApiFetch(buildProjectApiPath(projectId, `/renders`));
70893
70905
  if (!res.ok) {
70894
70906
  setLoadError(`Couldn't load render history (server error ${res.status}).`);
70895
70907
  return;
@@ -70976,7 +70988,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
70976
70988
  }
70977
70989
  let res;
70978
70990
  try {
70979
- res = await fetch(buildProjectApiPath(projectId, `/render`), {
70991
+ res = await studioApiFetch(buildProjectApiPath(projectId, `/render`), {
70980
70992
  method: "POST",
70981
70993
  headers: { "Content-Type": "application/json" },
70982
70994
  body: JSON.stringify(body)
@@ -71069,7 +71081,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71069
71081
  )
71070
71082
  );
71071
71083
  try {
71072
- const res = await fetch(`/api/render/${jobId}/cancel`, { method: "POST" });
71084
+ const res = await studioApiFetch(`/api/render/${jobId}/cancel`, { method: "POST" });
71073
71085
  if (!res.ok && res.status !== 404) {
71074
71086
  setActionError("Couldn't cancel on the server \u2014 the render may still be running.");
71075
71087
  return;
@@ -71091,7 +71103,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71091
71103
  setActionError(null);
71092
71104
  closeActiveEventSource(jobId);
71093
71105
  try {
71094
- const res = await fetch(`/api/render/${jobId}`, { method: "DELETE" });
71106
+ const res = await studioApiFetch(`/api/render/${jobId}`, { method: "DELETE" });
71095
71107
  if (!res.ok) {
71096
71108
  setActionError("Couldn't delete the render \u2014 it's still on disk.");
71097
71109
  return;
@@ -71148,7 +71160,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
71148
71160
  };
71149
71161
  }, [projectId]);
71150
71162
  const isRendering = jobs.some((j) => j.status === "rendering");
71151
- return useMemo68(
71163
+ return useMemo69(
71152
71164
  () => ({
71153
71165
  jobs,
71154
71166
  isRendering,
@@ -71574,7 +71586,7 @@ async function captureProjectProvenance(projectId, files, compositions) {
71574
71586
  };
71575
71587
  if (!scaffolded) return;
71576
71588
  try {
71577
- const res = await fetch(
71589
+ const res = await studioApiFetch(
71578
71590
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(CONFIG_FILE)}`)
71579
71591
  );
71580
71592
  if (!res.ok) return;
@@ -72195,7 +72207,7 @@ function ProjectUnreachableBanner({ projectId }) {
72195
72207
  const [served, setServed] = useState110(null);
72196
72208
  useEffect110(() => {
72197
72209
  let cancelled = false;
72198
- fetch("/api/projects").then((res) => res.ok ? res.json() : { projects: [] }).then((data) => {
72210
+ studioApiFetch("/api/projects").then((res) => res.ok ? res.json() : { projects: [] }).then((data) => {
72199
72211
  if (!cancelled) setServed(data.projects ?? []);
72200
72212
  }).catch(() => {
72201
72213
  if (!cancelled) setServed([]);
@@ -72305,11 +72317,14 @@ function useCaptionSync(projectId) {
72305
72317
  }
72306
72318
  const seqAtSave = editSeqRef.current;
72307
72319
  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) => {
72320
+ studioApiFetch(
72321
+ buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
72322
+ {
72323
+ method: "PUT",
72324
+ headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
72325
+ body: JSON.stringify(overrides, null, 2)
72326
+ }
72327
+ ).then((res) => {
72313
72328
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
72314
72329
  if (editSeqRef.current === seqAtSave) pendingRef.current = false;
72315
72330
  const s = useCaptionStore.getState();
@@ -72362,7 +72377,7 @@ function useCaptionSync(projectId) {
72362
72377
  if (!pid) return;
72363
72378
  let data;
72364
72379
  try {
72365
- const res = await fetch(
72380
+ const res = await studioApiFetch(
72366
72381
  buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`)
72367
72382
  );
72368
72383
  if (!res.ok) return;
@@ -72418,13 +72433,15 @@ function useCaptionSync(projectId) {
72418
72433
  }
72419
72434
 
72420
72435
  // src/hooks/usePersistentEditHistory.ts
72421
- import { useCallback as useCallback99, useEffect as useEffect111, useMemo as useMemo69, useRef as useRef126, useState as useState111 } from "react";
72436
+ import { useCallback as useCallback99, useEffect as useEffect111, useMemo as useMemo70, useRef as useRef126, useState as useState111 } from "react";
72422
72437
  var EMPTY3 = { entries: [], back: null, forward: null };
72423
72438
  var OWN_ENTRIES_KEPT = 100;
72424
72439
  function createOwnHistory() {
72425
72440
  const own = /* @__PURE__ */ new Map();
72426
72441
  let next = null;
72427
72442
  let changes = 0;
72443
+ let claimed = { count: 0, id: "" };
72444
+ const undoneIds = /* @__PURE__ */ new Set();
72428
72445
  const remember = (id, files) => {
72429
72446
  const known = own.get(id) ?? {};
72430
72447
  for (const [path, { before, after }] of Object.entries(files)) {
@@ -72445,7 +72462,15 @@ function createOwnHistory() {
72445
72462
  if (seen === changes) next = { undo: view.back, redo: view.forward };
72446
72463
  },
72447
72464
  changes: () => changes,
72465
+ claimCount: () => claimed.count,
72466
+ claimedAfter: (count) => claimed.count > count && claimed.id ? claimed.id : null,
72467
+ targetFor: (id, pressedAt) => !id || !undoneIds.has(id) ? id : claimed.count > pressedAt ? false : null,
72468
+ noteClaim: (id) => {
72469
+ claimed = { count: claimed.count + 1, id };
72470
+ },
72448
72471
  stepped: (entry2) => {
72472
+ if (entry2.undoes) undoneIds.add(entry2.undoes);
72473
+ if (undoneIds.size > OWN_ENTRIES_KEPT) undoneIds.delete(undoneIds.values().next().value);
72449
72474
  const undone = entry2.undoes ? own.get(entry2.undoes) : void 0;
72450
72475
  if (!undone) return;
72451
72476
  const swapped = Object.entries(undone).map(([path, f]) => [
@@ -72461,14 +72486,15 @@ function createOwnHistory() {
72461
72486
  const paths = files ? Object.keys(files) : [];
72462
72487
  if (!files || paths.length !== step.paths.length || !paths.every((p) => step.paths.includes(p)))
72463
72488
  return null;
72464
- const restore = paths.map((path) => [
72489
+ const restore2 = paths.map((path) => [
72465
72490
  path,
72466
72491
  { previous: files[path].after, restored: files[path].before }
72467
72492
  ]);
72468
- return { id: step.id, files: Object.fromEntries(restore) };
72493
+ return { id: step.id, files: Object.fromEntries(restore2) };
72469
72494
  },
72470
72495
  clear: () => {
72471
72496
  own.clear();
72497
+ undoneIds.clear();
72472
72498
  next = null;
72473
72499
  changes += 1;
72474
72500
  }
@@ -72478,8 +72504,14 @@ var DEFAULT_COALESCE_MS = 300;
72478
72504
  function historyUrl(projectId, path = "") {
72479
72505
  return `/api/projects/${encodeURIComponent(projectId)}/history${path}`;
72480
72506
  }
72507
+ async function cantUndo(paths) {
72508
+ return { ok: false, reason: "content-mismatch", paths: paths ?? [] };
72509
+ }
72510
+ function unionPaths(...lists) {
72511
+ return [...new Set(lists.flatMap((list) => list ?? []))];
72512
+ }
72481
72513
  async function post(url, body, headers = {}) {
72482
- const response = await fetch(url, {
72514
+ const response = await studioApiFetch(url, {
72483
72515
  method: "POST",
72484
72516
  headers: { "Content-Type": "application/json", ...headers },
72485
72517
  body: JSON.stringify(body)
@@ -72538,9 +72570,10 @@ function usePersistentEditHistory({ projectId }) {
72538
72570
  const refresh = useCallback99(async () => {
72539
72571
  if (!projectId) return;
72540
72572
  const seen = own.changes();
72541
- const response = await fetch(historyUrl(projectId)).catch(() => null);
72542
- const next = response?.ok ? await response.json() : EMPTY3;
72573
+ const response = await studioApiFetch(historyUrl(projectId)).catch(() => null);
72574
+ const next = response?.ok ? await response.json().catch(() => null) : EMPTY3;
72543
72575
  if (projectIdRef.current !== projectId) return;
72576
+ if (!next) return console.error("The history's reply was unreadable.");
72544
72577
  setView(next);
72545
72578
  own.offered(seen, next);
72546
72579
  }, [projectId, own]);
@@ -72563,7 +72596,10 @@ function usePersistentEditHistory({ projectId }) {
72563
72596
  ...coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }
72564
72597
  });
72565
72598
  const claimed = claimHeld(reply, label);
72566
- if (claimed) own.remember(claimed, files);
72599
+ if (claimed) {
72600
+ own.remember(claimed, files);
72601
+ own.noteClaim(claimed);
72602
+ }
72567
72603
  heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
72568
72604
  await refresh();
72569
72605
  },
@@ -72572,14 +72608,17 @@ function usePersistentEditHistory({ projectId }) {
72572
72608
  const step = useCallback99(
72573
72609
  async (direction, callbacks) => {
72574
72610
  if (!projectId) return { ok: false, reason: "empty" };
72611
+ const candidate = direction === "undo" && callbacks.claimedAfter !== void 0 ? own.claimedAfter(callbacks.claimedAfter) : null;
72575
72612
  const next = direction === "undo" ? view.back : view.forward;
72576
- const paths = [.../* @__PURE__ */ new Set([...next?.paths ?? [], ...heldClaimRef.current?.paths ?? []])];
72613
+ const stepPaths = candidate ? Object.keys(own.afterOf(candidate)) : next?.paths;
72614
+ const paths = unionPaths(stepPaths, heldClaimRef.current?.paths);
72615
+ const pressedAt = own.claimCount();
72577
72616
  own.overtake();
72578
- const run = async () => {
72617
+ const attempt = async (target) => {
72579
72618
  const previous = await readAll(paths, callbacks.readFile);
72580
72619
  const posted = await post(
72581
- historyUrl(projectId, "/step"),
72582
- { direction: direction === "undo" ? "back" : "forward" },
72620
+ historyUrl(projectId, target ? "/undo" : "/step"),
72621
+ target ? { entryId: target } : { direction: direction === "undo" ? "back" : "forward" },
72583
72622
  studioWriteHeaders()
72584
72623
  );
72585
72624
  heldClaimRef.current = null;
@@ -72606,6 +72645,10 @@ function usePersistentEditHistory({ projectId }) {
72606
72645
  )
72607
72646
  };
72608
72647
  };
72648
+ const run = () => {
72649
+ const target = own.targetFor(candidate, pressedAt);
72650
+ return target === false ? cantUndo(stepPaths) : attempt(target);
72651
+ };
72609
72652
  return callbacks.serialize ? callbacks.serialize(paths, run) : run();
72610
72653
  },
72611
72654
  [projectId, view, refresh, own]
@@ -72616,7 +72659,7 @@ function usePersistentEditHistory({ projectId }) {
72616
72659
  }, [own, refresh]);
72617
72660
  const undo = useCallback99((callbacks) => step("undo", callbacks), [step]);
72618
72661
  const redo = useCallback99((callbacks) => step("redo", callbacks), [step]);
72619
- const state = useMemo69(() => {
72662
+ const state = useMemo70(() => {
72620
72663
  const backAt = Math.max(view.back?.endedAt ?? 0, heldClaimRef.current?.at ?? 0);
72621
72664
  const redoAt = redoneAt(view);
72622
72665
  return {
@@ -72637,6 +72680,7 @@ function usePersistentEditHistory({ projectId }) {
72637
72680
  undo,
72638
72681
  redo,
72639
72682
  predict: own.predict,
72683
+ claims: own.claimCount,
72640
72684
  noteOutsideChange
72641
72685
  };
72642
72686
  }
@@ -72701,7 +72745,7 @@ function usePanelLayout(initialState2) {
72701
72745
  import { useState as useState113, useCallback as useCallback104, useRef as useRef130 } from "react";
72702
72746
 
72703
72747
  // src/hooks/useFileTree.ts
72704
- import { useState as useState112, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo70, useRef as useRef128 } from "react";
72748
+ import { useState as useState112, useCallback as useCallback101, useEffect as useEffect113, useMemo as useMemo71, useRef as useRef128 } from "react";
72705
72749
  var EMPTY_FILE_LIST = [];
72706
72750
  function useFileTree({ projectId, projectIdRef }) {
72707
72751
  const [fetched, setFetched] = useState112(null);
@@ -72714,7 +72758,7 @@ function useFileTree({ projectId, projectIdRef }) {
72714
72758
  if (!projectId) return;
72715
72759
  let cancelled = false;
72716
72760
  setFetched({ projectId, loaded: false, fileTree: [], compositionPaths: [], projectDir: null });
72717
- fetch(buildProjectApiPath(projectId)).then((r) => {
72761
+ studioApiFetch(buildProjectApiPath(projectId)).then((r) => {
72718
72762
  if (!r.ok) throw new Error(`tree fetch failed: ${r.status}`);
72719
72763
  return r.json();
72720
72764
  }).then((data) => {
@@ -72747,7 +72791,7 @@ function useFileTree({ projectId, projectIdRef }) {
72747
72791
  refreshAbortRef.current = controller;
72748
72792
  let data;
72749
72793
  try {
72750
- const res = await fetch(buildProjectApiPath(pid), { signal: controller.signal });
72794
+ const res = await studioApiFetch(buildProjectApiPath(pid), { signal: controller.signal });
72751
72795
  data = await res.json();
72752
72796
  } catch (err) {
72753
72797
  if (err instanceof DOMException && err.name === "AbortError") return;
@@ -72765,11 +72809,11 @@ function useFileTree({ projectId, projectIdRef }) {
72765
72809
  );
72766
72810
  }
72767
72811
  }, [projectIdRef]);
72768
- const assets = useMemo70(
72812
+ const assets = useMemo71(
72769
72813
  () => fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
72770
72814
  [fileTree]
72771
72815
  );
72772
- const fontAssets = useMemo70(
72816
+ const fontAssets = useMemo71(
72773
72817
  () => (projectId ? assets : []).filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
72774
72818
  family: fontFamilyFromAssetPath(asset),
72775
72819
  path: asset,
@@ -72913,9 +72957,18 @@ function useEditorSave({
72913
72957
  }
72914
72958
 
72915
72959
  // src/hooks/useProjectFileWriter.ts
72916
- import { useCallback as useCallback103, useMemo as useMemo71 } from "react";
72960
+ import { useCallback as useCallback103, useMemo as useMemo72 } from "react";
72961
+ async function fetchProjectFile(projectId, path, query) {
72962
+ if (!projectId) throw new Error("No active project");
72963
+ const response = await studioApiFetch(
72964
+ `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}${query}`
72965
+ );
72966
+ if (!response.ok) throw new Error(`Failed to read ${path}`);
72967
+ const data = await response.json();
72968
+ return { content: data.content, version: data.version ?? response.headers.get("etag") };
72969
+ }
72917
72970
  function useProjectFileWriter({ projectId }) {
72918
- const fileVersionScope = useMemo71(
72971
+ const fileVersionScope = useMemo72(
72919
72972
  () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
72920
72973
  [projectId]
72921
72974
  );
@@ -72928,28 +72981,18 @@ function useProjectFileWriter({ projectId }) {
72928
72981
  );
72929
72982
  const readProjectFile = useCallback103(
72930
72983
  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;
72984
+ const { content, version } = await fetchProjectFile(projectId, path, "");
72985
+ if (typeof content !== "string") throw new Error(`Missing file contents for ${path}`);
72986
+ fileVersions.set(path, version);
72987
+ return content;
72940
72988
  },
72941
72989
  [fileVersions, projectId]
72942
72990
  );
72943
72991
  const readOptionalProjectFile = useCallback103(
72944
72992
  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 : "";
72993
+ const { content, version } = await fetchProjectFile(projectId, path, "?optional=1");
72994
+ fileVersions.set(path, version);
72995
+ return typeof content === "string" ? content : "";
72953
72996
  },
72954
72997
  [fileVersions, projectId]
72955
72998
  );
@@ -72959,7 +73002,7 @@ function useProjectFileWriter({ projectId }) {
72959
73002
  const writeProjectId = projectId;
72960
73003
  let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
72961
73004
  if (expectedVersion === void 0) {
72962
- const preflight = await fetch(
73005
+ const preflight = await studioApiFetch(
72963
73006
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`
72964
73007
  );
72965
73008
  if (preflight.ok) {
@@ -72979,7 +73022,7 @@ function useProjectFileWriter({ projectId }) {
72979
73022
  await retryStudioSave(async () => {
72980
73023
  let response;
72981
73024
  try {
72982
- response = await fetch(
73025
+ response = await studioApiFetch(
72983
73026
  `/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
72984
73027
  {
72985
73028
  method: "PUT",
@@ -73020,7 +73063,7 @@ function useProjectFileWriter({ projectId }) {
73020
73063
  },
73021
73064
  [fileVersions, projectId]
73022
73065
  );
73023
- return useMemo71(
73066
+ return useMemo72(
73024
73067
  () => ({
73025
73068
  fileVersions,
73026
73069
  readProjectFile,
@@ -73124,7 +73167,7 @@ function useFileManager({
73124
73167
  setEditingFile({ path, content: null });
73125
73168
  return;
73126
73169
  }
73127
- fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`).then((r) => {
73170
+ studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`).then((r) => {
73128
73171
  if (!r.ok) throw new Error(`Failed to load ${path} (${r.status})`);
73129
73172
  return r.json();
73130
73173
  }).then((data) => {
@@ -73152,9 +73195,12 @@ function useFileManager({
73152
73195
  const requestId = ++revealRequestIdRef.current;
73153
73196
  const controller = new AbortController();
73154
73197
  revealAbortRef.current = controller;
73155
- fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`, {
73156
- signal: controller.signal
73157
- }).then((r) => r.json()).then((data) => {
73198
+ studioApiFetch(
73199
+ `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`,
73200
+ {
73201
+ signal: controller.signal
73202
+ }
73203
+ ).then((r) => r.json()).then((data) => {
73158
73204
  if (requestId !== revealRequestIdRef.current) return;
73159
73205
  if (data.content != null) {
73160
73206
  fileVersions.set(sourceFile, data.version ?? null);
@@ -73178,7 +73224,7 @@ function useFileManager({
73178
73224
  }
73179
73225
  const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
73180
73226
  try {
73181
- const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
73227
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
73182
73228
  method: "POST",
73183
73229
  body: formData
73184
73230
  });
@@ -73218,7 +73264,7 @@ function useFileManager({
73218
73264
  if (path.endsWith(".html")) {
73219
73265
  content = '<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n</head>\n<body>\n\n</body>\n</html>\n';
73220
73266
  }
73221
- const res = await fetch(
73267
+ const res = await studioApiFetch(
73222
73268
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
73223
73269
  {
73224
73270
  method: "POST",
@@ -73241,7 +73287,7 @@ function useFileManager({
73241
73287
  async (path) => {
73242
73288
  const pid = projectIdRef.current;
73243
73289
  if (!pid) return;
73244
- const res = await fetch(
73290
+ const res = await studioApiFetch(
73245
73291
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path + "/.gitkeep")}`,
73246
73292
  {
73247
73293
  method: "POST",
@@ -73263,7 +73309,7 @@ function useFileManager({
73263
73309
  async (path) => {
73264
73310
  const pid = projectIdRef.current;
73265
73311
  if (!pid) return;
73266
- const res = await fetch(
73312
+ const res = await studioApiFetch(
73267
73313
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
73268
73314
  {
73269
73315
  method: "DELETE"
@@ -73284,7 +73330,7 @@ function useFileManager({
73284
73330
  async (oldPath, newPath) => {
73285
73331
  const pid = projectIdRef.current;
73286
73332
  if (!pid) return;
73287
- const res = await fetch(
73333
+ const res = await studioApiFetch(
73288
73334
  `/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(oldPath)}`,
73289
73335
  {
73290
73336
  method: "PATCH",
@@ -73310,7 +73356,7 @@ function useFileManager({
73310
73356
  async (path) => {
73311
73357
  const pid = projectIdRef.current;
73312
73358
  if (!pid) return;
73313
- const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
73359
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
73314
73360
  method: "POST",
73315
73361
  headers: { "Content-Type": "application/json" },
73316
73362
  body: JSON.stringify({ path })
@@ -73619,18 +73665,17 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
73619
73665
  const active = files[activeDocPath];
73620
73666
  const restoredScript = active ? extractGsapScriptText(active.restored) : null;
73621
73667
  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) {
73668
+ const reparse = plan.scripted && plan.targets.some(({ live }) => "_gsap" in live);
73669
+ if (reparse && !restoredScript) {
73624
73670
  reloadPreview();
73625
73671
  return "full";
73626
73672
  }
73627
- if (restoredScript && (restoredScript !== previousScript || reparse.length)) {
73673
+ if (restoredScript && (restoredScript !== previousScript || reparse)) {
73628
73674
  syncStaleEditMarks(doc, active);
73629
73675
  const result = applySoftReload(iframe, restoredScript, {
73630
73676
  onAsyncFailure: reloadPreview,
73631
73677
  currentTimeOverride: currentTime,
73632
73678
  authoredHtml: active.restored,
73633
- reparse,
73634
73679
  nestedFiles
73635
73680
  });
73636
73681
  if (result === "cannot-soft-reload") {
@@ -73799,8 +73844,9 @@ function usePreviewPersistence({
73799
73844
  if (result.status !== "clean") throw result.error;
73800
73845
  }, [drainPendingDomEditSaves]);
73801
73846
  const settlePendingEdits = useCallback105(async () => {
73802
- await flushStudioPendingEdits();
73803
- await domEditSaveQueueRef.current?.waitForIdle();
73847
+ const queued = domEditSaveQueueRef.current?.waitForIdle();
73848
+ await flushStudioPendingEdits({ onlyCurrent: true });
73849
+ await queued;
73804
73850
  }, []);
73805
73851
  const resetDomEditSaveQueueBreaker = useCallback105(() => {
73806
73852
  domEditSaveQueueRef.current?.reset();
@@ -73825,20 +73871,20 @@ function usePreviewPersistence({
73825
73871
  );
73826
73872
  applyStudioManualEditsToPreviewRef.current = applyStudioManualEditsToPreview;
73827
73873
  const syncHistoryPreviewAfterApply = useCallback105(
73828
- async (restore) => {
73874
+ async (restore2) => {
73829
73875
  const projectId = usePlayerStore.getState().timelineProjectId;
73830
73876
  const nestedFiles = await settleNestedReads(
73831
- projectId ? readUndoNestedFiles(
73877
+ readUndoNestedFiles(
73832
73878
  previewIframeRef.current,
73833
73879
  activeCompPathRef.current,
73834
- restore.files,
73835
- (path) => readProjectFileContent(projectId, path)
73836
- ) : null
73880
+ restore2.files,
73881
+ (path) => projectId ? readProjectFileContent(projectId, path) : Promise.reject(new Error("No project is open to read nested files from."))
73882
+ )
73837
73883
  );
73838
73884
  const strategy = applyUndoRestoreToPreview(
73839
73885
  previewIframeRef.current,
73840
73886
  activeCompPathRef.current,
73841
- restore.files,
73887
+ restore2.files,
73842
73888
  usePlayerStore.getState().currentTime,
73843
73889
  reloadPreview,
73844
73890
  nestedFiles
@@ -73952,7 +73998,7 @@ function buildAtomicCutIntents(elements, splitTime, activeCompPath) {
73952
73998
  return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
73953
73999
  }
73954
74000
  async function readFileVersion(projectId, path) {
73955
- const response = await fetch(
74001
+ const response = await studioApiFetch(
73956
74002
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(path)}`)
73957
74003
  );
73958
74004
  if (!response.ok) throw new Error(`Failed to read ${path} before cut (${response.status})`);
@@ -73971,14 +74017,17 @@ async function requestAtomicCut(projectId, intents) {
73971
74017
  }
73972
74018
  const transactionToken = `cut:${crypto.randomUUID()}`;
73973
74019
  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
- });
74020
+ const response = await studioApiFetch(
74021
+ buildProjectApiPath(projectId, "/file-mutations/split-batch"),
74022
+ {
74023
+ method: "POST",
74024
+ headers: {
74025
+ "Content-Type": "application/json",
74026
+ "X-Hyperframes-Write-Token": transactionToken
74027
+ },
74028
+ body: JSON.stringify({ files, transactionToken })
74029
+ }
74030
+ );
73982
74031
  const body = await response.json().catch(() => null);
73983
74032
  if (!response.ok || body?.ok !== true || !Array.isArray(body.files)) {
73984
74033
  const prefix = response.status === 409 ? "Cut conflict" : "Cut failed";
@@ -74046,7 +74095,7 @@ function errorOf(value) {
74046
74095
  return typeof value.error === "string" ? value.error : null;
74047
74096
  }
74048
74097
  async function readVersion(projectId, path) {
74049
- const response = await fetch(
74098
+ const response = await studioApiFetch(
74050
74099
  buildProjectApiPath(projectId, `/files/${encodeURIComponent(path)}`)
74051
74100
  );
74052
74101
  const body = await response.json().catch(() => null);
@@ -74060,7 +74109,7 @@ async function requestFreezeFrame(input) {
74060
74109
  const expectedVersion = await readVersion(input.projectId, input.path);
74061
74110
  const transactionToken = `freeze:${crypto.randomUUID()}`;
74062
74111
  markStudioWriteToken(transactionToken);
74063
- const response = await fetch(
74112
+ const response = await studioApiFetch(
74064
74113
  buildProjectApiPath(input.projectId, "/file-mutations/freeze-frame"),
74065
74114
  {
74066
74115
  method: "POST",
@@ -74315,7 +74364,7 @@ function resolveDropTrack(input) {
74315
74364
 
74316
74365
  // src/utils/timelineCompositionInsert.ts
74317
74366
  async function insertTimelineComposition(input) {
74318
- const current = await fetch(
74367
+ const current = await studioApiFetch(
74319
74368
  buildProjectApiPath(input.projectId, `/files/${encodeURIComponent(input.targetPath)}`)
74320
74369
  );
74321
74370
  if (!current.ok) {
@@ -74323,7 +74372,7 @@ async function insertTimelineComposition(input) {
74323
74372
  }
74324
74373
  const snapshot3 = await current.json();
74325
74374
  if (typeof snapshot3.version !== "string") throw new Error("Missing composition file version");
74326
- const response = await fetch(
74375
+ const response = await studioApiFetch(
74327
74376
  buildProjectApiPath(
74328
74377
  input.projectId,
74329
74378
  `/file-mutations/insert-composition/${encodeURIComponent(input.targetPath)}`
@@ -74656,10 +74705,10 @@ function useTimelineEditRefusal(canEdit, showToast) {
74656
74705
  );
74657
74706
  }
74658
74707
  function useTimelineEditGate(canEdit, showToast) {
74659
- const refuse = useTimelineEditRefusal(canEdit, showToast);
74708
+ const refuse2 = useTimelineEditRefusal(canEdit, showToast);
74660
74709
  return useCallback110(
74661
- (targets) => refuse(targets) === null,
74662
- [refuse]
74710
+ (targets) => refuse2(targets) === null,
74711
+ [refuse2]
74663
74712
  );
74664
74713
  }
74665
74714
 
@@ -75261,7 +75310,7 @@ function useTimelineDeleteOps({
75261
75310
  }
75262
75311
  return patchTarget;
75263
75312
  });
75264
- const removeResponse = await fetch(
75313
+ const removeResponse = await studioApiFetch(
75265
75314
  buildProjectApiPath(
75266
75315
  pid,
75267
75316
  `/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`
@@ -75892,11 +75941,11 @@ function assertAudioGroupEditable(checkEditable, elements) {
75892
75941
  throw new Error("Timeline edit blocked");
75893
75942
  }
75894
75943
  }
75895
- function patchLiveAudioGroupState(iframe, elements, groupId, activeCompPath, restore) {
75944
+ function patchLiveAudioGroupState(iframe, elements, groupId, activeCompPath, restore2) {
75896
75945
  for (const element of elements) {
75897
75946
  const target = findTimelineElementInIframe(iframe, element, activeCompPath);
75898
75947
  if (!target) continue;
75899
- const next = restore ? restore.get(element) ?? null : groupId;
75948
+ const next = restore2 ? restore2.get(element) ?? null : groupId;
75900
75949
  if (next) target.setAttribute(HF_AUDIO_GROUP_ATTR5, next);
75901
75950
  else target.removeAttribute(HF_AUDIO_GROUP_ATTR5);
75902
75951
  }
@@ -76896,9 +76945,9 @@ function useTimelineEditing({
76896
76945
  handleTimelineGroupResize: track2(
76897
76946
  guard((changes) => changes.map((c) => c.element), groupEditing.handleTimelineGroupResize)
76898
76947
  ),
76899
- restoreLiveLanes: (restore) => {
76900
- setElementFxAttribute.restoreLive(restore);
76901
- setAudioGroupAttribute2.restoreLive(restore);
76948
+ restoreLiveLanes: (restore2) => {
76949
+ setElementFxAttribute.restoreLive(restore2);
76950
+ setAudioGroupAttribute2.restoreLive(restore2);
76902
76951
  }
76903
76952
  };
76904
76953
  return useStableHandlers(editing, projectId);
@@ -76958,7 +77007,7 @@ function useAskAgentModal({
76958
77007
  if (!pid) return;
76959
77008
  const targetPath = selection.sourceFile || activeCompPath || "index.html";
76960
77009
  try {
76961
- const response = await fetch(
77010
+ const response = await studioApiFetch(
76962
77011
  buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`)
76963
77012
  );
76964
77013
  if (!response.ok) return;
@@ -78014,7 +78063,7 @@ async function assignGsapTargetAutoIdIfNeeded({
78014
78063
  showToast?.("Couldn't assign element id because the patch contains invalid values", "error");
78015
78064
  return false;
78016
78065
  }
78017
- const res = await fetch(
78066
+ const res = await studioApiFetch(
78018
78067
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
78019
78068
  {
78020
78069
  method: "POST",
@@ -78080,7 +78129,7 @@ function isAtomicElementPatchFile(value) {
78080
78129
  async function patchElementBatches(projectId, batches) {
78081
78130
  const body = JSON.stringify({ batches });
78082
78131
  try {
78083
- const response = await fetch(
78132
+ const response = await studioApiFetch(
78084
78133
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
78085
78134
  {
78086
78135
  method: "POST",
@@ -78120,7 +78169,7 @@ function batchesAreInlineStyleOnly(batches) {
78120
78169
  );
78121
78170
  }
78122
78171
  async function postPatchElement(projectId, targetPath, body, showToast) {
78123
- const response = await fetch(
78172
+ const response = await studioApiFetch(
78124
78173
  buildProjectApiPath(
78125
78174
  projectId,
78126
78175
  `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`
@@ -78418,7 +78467,13 @@ var IMAGE_BACKGROUND_FIT = [
78418
78467
  ["background-repeat", "no-repeat"],
78419
78468
  ["background-size", "contain"]
78420
78469
  ];
78421
- async function commitDomStyles(context, selection, styles) {
78470
+ async function commitDomStyles(context, selection, stylesInEitherCase) {
78471
+ const styles = Object.fromEntries(
78472
+ Object.entries(stylesInEitherCase).map(([property, value]) => [
78473
+ cssPropertyName(property),
78474
+ value
78475
+ ])
78476
+ );
78422
78477
  const entries = Object.entries(styles);
78423
78478
  if (entries.length === 0) return domEditCommitDeclined("no-selection");
78424
78479
  if (entries.some(([property]) => isManualGeometryStyleProperty(property)))
@@ -78855,7 +78910,7 @@ function useDomEditTextCommits({
78855
78910
  persistDomEditOperations
78856
78911
  });
78857
78912
  const handleDomStyleCommitForSelection = useCallback130(
78858
- (selection, property, value) => commitDomStyles(
78913
+ (selection, propertyOrStylesAsOnePatch, value = "") => commitDomStyles(
78859
78914
  {
78860
78915
  activeCompPath,
78861
78916
  previewIframeRef,
@@ -78866,7 +78921,7 @@ function useDomEditTextCommits({
78866
78921
  resync: refreshDomEditSelectionFromPreview
78867
78922
  },
78868
78923
  selection,
78869
- { [property]: value }
78924
+ typeof propertyOrStylesAsOnePatch === "string" ? { [propertyOrStylesAsOnePatch]: value } : propertyOrStylesAsOnePatch
78870
78925
  ),
78871
78926
  [
78872
78927
  activeCompPath,
@@ -78953,24 +79008,24 @@ function useDomEditTextCommits({
78953
79008
  if (element.isConnected && element.innerHTML === html2) element.innerHTML = previousHtml;
78954
79009
  };
78955
79010
  if (latestReadOnlyPreviewRef.current) return putBack();
78956
- const refuse = (reason) => {
79011
+ const refuse2 = (reason) => {
78957
79012
  console.error("[Studio] text edit not saved:", reason, element);
78958
79013
  showToast(`Couldn't save the text edit: ${reason}`, "error");
78959
79014
  putBack();
78960
79015
  };
78961
79016
  const doc = previewIframeRef.current?.contentDocument;
78962
79017
  if (!doc || !element.isConnected || element.ownerDocument !== doc) {
78963
- return refuse("the text's element is gone from the preview");
79018
+ return refuse2("the text's element is gone from the preview");
78964
79019
  }
78965
79020
  const selection = await buildDomSelectionFromTarget(element, {
78966
79021
  exactTarget: true,
78967
79022
  skipSourceProbe: true
78968
79023
  });
78969
79024
  if (selection?.element !== element) {
78970
- return refuse("this text was not found in the composition's source");
79025
+ return refuse2("this text was not found in the composition's source");
78971
79026
  }
78972
79027
  if (!canCommitInlineTextSelection(selection, element)) {
78973
- return refuse("this text can't be edited in place");
79028
+ return refuse2("this text can't be edited in place");
78974
79029
  }
78975
79030
  const isLatestTextCommit = bumpDomEditCommitMapVersion(
78976
79031
  domTextCommitVersionRef.current,
@@ -79229,18 +79284,18 @@ var ELEMENT_OFFSET_REFUSED = {
79229
79284
  anchored: "This layer is anchored from its right or bottom edge. Move it in the Code tab.",
79230
79285
  percent: "This layer's position is set in percent. Move it in the Code tab."
79231
79286
  };
79232
- function gsapOf(el) {
79287
+ function gsapOf2(el) {
79233
79288
  return el.ownerDocument.defaultView?.gsap;
79234
79289
  }
79235
79290
  function settleGsapDraftAtGestureStart(el) {
79236
- const gsap3 = gsapOf(el);
79291
+ const gsap3 = gsapOf2(el);
79237
79292
  const x = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-x") ?? "");
79238
79293
  const y = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
79239
79294
  if (gsap3 && Number.isFinite(x) && Number.isFinite(y)) gsap3.set(el, { x, y });
79240
79295
  }
79241
79296
  function refuseGsapTakeover(el, showToast) {
79242
79297
  if (!gsapHoldsTranslate(el)) return;
79243
- gsapOf(el)?.set(el, { x: 0, y: 0, xPercent: 0, yPercent: 0 });
79298
+ gsapOf2(el)?.set(el, { x: 0, y: 0, xPercent: 0, yPercent: 0 });
79244
79299
  showToast(GSAP_TOOK_OVER, "error");
79245
79300
  throw markStudioSaveErrorAlreadyToasted(new Error(GSAP_TOOK_OVER));
79246
79301
  }
@@ -79331,7 +79386,7 @@ function useDomGeometryCommits({
79331
79386
  [commitPositionPatchToHtml, readOnlyPreview, showToast]
79332
79387
  );
79333
79388
  const handleDomBoxSizeCommit = useCallback131(
79334
- (selection, next, offset, restore, undoKey) => {
79389
+ (selection, next, offset, restore2, undoKey) => {
79335
79390
  if (readOnlyPreview) return Promise.resolve(void 0);
79336
79391
  const element = selection.element;
79337
79392
  const beforeSize = captureStudioBoxSize(element);
@@ -79349,7 +79404,7 @@ function useDomGeometryCommits({
79349
79404
  restoreStudioBoxSize(element, beforeSize);
79350
79405
  if (offset) restoreStudioPathOffset(element, beforeOffset);
79351
79406
  crop?.revert();
79352
- restore?.();
79407
+ restore2?.();
79353
79408
  throw error;
79354
79409
  });
79355
79410
  },
@@ -79672,7 +79727,7 @@ async function commitStructuralMutation(pid, targetPath, route, body, label, dep
79672
79727
  writeFile: deps.writeProjectFile,
79673
79728
  recordEdit: deps.editHistory.recordEdit,
79674
79729
  rewrite: async (originalContent) => {
79675
- const mutateResponse = await fetch(
79730
+ const mutateResponse = await studioApiFetch(
79676
79731
  buildProjectApiPath(pid, `/file-mutations/${route}/${encodeURIComponent(targetPath)}`),
79677
79732
  {
79678
79733
  method: "POST",
@@ -79749,7 +79804,7 @@ function useGroupCommits(params) {
79749
79804
  }
79750
79805
 
79751
79806
  // src/hooks/useGsapScriptCommits.ts
79752
- import { useCallback as useCallback139, useMemo as useMemo72, useRef as useRef148 } from "react";
79807
+ import { useCallback as useCallback139, useMemo as useMemo73, useRef as useRef148 } from "react";
79753
79808
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
79754
79809
 
79755
79810
  // src/hooks/gsapRuntimePatch.ts
@@ -79943,6 +79998,11 @@ function createKeyedSerializer() {
79943
79998
  };
79944
79999
  }
79945
80000
 
80001
+ // src/hooks/gsapScriptCommitTypes.ts
80002
+ function gsapWriteFile(selection, activeCompPath) {
80003
+ return selection.sourceFile || activeCompPath || "index.html";
80004
+ }
80005
+
79946
80006
  // src/hooks/useGsapAnimationOps.ts
79947
80007
  import { useCallback as useCallback134 } from "react";
79948
80008
  function useGsapAnimationOps({
@@ -80862,7 +80922,7 @@ function useSafeGsapCommitMutation(commitMutation, trackGsapSaveFailure, showToa
80862
80922
 
80863
80923
  // src/hooks/useGsapScriptCommits.ts
80864
80924
  async function mutateGsapScript(projectId, sourceFile, mutation) {
80865
- const res = await fetch(
80925
+ const res = await studioApiFetch(
80866
80926
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
80867
80927
  {
80868
80928
  method: "POST",
@@ -80876,7 +80936,7 @@ async function mutateGsapScript(projectId, sourceFile, mutation) {
80876
80936
  return result;
80877
80937
  }
80878
80938
  async function mutateGsapScriptBatch(projectId, sourceFile, mutations) {
80879
- const res = await fetch(
80939
+ const res = await studioApiFetch(
80880
80940
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
80881
80941
  {
80882
80942
  method: "POST",
@@ -81105,7 +81165,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81105
81165
  const { instantPatch: _instantPatch, ...batchOptions } = options;
81106
81166
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, instantPatches.length > 0 ? { ...batchOptions, instantPatches } : batchOptions);
81107
81167
  }, [showToast, finalizeSuccessfulMutation]);
81108
- const commitMutation = useMemo72(() => {
81168
+ const commitMutation = useMemo73(() => {
81109
81169
  const serializeFile = (file, task) => {
81110
81170
  if (writeProjectFile) {
81111
81171
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -81122,7 +81182,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81122
81182
  };
81123
81183
  const commit = trackedStudioEdit((selection, mutation, options) => {
81124
81184
  if (!activeProjectId) return Promise.resolve();
81125
- const file = selection.sourceFile || activeCompPath || "index.html";
81185
+ const file = gsapWriteFile(selection, activeCompPath);
81126
81186
  return serializeCommit(
81127
81187
  file,
81128
81188
  options.serializeKey,
@@ -81131,7 +81191,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81131
81191
  });
81132
81192
  commit.batch = trackedStudioEdit((calls, options) => {
81133
81193
  if (!activeProjectId) return Promise.resolve();
81134
- const file = calls[0]?.selection.sourceFile || activeCompPath || "index.html";
81194
+ const file = calls[0] ? gsapWriteFile(calls[0].selection, activeCompPath) : "index.html";
81135
81195
  return serializeCommit(
81136
81196
  file,
81137
81197
  options.serializeKey,
@@ -81170,7 +81230,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
81170
81230
  },
81171
81231
  [activeProjectId]
81172
81232
  );
81173
- const sdkDeps = useMemo72(
81233
+ const sdkDeps = useMemo73(
81174
81234
  () => writeProjectFile ? {
81175
81235
  editHistory: { recordEdit: editHistory.recordEdit },
81176
81236
  writeProjectFile,
@@ -81363,13 +81423,13 @@ function useGsapAnimationFetchFallback(projectId) {
81363
81423
 
81364
81424
  // src/hooks/useGsapInteractionFailureTelemetry.ts
81365
81425
  import { useCallback as useCallback141 } from "react";
81366
- function failureToast(error) {
81367
- if (wasAlreadyToasted(error)) return null;
81426
+ function failureToast(error, toast) {
81427
+ if (!toast || wasAlreadyToasted(error)) return null;
81368
81428
  return isGsapEditBlockedError(error) ? error.message : "Failed to save animated edit.";
81369
81429
  }
81370
81430
  function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
81371
81431
  return useCallback141(
81372
- (error, selection, mutationType, label) => {
81432
+ (error, selection, mutationType, label, toast) => {
81373
81433
  const report = isGsapEditBlockedError(error) ? trackStudioEditBlocked : trackStudioSaveFailure;
81374
81434
  report({
81375
81435
  source: "gsap_commit",
@@ -81381,7 +81441,7 @@ function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
81381
81441
  targetSelector: selection?.selector,
81382
81442
  targetSourceFile: selection?.sourceFile
81383
81443
  });
81384
- const message = failureToast(error);
81444
+ const message = failureToast(error, toast !== false);
81385
81445
  if (message) showToast(message, "error");
81386
81446
  },
81387
81447
  [activeCompPath, showToast]
@@ -81835,17 +81895,29 @@ function useDomEditWiring(params) {
81835
81895
 
81836
81896
  // src/hooks/useGsapAwareGroupMove.ts
81837
81897
  import { useCallback as useCallback144 } from "react";
81838
- var groupDragCommitCounter = 0;
81839
- function firstPreflightFailure(results, updates) {
81898
+
81899
+ // src/hooks/gsapGroupPreflight.ts
81900
+ function firstPreflightFailure(results, updates, savedOnElement, activeCompPath) {
81840
81901
  for (const [index, result] of results.entries()) {
81841
81902
  if (result.status !== "rejected") continue;
81842
81903
  const selection = updates[index]?.selection;
81843
81904
  if (selection) return { error: result.reason, selection };
81844
81905
  }
81845
- return null;
81906
+ return secondFile(updates, savedOnElement, activeCompPath);
81907
+ }
81908
+ function secondFile(updates, savedOnElement, activeCompPath) {
81909
+ const scripted = updates.filter(({ selection }) => !savedOnElement.has(selection));
81910
+ const fileOf = (s) => gsapWriteFile(s, activeCompPath);
81911
+ const file = scripted[0] && fileOf(scripted[0].selection);
81912
+ const other = scripted.find(({ selection }) => fileOf(selection) !== file);
81913
+ return other ? { error: new GsapEditBlockedError("mixed-files"), selection: other.selection } : null;
81846
81914
  }
81915
+
81916
+ // src/hooks/useGsapAwareGroupMove.ts
81917
+ var groupDragCommitCounter = 0;
81847
81918
  function useGsapAwareGroupMove({
81848
81919
  gsapCommitMutation,
81920
+ activeCompPath,
81849
81921
  previewIframeRef,
81850
81922
  makeFetchFallback,
81851
81923
  trackGsapInteractionFailure,
@@ -81853,15 +81925,16 @@ function useGsapAwareGroupMove({
81853
81925
  showToast
81854
81926
  }) {
81855
81927
  const handleGsapAwareGroupPathOffsetCommit = useCallback144(
81856
- async (updates) => {
81928
+ async (updates, options = {}) => {
81857
81929
  const writes = observeGsapGesture(gsapCommitMutation);
81858
81930
  const writer = writes.commit;
81859
81931
  if (!writer) return { ok: true, changed: false };
81932
+ const toastRefusal = options.refusalToast !== false;
81860
81933
  const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
81861
81934
  let renderOnCommit = false;
81862
81935
  const previewFallbackLatch = { pending: false };
81863
- const withGroupOptions = (options) => ({
81864
- ...options,
81936
+ const withGroupOptions = (options2) => ({
81937
+ ...options2,
81865
81938
  coalesceKey,
81866
81939
  coalesceMs: Number.POSITIVE_INFINITY,
81867
81940
  deferPreviewSync: !renderOnCommit,
@@ -81882,23 +81955,24 @@ function useGsapAwareGroupMove({
81882
81955
  label: "Move animated layer (group)"
81883
81956
  });
81884
81957
  };
81885
- const coalescedCommit = (selection, mutation, options) => {
81886
- queued.push({ selection, mutation, options: withGroupOptions(options) });
81958
+ const coalescedCommit = (selection, mutation, options2) => {
81959
+ queued.push({ selection, mutation, options: withGroupOptions(options2) });
81887
81960
  return Promise.resolve();
81888
81961
  };
81889
81962
  const preflightAnimations = /* @__PURE__ */ new Map();
81890
81963
  const offsetMembers = /* @__PURE__ */ new Map();
81891
81964
  const preflightResults = await Promise.allSettled(
81892
- updates.map(async ({ selection, plainTranslate }) => {
81965
+ updates.map(async ({ selection, next, plainTranslate }) => {
81893
81966
  if (plainTranslate ?? !gsapWritesPosition(selection.element)) {
81894
- refuseGsapTakeover(selection.element, showToast);
81967
+ refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {
81968
+ });
81895
81969
  return void offsetMembers.set(selection, true);
81896
81970
  }
81897
81971
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
81898
81972
  preflightAnimations.set(selection, animations);
81899
81973
  const outcome = await tryGsapDragIntercept(
81900
81974
  selection,
81901
- { x: 0, y: 0 },
81975
+ next,
81902
81976
  animations,
81903
81977
  previewIframeRef.current,
81904
81978
  coalescedCommit,
@@ -81909,13 +81983,19 @@ function useGsapAwareGroupMove({
81909
81983
  assertGsapEditPersisted(outcome);
81910
81984
  })
81911
81985
  );
81912
- const preflightFailure = firstPreflightFailure(preflightResults, updates);
81986
+ const preflightFailure = firstPreflightFailure(
81987
+ preflightResults,
81988
+ updates,
81989
+ offsetMembers,
81990
+ activeCompPath
81991
+ );
81913
81992
  if (preflightFailure) {
81914
81993
  trackGsapInteractionFailure(
81915
81994
  preflightFailure.error,
81916
81995
  preflightFailure.selection,
81917
81996
  "drag",
81918
- "Move animated layer (group)"
81997
+ "Move animated layer (group)",
81998
+ toastRefusal
81919
81999
  );
81920
82000
  throw preflightFailure.error;
81921
82001
  }
@@ -81967,6 +82047,7 @@ function useGsapAwareGroupMove({
81967
82047
  },
81968
82048
  [
81969
82049
  gsapCommitMutation,
82050
+ activeCompPath,
81970
82051
  previewIframeRef,
81971
82052
  makeFetchFallback,
81972
82053
  trackGsapInteractionFailure,
@@ -81980,23 +82061,6 @@ function useGsapAwareGroupMove({
81980
82061
  // src/hooks/useGsapAwareEditing.ts
81981
82062
  import { useCallback as useCallback146 } from "react";
81982
82063
 
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
82064
  // src/hooks/gsapResizePreflight.ts
82001
82065
  function resizeRoute(animations, fetchedAnimations) {
82002
82066
  const allKnownAnimations = [...animations, ...fetchedAnimations];
@@ -82028,16 +82092,8 @@ function preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnim
82028
82092
  }
82029
82093
 
82030
82094
  // src/hooks/gsapResizeIntercept.ts
82031
- var IDENTITY_ONE_PROPS = /* @__PURE__ */ new Set(["opacity", "autoAlpha", "scale", "scaleX", "scaleY"]);
82032
82095
  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
- }
82096
+ var POSITION_XY = /* @__PURE__ */ new Set(["x", "y"]);
82041
82097
  function originalBoxSize(el, measuredAttr, inlineProperty) {
82042
82098
  const measured = Number.parseFloat(el?.getAttribute(measuredAttr) ?? "");
82043
82099
  if (Number.isFinite(measured) && measured > 0) return measured;
@@ -82046,6 +82102,36 @@ function originalBoxSize(el, measuredAttr, inlineProperty) {
82046
82102
  );
82047
82103
  return Number.isFinite(inline) && inline > 0 ? inline : null;
82048
82104
  }
82105
+ function preGestureBoxSize(el) {
82106
+ const width = originalBoxSize(el, STUDIO_ORIGINAL_BOX_WIDTH_ATTR, "width");
82107
+ const height = originalBoxSize(el, STUDIO_ORIGINAL_BOX_HEIGHT_ATTR, "height");
82108
+ return { ...width != null && { width }, ...height != null && { height } };
82109
+ }
82110
+ async function commitSizeAtPlayhead(selection, anim, size, iframe, dragOffset, callbacks) {
82111
+ const selector = selectorFromSelection(selection);
82112
+ const moves = !!dragOffset && (dragOffset.x !== 0 || dragOffset.y !== 0) && !!selector;
82113
+ const anchor2 = moves && animationWritesAnyProperty(anim, POSITION_XY) ? computeDraggedGsapPosition(
82114
+ selection.element,
82115
+ dragOffset,
82116
+ readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 }
82117
+ ) : null;
82118
+ const written = await commitValueAtPlayhead(
82119
+ selection,
82120
+ anim,
82121
+ anchor2 ? { ...size, x: anchor2.newX, y: anchor2.newY } : size,
82122
+ iframe,
82123
+ callbacks,
82124
+ {
82125
+ label: "Resize",
82126
+ backfill: {
82127
+ ...preGestureBoxSize(selection.element),
82128
+ ...anchor2 && { x: anchor2.baseGsapX, y: anchor2.baseGsapY }
82129
+ },
82130
+ ...anchor2 && { beforeReload: () => restoreDragOffset(selection.element) }
82131
+ }
82132
+ );
82133
+ return written.status === "persisted" && anchor2 ? { ...written, ownsDragOffset: true } : written;
82134
+ }
82049
82135
  function tweenUsesScaleLonghands(anim) {
82050
82136
  const isLonghand = (name) => name === "scaleX" || name === "scaleY";
82051
82137
  const inKeyframes = (anim?.keyframes?.keyframes ?? []).some(
@@ -82053,7 +82139,7 @@ function tweenUsesScaleLonghands(anim) {
82053
82139
  );
82054
82140
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
82055
82141
  }
82056
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations) {
82142
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset) {
82057
82143
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
82058
82144
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
82059
82145
  if (outcome.status === "blocked") return outcome;
@@ -82069,7 +82155,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82069
82155
  commitMutation,
82070
82156
  postSplitFetch
82071
82157
  );
82072
- let anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, resizeProperties) ? resolved2.anim : null;
82158
+ const anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, resizeProperties) ? resolved2.anim : null;
82073
82159
  logResize("intercept-enter", {
82074
82160
  hasScaleGroup: resizeGroup === "scale",
82075
82161
  resizeGroup,
@@ -82093,15 +82179,11 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82093
82179
  );
82094
82180
  if (animatedTween) {
82095
82181
  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" };
82182
+ const sized = { width: roundToLayoutPx(size.width), height: roundToLayoutPx(size.height) };
82183
+ return commitSizeAtPlayhead(selection, animatedTween, sized, iframe, dragOffset, {
82184
+ commitMutation,
82185
+ fetchAnimations: fetchFallbackAnimations
82186
+ });
82105
82187
  }
82106
82188
  }
82107
82189
  logResize("intercept-route", { route: "static-size-set", hadSizeSet: !!sizeSet, resizeGroup });
@@ -82117,10 +82199,9 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82117
82199
  }
82118
82200
  const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
82119
82201
  const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
82120
- if (activeKeyframePct != null) setActiveKeyframePct(null);
82121
82202
  const selector = selectorFromSelection(selection);
82122
- const runtimeProps = selector ? readAllAnimatedProperties(iframe, selector, anim, resizeGroup) : {};
82123
82203
  let resizeProps;
82204
+ let resizeBackfill;
82124
82205
  let scaleDraftEl = null;
82125
82206
  let scaleDraftDropPoint = null;
82126
82207
  let committedScale = null;
@@ -82140,6 +82221,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82140
82221
  nonUniformScale = uniformDrift > 0.5;
82141
82222
  useScaleLonghands = nonUniformScale || tweenUsesScaleLonghands(anim);
82142
82223
  resizeProps = useScaleLonghands ? { scaleX: newScaleX, scaleY: newScaleY } : { scale: newScaleX };
82224
+ resizeBackfill = { scaleX: liveScaleX, scaleY: liveScaleY };
82143
82225
  logResize("intercept-route", {
82144
82226
  route: "scale-tween",
82145
82227
  cssW,
@@ -82161,6 +82243,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82161
82243
  width: roundToLayoutPx(size.width),
82162
82244
  height: roundToLayoutPx(size.height)
82163
82245
  };
82246
+ resizeBackfill = preGestureBoxSize(selection.element);
82164
82247
  }
82165
82248
  const finalizeScaleResizeCommit = async () => {
82166
82249
  if (!scaleDraftEl) return false;
@@ -82206,10 +82289,12 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82206
82289
  );
82207
82290
  if (positionTween) {
82208
82291
  logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
82209
- await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, selector, {
82210
- commitMutation,
82211
- fetchAnimations: fetchFallbackAnimations
82212
- });
82292
+ assertGsapEditPersisted(
82293
+ await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, {
82294
+ commitMutation,
82295
+ fetchAnimations: fetchFallbackAnimations
82296
+ })
82297
+ );
82213
82298
  return true;
82214
82299
  }
82215
82300
  const existingSet = findExistingPositionWrite(currentAnimations, selector, selection.element);
@@ -82232,102 +82317,14 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
82232
82317
  );
82233
82318
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
82234
82319
  }
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
- );
82320
+ const callbacks = { commitMutation, fetchAnimations: fetchFallbackAnimations };
82321
+ if (resizeGroup === "size")
82322
+ return commitSizeAtPlayhead(selection, anim, resizeProps, iframe, dragOffset, callbacks);
82323
+ const written = await commitValueAtPlayhead(selection, anim, resizeProps, iframe, callbacks, {
82324
+ label: "Resize",
82325
+ backfill: resizeBackfill
82326
+ });
82327
+ if (written.status !== "persisted") return written;
82331
82328
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
82332
82329
  }
82333
82330
 
@@ -82690,6 +82687,7 @@ function useGsapAwareEditing({
82690
82687
  domEditSelection,
82691
82688
  selectedGsapAnimations,
82692
82689
  gsapCommitMutation,
82690
+ activeCompPath,
82693
82691
  previewIframeRef,
82694
82692
  showToast,
82695
82693
  bumpGsapCache,
@@ -82753,6 +82751,7 @@ function useGsapAwareEditing({
82753
82751
  );
82754
82752
  const handleGsapAwareGroupPathOffsetCommit = useGsapAwareGroupMove({
82755
82753
  gsapCommitMutation,
82754
+ activeCompPath,
82756
82755
  previewIframeRef,
82757
82756
  makeFetchFallback,
82758
82757
  trackGsapInteractionFailure,
@@ -82760,10 +82759,10 @@ function useGsapAwareEditing({
82760
82759
  showToast
82761
82760
  });
82762
82761
  const handleGsapAwareBoxSizeCommit = useCallback146(
82763
- async (selection, next, offset, restore = () => void 0) => {
82762
+ async (selection, next, offset, restore2 = () => void 0) => {
82764
82763
  const writes = observeGsapGesture(gsapCommitMutation);
82765
82764
  if (!gsapWritesBox(selection.element)) {
82766
- const result = await handleDomBoxSizeCommit(selection, next, offset, restore);
82765
+ const result = await handleDomBoxSizeCommit(selection, next, offset, restore2);
82767
82766
  return writes.finish(result?.changed === true);
82768
82767
  }
82769
82768
  let targetAnimations;
@@ -82771,7 +82770,7 @@ function useGsapAwareEditing({
82771
82770
  const ownedAnimations = getGsapAnimationsForSelection(selection);
82772
82771
  targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
82773
82772
  } catch (error) {
82774
- restore();
82773
+ restore2();
82775
82774
  trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
82776
82775
  throw error;
82777
82776
  }
@@ -82832,7 +82831,8 @@ function useGsapAwareEditing({
82832
82831
  targetAnimations,
82833
82832
  previewIframeRef.current,
82834
82833
  commitMutation2,
82835
- makeFetchFallback(selection)
82834
+ makeFetchFallback(selection),
82835
+ offset
82836
82836
  );
82837
82837
  assertGsapEditPersisted(outcome);
82838
82838
  if (outcome.status === "element-size") {
@@ -82876,7 +82876,7 @@ function useGsapAwareEditing({
82876
82876
  },
82877
82877
  restore: () => {
82878
82878
  anchorMove?.rollback();
82879
- restore();
82879
+ restore2();
82880
82880
  },
82881
82881
  skipPixelAssert: hasLivePositionTween
82882
82882
  });
@@ -83089,7 +83089,7 @@ function reportSelectionPublishError(error) {
83089
83089
  console.warn("[Studio] Failed to update agent selection context", error);
83090
83090
  }
83091
83091
  function putSelection(projectId, selection, signal) {
83092
- return fetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
83092
+ return studioApiFetch(`/api/projects/${encodeURIComponent(projectId)}/selection`, {
83093
83093
  method: "PUT",
83094
83094
  headers: { "Content-Type": "application/json" },
83095
83095
  body: JSON.stringify({ selection }),
@@ -83205,7 +83205,7 @@ function useKeyframeEaseCommits({
83205
83205
  }
83206
83206
 
83207
83207
  // src/hooks/useCommitPreflightCapabilities.ts
83208
- import { useEffect as useEffect122, useMemo as useMemo73, useRef as useRef153, useState as useState119 } from "react";
83208
+ import { useEffect as useEffect122, useMemo as useMemo74, useRef as useRef153, useState as useState119 } from "react";
83209
83209
  function runCommitPreflights(selection, fileAnimations, iframe, group) {
83210
83210
  const target = { id: selection.id ?? null, selector: selection.selector ?? null };
83211
83211
  const animations = getAnimationsForElement(fileAnimations, target, selection.element);
@@ -83274,7 +83274,7 @@ function useCommitPreflightCapabilities({
83274
83274
  });
83275
83275
  }
83276
83276
  }, [enabled, projectId, selection, groupSelections, version]);
83277
- return useMemo73(() => {
83277
+ return useMemo74(() => {
83278
83278
  void parseTick;
83279
83279
  if (!enabled || !projectId) return { selection, groupSelections };
83280
83280
  const group = groupSelections.length > 1;
@@ -83322,6 +83322,70 @@ function useLivePreviewIframe2(host) {
83322
83322
  return live;
83323
83323
  }
83324
83324
 
83325
+ // src/components/editor/domGroupMoveBy.ts
83326
+ function toScreen(doc, delta) {
83327
+ const origin2 = toTopScreenPoint(doc, { x: 0, y: 0 });
83328
+ const moved = toTopScreenPoint(doc, delta);
83329
+ if (!origin2 || !moved) throw new Error("The preview has no measurable size.");
83330
+ return { x: moved.x - origin2.x, y: moved.y - origin2.y };
83331
+ }
83332
+ function memberFor(selection, scale) {
83333
+ const { element } = selection;
83334
+ const box = element.getBoundingClientRect();
83335
+ const result = createManualOffsetDragMember({
83336
+ key: selection.id ?? selection.selector ?? selection.label,
83337
+ selection,
83338
+ element,
83339
+ rect: {
83340
+ left: box.left,
83341
+ top: box.top,
83342
+ width: box.width,
83343
+ height: box.height,
83344
+ editScaleX: scale.x,
83345
+ editScaleY: scale.y
83346
+ },
83347
+ gesture: "drag"
83348
+ });
83349
+ if (!result.ok) throw new Error(result.reason);
83350
+ return result.member;
83351
+ }
83352
+ async function moveDomGroupBy(moves, commitGroup) {
83353
+ const moving = moves.filter(({ delta }) => delta.x !== 0 || delta.y !== 0);
83354
+ if (moving.length === 0) return;
83355
+ const selections = moving.map(({ selection }) => selection);
83356
+ if (!canCanvasNudgeTargets(selections)) {
83357
+ const blocked = selections.find((s) => !s.capabilities.canApplyManualOffset);
83358
+ throw new Error(blocked?.capabilities.reasonIfDisabled ?? `${blocked?.label} can't be moved.`);
83359
+ }
83360
+ const screens = moving.map(
83361
+ ({ selection, delta }) => toScreen(selection.element.ownerDocument, delta)
83362
+ );
83363
+ const members = [];
83364
+ try {
83365
+ for (const selection of selections)
83366
+ members.push(memberFor(selection, toScreen(selection.element.ownerDocument, { x: 1, y: 1 })));
83367
+ } catch (error) {
83368
+ restoreManualOffsetDragMembers(members);
83369
+ throw error;
83370
+ }
83371
+ const updates = members.map((member, index) => ({
83372
+ selection: member.selection,
83373
+ next: applyManualOffsetDragCommit(member, screens[index].x, screens[index].y),
83374
+ plainTranslate: member.plainTranslate
83375
+ }));
83376
+ try {
83377
+ await countStudioManualEditSave(members[0].element, () => commitGroup(updates));
83378
+ } catch (error) {
83379
+ for (const member of members) {
83380
+ if (isStudioManualEditGestureCurrent(member.element, member.gestureToken))
83381
+ restoreStudioPathOffset(member.element, member.initialPathOffset);
83382
+ }
83383
+ throw error;
83384
+ } finally {
83385
+ endManualOffsetDragMembers(members);
83386
+ }
83387
+ }
83388
+
83325
83389
  // src/hooks/useDomEditSession.ts
83326
83390
  function useDomEditSession({
83327
83391
  projectId,
@@ -83674,6 +83738,7 @@ function useDomEditSession({
83674
83738
  domEditSelection,
83675
83739
  selectedGsapAnimations,
83676
83740
  gsapCommitMutation,
83741
+ activeCompPath,
83677
83742
  previewIframeRef,
83678
83743
  showToast,
83679
83744
  bumpGsapCache,
@@ -83688,6 +83753,13 @@ function useDomEditSession({
83688
83753
  setArcPath,
83689
83754
  updateArcSegment
83690
83755
  });
83756
+ const handleDomGroupMoveBy = useCallback148(
83757
+ (moves) => moveDomGroupBy(
83758
+ moves,
83759
+ (updates) => handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false })
83760
+ ),
83761
+ [handleGsapAwareGroupPathOffsetCommit]
83762
+ );
83691
83763
  const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } = useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
83692
83764
  const committable = useCommitPreflightCapabilities({
83693
83765
  projectId,
@@ -83725,6 +83797,7 @@ function useDomEditSession({
83725
83797
  handleDomAttributeBatchCommit,
83726
83798
  handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
83727
83799
  handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
83800
+ handleDomGroupMoveBy,
83728
83801
  handleDomZIndexReorderCommit,
83729
83802
  handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
83730
83803
  handleDomRotationCommit: handleGsapAwareRotationCommit,
@@ -83873,7 +83946,7 @@ async function readProjectFileOptional(projectId, path) {
83873
83946
  let res;
83874
83947
  const fetchStarted = performance.now();
83875
83948
  try {
83876
- res = await fetch(
83949
+ res = await studioApiFetch(
83877
83950
  `/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`
83878
83951
  );
83879
83952
  } catch {
@@ -84458,8 +84531,10 @@ function useExternalFileChangeCoordinator({
84458
84531
  },
84459
84532
  [projectId, pendingTimelineEditPathRef, startDrainLoop, onAcceptedPersistedFileChange]
84460
84533
  );
84534
+ const processChangeRef = useRef156(processChange);
84535
+ processChangeRef.current = processChange;
84461
84536
  useEffect127(() => {
84462
- const handler = (delivery) => processChange(decodeFileChange(delivery));
84537
+ const handler = (delivery) => processChangeRef.current(decodeFileChange(delivery));
84463
84538
  const adapter = testHotAdapter();
84464
84539
  if (adapter) {
84465
84540
  adapter.on("hf:file-change", handler);
@@ -84470,7 +84545,7 @@ function useExternalFileChangeCoordinator({
84470
84545
  return () => import.meta.hot?.off?.("hf:file-change", handler);
84471
84546
  }
84472
84547
  return sseFileChangeChannel(handler);
84473
- }, [processChange]);
84548
+ }, []);
84474
84549
  const retry = useCallback150(async () => {
84475
84550
  const current = blockedRef.current;
84476
84551
  if (!current || current.status === "conflict" || current.recovered) return;
@@ -84617,7 +84692,7 @@ function useStudioExternalFileChanges({
84617
84692
  }
84618
84693
 
84619
84694
  // src/hooks/useBlockHandlers.ts
84620
- import { useCallback as useCallback152, useMemo as useMemo74, useRef as useRef157, useState as useState123 } from "react";
84695
+ import { useCallback as useCallback152, useMemo as useMemo75, useRef as useRef157, useState as useState123 } from "react";
84621
84696
 
84622
84697
  // src/utils/blockInstaller.ts
84623
84698
  function getMaxZIndexFromIframe(iframe) {
@@ -84646,7 +84721,7 @@ async function installRegistryItem({
84646
84721
  blockName,
84647
84722
  showToast
84648
84723
  }) {
84649
- const response = await fetch(buildProjectApiPath(projectId, `/registry/install`), {
84724
+ const response = await studioApiFetch(buildProjectApiPath(projectId, `/registry/install`), {
84650
84725
  method: "POST",
84651
84726
  headers: { "Content-Type": "application/json" },
84652
84727
  body: JSON.stringify({ blockName })
@@ -84811,7 +84886,7 @@ function useBlockHandlers({
84811
84886
  setRightPanelTab
84812
84887
  }) {
84813
84888
  const [activeBlockParams, setActiveBlockParams] = useState123(null);
84814
- const blockCtx = useMemo74(
84889
+ const blockCtx = useMemo75(
84815
84890
  () => ({
84816
84891
  activeCompPath: blockCtxDeps.activeCompPath,
84817
84892
  timelineElements: blockCtxDeps.timelineElements,
@@ -85178,7 +85253,7 @@ function dispatchPlainKey(event, key2, cb) {
85178
85253
  }
85179
85254
 
85180
85255
  // src/hooks/useEditHistoryActions.ts
85181
- import { useCallback as useCallback153, useMemo as useMemo75 } from "react";
85256
+ import { useCallback as useCallback153, useMemo as useMemo76 } from "react";
85182
85257
  function useEditHistoryActions({
85183
85258
  editHistory,
85184
85259
  readOptionalProjectFile,
@@ -85205,20 +85280,25 @@ function useEditHistoryActions({
85205
85280
  const noun = direction === "undo" ? "Undo" : "Redo";
85206
85281
  const predicted = editHistory.predict?.(direction) ?? null;
85207
85282
  const predictedShown = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
85208
- const pendingEditShown = !predictedShown && direction === "undo" ? revertNewestStudioPendingEdit() : null;
85209
- const putBack = predictedShown ?? pendingEditShown;
85283
+ const pendingEditShown = !predictedShown && direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
85284
+ const putBack = predictedShown ?? pendingEditShown?.showAgain;
85285
+ const claimedAfter = direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : void 0;
85210
85286
  let result = { ok: false, reason: "failed" };
85211
85287
  let serverSteppedShown = false;
85288
+ let revertIsTheUndo = false;
85212
85289
  try {
85213
85290
  await waitForPendingDomEditSaves();
85291
+ revertIsTheUndo = Boolean(pendingEditShown && !await pendingEditShown.landed());
85292
+ if (revertIsTheUndo) return;
85214
85293
  result = await editHistory[direction]({
85215
85294
  readFile: readHistoryFile,
85216
- serialize: serializeHistoryFiles
85295
+ serialize: serializeHistoryFiles,
85296
+ claimedAfter
85217
85297
  });
85218
85298
  const stepped = Boolean(result.ok && result.label);
85219
85299
  serverSteppedShown = predictedShown ? stepped && result.undoes === predicted?.id : stepped && Boolean(pendingEditShown);
85220
85300
  } finally {
85221
- if (putBack && !serverSteppedShown) putBack();
85301
+ if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
85222
85302
  }
85223
85303
  if (!result.ok && result.reason === "content-mismatch") {
85224
85304
  showToast(
@@ -85233,12 +85313,12 @@ function useEditHistoryActions({
85233
85313
  }
85234
85314
  if (result.ok && result.label) {
85235
85315
  const files = serverSteppedShown && predictedShown ? fromShown(result.files, predicted.files) : result.files;
85236
- const restore = { paths: result.paths, files };
85237
- onAfterUndoRedo?.(restore);
85316
+ const restore2 = { paths: result.paths, files };
85317
+ onAfterUndoRedo?.(restore2);
85238
85318
  if (activeCompPath && result.paths?.includes(activeCompPath)) {
85239
85319
  forceReloadSdkSession?.();
85240
85320
  }
85241
- await syncHistoryPreviewAfterApply(restore);
85321
+ await syncHistoryPreviewAfterApply(restore2);
85242
85322
  showToast(result.label, "info");
85243
85323
  }
85244
85324
  },
@@ -85257,7 +85337,7 @@ function useEditHistoryActions({
85257
85337
  );
85258
85338
  const undo = useCallback153(() => apply("undo"), [apply]);
85259
85339
  const redo = useCallback153(() => apply("redo"), [apply]);
85260
- return useMemo75(() => ({ undo, redo }), [undo, redo]);
85340
+ return useMemo76(() => ({ undo, redo }), [undo, redo]);
85261
85341
  }
85262
85342
  function fromShown(files, shown2) {
85263
85343
  if (!files) return files;
@@ -86252,7 +86332,7 @@ function useCaptionDetection({
86252
86332
  if (!captionSrcPath) return;
86253
86333
  activating = true;
86254
86334
  const srcPath = captionSrcPath;
86255
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`)).then((r) => r.json()).then((data) => {
86335
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`)).then((r) => r.json()).then((data) => {
86256
86336
  if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
86257
86337
  const root = doc.querySelector("[data-composition-id]");
86258
86338
  const w = parseInt(root?.getAttribute("data-width") ?? "1920", 10);
@@ -86365,7 +86445,7 @@ function useConsoleErrorCapture(previewIframe) {
86365
86445
  }
86366
86446
 
86367
86447
  // src/hooks/useFrameCapture.ts
86368
- import { useMemo as useMemo76, useState as useState125, useCallback as useCallback157, useRef as useRef162 } from "react";
86448
+ import { useMemo as useMemo77, useState as useState125, useCallback as useCallback157, useRef as useRef162 } from "react";
86369
86449
  function useFrameCapture({
86370
86450
  projectId,
86371
86451
  activeCompPath,
@@ -86417,7 +86497,10 @@ function useFrameCapture({
86417
86497
  const controller = new AbortController();
86418
86498
  const timeout = setTimeout(() => controller.abort(), 3e4);
86419
86499
  try {
86420
- const response = await fetch(href, { cache: "no-store", signal: controller.signal });
86500
+ const response = await studioApiFetch(href, {
86501
+ cache: "no-store",
86502
+ signal: controller.signal
86503
+ });
86421
86504
  clearTimeout(timeout);
86422
86505
  if (!response.ok) {
86423
86506
  let msg = `Capture failed (${response.status})`;
@@ -86453,7 +86536,7 @@ function useFrameCapture({
86453
86536
  },
86454
86537
  [activeCompPath, playheadTime, projectId, showToast, waitForPendingDomEditSaves]
86455
86538
  );
86456
- const captureFrameHref = useMemo76(
86539
+ const captureFrameHref = useMemo77(
86457
86540
  () => projectId ? buildFrameCaptureUrl({
86458
86541
  projectId,
86459
86542
  compositionPath: activeCompPath,
@@ -86473,7 +86556,7 @@ function useFrameCapture({
86473
86556
  }
86474
86557
 
86475
86558
  // src/hooks/useLintModal.ts
86476
- import { useState as useState126, useCallback as useCallback158, useEffect as useEffect131, useRef as useRef163, useMemo as useMemo77 } from "react";
86559
+ import { useState as useState126, useCallback as useCallback158, useEffect as useEffect131, useRef as useRef163, useMemo as useMemo78 } from "react";
86477
86560
  function parseFinding(f) {
86478
86561
  return {
86479
86562
  severity: f.severity === "error" ? "error" : "warning",
@@ -86484,7 +86567,7 @@ function parseFinding(f) {
86484
86567
  };
86485
86568
  }
86486
86569
  async function fetchLintFindings(projectId) {
86487
- const res = await fetch(buildProjectApiPath(projectId, `/lint`));
86570
+ const res = await studioApiFetch(buildProjectApiPath(projectId, `/lint`));
86488
86571
  const data = await res.json();
86489
86572
  return (data.findings ?? []).map(parseFinding);
86490
86573
  }
@@ -86546,8 +86629,8 @@ function useLintModal(projectId, refreshKey) {
86546
86629
  },
86547
86630
  [backgroundFindings]
86548
86631
  );
86549
- const findingsByElement = useMemo77(() => groupFindings((f) => f.elementId), [groupFindings]);
86550
- const findingsByFile = useMemo77(() => groupFindings((f) => f.file), [groupFindings]);
86632
+ const findingsByElement = useMemo78(() => groupFindings((f) => f.elementId), [groupFindings]);
86633
+ const findingsByFile = useMemo78(() => groupFindings((f) => f.file), [groupFindings]);
86551
86634
  const badgeFindings = lintModal ?? backgroundFindings;
86552
86635
  const lintFindingCount = badgeFindings.length;
86553
86636
  const hasLintError = badgeFindings.some((f) => f.severity === "error");
@@ -87012,16 +87095,16 @@ function useStudioUrlState({
87012
87095
  }
87013
87096
 
87014
87097
  // src/hooks/useEffectiveTimelineDuration.ts
87015
- import { useMemo as useMemo78 } from "react";
87098
+ import { useMemo as useMemo79 } from "react";
87016
87099
  function useEffectiveTimelineDuration(timelineDuration2, timelineElements) {
87017
- return useMemo78(
87100
+ return useMemo79(
87018
87101
  () => getEffectiveTimelineDuration(timelineDuration2, timelineElements),
87019
87102
  [timelineDuration2, timelineElements]
87020
87103
  );
87021
87104
  }
87022
87105
 
87023
87106
  // src/hooks/useStudioContextValue.ts
87024
- import { useCallback as useCallback161, useMemo as useMemo79 } from "react";
87107
+ import { useCallback as useCallback161, useMemo as useMemo80 } from "react";
87025
87108
  function buildStudioContextValue(input) {
87026
87109
  return {
87027
87110
  projectId: input.projectId,
@@ -87049,7 +87132,7 @@ function buildStudioContextValue(input) {
87049
87132
  };
87050
87133
  }
87051
87134
  function useInspectorState(rightPanel, isPlaying, domEditSelection, isGestureRecording) {
87052
- return useMemo79(() => {
87135
+ return useMemo80(() => {
87053
87136
  const inspectorPanelActive = rightPanel === "layers" || rightPanel === "design";
87054
87137
  return {
87055
87138
  inspectorPanelActive,
@@ -87095,7 +87178,7 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
87095
87178
  import { memo as memo44, useContext as useContext15 } from "react";
87096
87179
 
87097
87180
  // src/contexts/PanelLayoutContext.tsx
87098
- import { useContext as useContext14, useMemo as useMemo80 } from "react";
87181
+ import { useContext as useContext14, useMemo as useMemo81 } from "react";
87099
87182
  import { jsx as jsx199 } from "react/jsx-runtime";
87100
87183
  var PanelLayoutContext = createStableContext("PanelLayoutContext", null);
87101
87184
  function usePanelLayoutContext() {
@@ -87107,7 +87190,7 @@ function PanelLayoutProvider({
87107
87190
  value: { rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab },
87108
87191
  children
87109
87192
  }) {
87110
- const stable = useMemo80(
87193
+ const stable = useMemo81(
87111
87194
  () => ({ rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab }),
87112
87195
  [rightCollapsed, setRightCollapsed, rightPanelTab, setRightPanelTab]
87113
87196
  );
@@ -88179,7 +88262,7 @@ function useGestureCommit({
88179
88262
  import { useLayoutEffect as useLayoutEffect13, useRef as useRef168 } from "react";
88180
88263
 
88181
88264
  // src/components/editor/GestureTrailOverlay.tsx
88182
- import { memo as memo45, useMemo as useMemo81 } from "react";
88265
+ import { memo as memo45, useMemo as useMemo82 } from "react";
88183
88266
  import { Fragment as Fragment57, jsx as jsx203, jsxs as jsxs162 } from "react/jsx-runtime";
88184
88267
  var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88185
88268
  samples,
@@ -88191,7 +88274,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88191
88274
  mode,
88192
88275
  accentColor = "#3CE6AC"
88193
88276
  }) {
88194
- const trailPoints = useMemo81(() => {
88277
+ const trailPoints = useMemo82(() => {
88195
88278
  if (!canvasRect) return "";
88196
88279
  if (trail2 && trail2.length > 1) {
88197
88280
  return trail2.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -88199,7 +88282,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88199
88282
  if (samples.length === 0) return "";
88200
88283
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
88201
88284
  }, [samples, trail2, sampleCount, canvasRect?.left, canvasRect?.top]);
88202
- const simplifiedPath = useMemo81(() => {
88285
+ const simplifiedPath = useMemo82(() => {
88203
88286
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
88204
88287
  const pts = [];
88205
88288
  for (const [pct, props] of simplifiedPoints) {
@@ -88211,7 +88294,7 @@ var GestureTrailOverlay = memo45(function GestureTrailOverlay2({
88211
88294
  if (pts.length === 0) return "";
88212
88295
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
88213
88296
  }, [simplifiedPoints]);
88214
- const diamondPositions = useMemo81(() => {
88297
+ const diamondPositions = useMemo82(() => {
88215
88298
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
88216
88299
  const pts = [];
88217
88300
  for (const [pct, props] of simplifiedPoints) {
@@ -88319,7 +88402,7 @@ function useGestureTrailOverlay({
88319
88402
  }
88320
88403
 
88321
88404
  // src/hooks/useStudioSidePanels.tsx
88322
- import { useMemo as useMemo87 } from "react";
88405
+ import { useMemo as useMemo88 } from "react";
88323
88406
 
88324
88407
  // src/components/StudioLeftPanels.tsx
88325
88408
  import { memo as memo49, useCallback as useCallback171 } from "react";
@@ -88441,7 +88524,7 @@ function MediaPreview({ projectId, filePath }) {
88441
88524
  }
88442
88525
 
88443
88526
  // 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";
88527
+ import { memo as memo46, useState as useState137, useCallback as useCallback167, useRef as useRef172, useMemo as useMemo84, useEffect as useEffect140 } from "react";
88445
88528
 
88446
88529
  // src/components/ui/SearchInput.tsx
88447
88530
  import { jsx as jsx206, jsxs as jsxs164 } from "react/jsx-runtime";
@@ -89053,7 +89136,7 @@ function AudioRow({
89053
89136
  }
89054
89137
 
89055
89138
  // src/components/sidebar/GlobalAssetsView.tsx
89056
- import { useEffect as useEffect137, useMemo as useMemo82, useState as useState134 } from "react";
89139
+ import { useEffect as useEffect137, useMemo as useMemo83, useState as useState134 } from "react";
89057
89140
  import { jsx as jsx209, jsxs as jsxs167 } from "react/jsx-runtime";
89058
89141
  function globalAssetRows(records, query = "") {
89059
89142
  const q = query.trim().toLowerCase();
@@ -89071,7 +89154,7 @@ function GlobalAssetsView({ searchQuery }) {
89071
89154
  const [records, setRecords] = useState134(null);
89072
89155
  useEffect137(() => {
89073
89156
  let cancelled = false;
89074
- fetch("/api/assets/global").then((r) => r.ok ? r.json() : { assets: [] }).then((d) => {
89157
+ studioApiFetch("/api/assets/global").then((r) => r.ok ? r.json() : { assets: [] }).then((d) => {
89075
89158
  if (!cancelled) setRecords(Array.isArray(d.assets) ? d.assets : []);
89076
89159
  }).catch(() => {
89077
89160
  if (!cancelled) setRecords([]);
@@ -89080,7 +89163,7 @@ function GlobalAssetsView({ searchQuery }) {
89080
89163
  cancelled = true;
89081
89164
  };
89082
89165
  }, []);
89083
- const rows = useMemo82(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
89166
+ const rows = useMemo83(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
89084
89167
  if (records === null) {
89085
89168
  return /* @__PURE__ */ jsx209("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
89086
89169
  }
@@ -89592,7 +89675,7 @@ var AssetsTab = memo46(function AssetsTab2({
89592
89675
  useEffect140(() => {
89593
89676
  if (manifest404Ref.current.has(projectId)) return;
89594
89677
  let cancelled = false;
89595
- fetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`)).then((r) => {
89678
+ studioApiFetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`)).then((r) => {
89596
89679
  if (!r.ok) {
89597
89680
  manifest404Ref.current.add(projectId);
89598
89681
  return "";
@@ -89642,12 +89725,12 @@ var AssetsTab = memo46(function AssetsTab2({
89642
89725
  setTimeout(() => setCopyFeedback(null), copied ? 1500 : 3e3);
89643
89726
  }, []);
89644
89727
  const elements = usePlayerStore((s) => s.elements);
89645
- const usedPaths = useMemo83(() => deriveUsedPaths(elements), [elements]);
89646
- const allMediaAssets = useMemo83(
89728
+ const usedPaths = useMemo84(() => deriveUsedPaths(elements), [elements]);
89729
+ const allMediaAssets = useMemo84(
89647
89730
  () => assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
89648
89731
  [assets]
89649
89732
  );
89650
- const mediaAssets = useMemo83(() => {
89733
+ const mediaAssets = useMemo84(() => {
89651
89734
  const all = filterByUsage(allMediaAssets, usedPaths, usageFilter);
89652
89735
  if (!searchQuery) return all;
89653
89736
  const q = searchQuery.toLowerCase();
@@ -89657,7 +89740,7 @@ var AssetsTab = memo46(function AssetsTab2({
89657
89740
  return rec?.description?.toLowerCase().includes(q);
89658
89741
  });
89659
89742
  }, [allMediaAssets, searchQuery, manifest, usageFilter, usedPaths]);
89660
- const categorized = useMemo83(() => {
89743
+ const categorized = useMemo84(() => {
89661
89744
  const groups = { audio: [], images: [], video: [], fonts: [] };
89662
89745
  for (const a of mediaAssets) {
89663
89746
  const cat = getCategory(a);
@@ -89672,12 +89755,12 @@ var AssetsTab = memo46(function AssetsTab2({
89672
89755
  }
89673
89756
  return groups;
89674
89757
  }, [mediaAssets, usedPaths]);
89675
- const counts = useMemo83(() => {
89758
+ const counts = useMemo84(() => {
89676
89759
  const c = { all: mediaAssets.length };
89677
89760
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
89678
89761
  return c;
89679
89762
  }, [mediaAssets, categorized]);
89680
- const usageCounts = useMemo83(
89763
+ const usageCounts = useMemo84(
89681
89764
  () => countUsage(
89682
89765
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
89683
89766
  usedPaths
@@ -90337,7 +90420,7 @@ function BlockCard({
90337
90420
  }
90338
90421
 
90339
90422
  // 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";
90423
+ 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
90424
  import { jsx as jsx215, jsxs as jsxs172 } from "react/jsx-runtime";
90342
90425
  var DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
90343
90426
  var CARD_W = 80;
@@ -90479,7 +90562,7 @@ function CompCard({
90479
90562
  const thumbnailUrl2 = compositionCardThumbnailUrl(projectId, comp, contentRevision);
90480
90563
  const [thumbnailBox, nearViewport] = useNearViewport();
90481
90564
  const timelineSessionEpoch = usePlayerStore((state) => state.timelineSessionEpoch);
90482
- const thumbnailRequest = useMemo84(
90565
+ const thumbnailRequest = useMemo85(
90483
90566
  () => previewBooted && nearViewport ? compositionThumbnailRequest(thumbnailUrl2, projectId, {
90484
90567
  sessionEpoch: timelineSessionEpoch,
90485
90568
  rich: true
@@ -94102,7 +94185,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
94102
94185
  }
94103
94186
 
94104
94187
  // src/components/panels/VariablesPanel.tsx
94105
- import { memo as memo58, useCallback as useCallback183, useEffect as useEffect151, useMemo as useMemo85, useState as useState154 } from "react";
94188
+ import { memo as memo58, useCallback as useCallback183, useEffect as useEffect151, useMemo as useMemo86, useState as useState154 } from "react";
94106
94189
 
94107
94190
  // src/hooks/useVariablesPersist.ts
94108
94191
  import { useCallback as useCallback180 } from "react";
@@ -94798,22 +94881,22 @@ var VariablesPanel = memo58(function VariablesPanel2({
94798
94881
  reloadPreview,
94799
94882
  publishSdkSession
94800
94883
  });
94801
- const declarations = useMemo85(
94884
+ const declarations = useMemo86(
94802
94885
  () => sdkSession?.getVariableDeclarations() ?? [],
94803
94886
  // eslint-disable-next-line react-hooks/exhaustive-deps
94804
94887
  [sdkSession, refreshKey, revision]
94805
94888
  );
94806
- const usage = useMemo85(
94889
+ const usage = useMemo86(
94807
94890
  () => sdkSession?.getVariableUsage() ?? null,
94808
94891
  // eslint-disable-next-line react-hooks/exhaustive-deps
94809
94892
  [sdkSession, refreshKey, revision]
94810
94893
  );
94811
- const issues = useMemo85(
94894
+ const issues = useMemo86(
94812
94895
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
94813
94896
  // eslint-disable-next-line react-hooks/exhaustive-deps
94814
94897
  [sdkSession, previewValues, refreshKey, revision]
94815
94898
  );
94816
- const effectiveValues = useMemo85(
94899
+ const effectiveValues = useMemo86(
94817
94900
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
94818
94901
  // eslint-disable-next-line react-hooks/exhaustive-deps
94819
94902
  [sdkSession, previewValues, refreshKey, revision]
@@ -95128,15 +95211,15 @@ function useSlideshowPersist({
95128
95211
  }
95129
95212
 
95130
95213
  // src/hooks/useSlideshowTabState.ts
95131
- import { useMemo as useMemo86 } from "react";
95214
+ import { useMemo as useMemo87 } from "react";
95132
95215
  import { SLIDESHOW_ISLAND_TYPE as SLIDESHOW_ISLAND_TYPE2, slideshowIslandRegex as slideshowIslandRegex2 } from "@hyperframes/core/slideshow";
95133
95216
  function useSlideshowTabState(params) {
95134
95217
  const { editingFileContent, previewIframeRef, refreshKey, slideshowVisible } = params;
95135
- const isSlideshowComposition = useMemo86(() => {
95218
+ const isSlideshowComposition = useMemo87(() => {
95136
95219
  if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE2)) return false;
95137
95220
  return slideshowIslandRegex2("i").test(editingFileContent);
95138
95221
  }, [editingFileContent]);
95139
- const slideshowScenes = useMemo86(() => {
95222
+ const slideshowScenes = useMemo87(() => {
95140
95223
  try {
95141
95224
  const win = previewIframeRef.current?.contentWindow;
95142
95225
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -95313,7 +95396,7 @@ function useRemoveBackground(projectId, refreshFileTree, showToast) {
95313
95396
  return useCallback186(
95314
95397
  // fallow-ignore-next-line complexity
95315
95398
  async (inputPath, options) => {
95316
- const response = await fetch(
95399
+ const response = await studioApiFetch(
95317
95400
  `/api/projects/${encodeURIComponent(projectId)}/media/remove-background`,
95318
95401
  {
95319
95402
  method: "POST",
@@ -95751,7 +95834,7 @@ import { Fragment as Fragment66, jsx as jsx236, jsxs as jsxs189 } from "react/js
95751
95834
  function useStudioSidePanels(left, right, projectId) {
95752
95835
  const stableLeft = useStableHandlers(left, projectId);
95753
95836
  const stableRight = useStableHandlers(right, projectId);
95754
- return useMemo87(
95837
+ return useMemo88(
95755
95838
  () => /* @__PURE__ */ jsxs189(Fragment66, { children: [
95756
95839
  /* @__PURE__ */ jsx236(StudioLeftPanels, { ...stableLeft }),
95757
95840
  /* @__PURE__ */ jsx236(StudioRightPanels, { ...stableRight })
@@ -95890,7 +95973,7 @@ async function extendKeyframedTweenToPlayhead(session, sel, anim, currentTime, d
95890
95973
  position: extended.position,
95891
95974
  duration: extended.duration,
95892
95975
  keyframes: extended.keyframes,
95893
- ease: anim.ease
95976
+ ...keyframeEases(anim)
95894
95977
  },
95895
95978
  {
95896
95979
  label: "Add keyframe",
@@ -96770,7 +96853,7 @@ function StudioSplash({ waiting }) {
96770
96853
  import { useEffect as useEffect155, useState as useState156 } from "react";
96771
96854
  async function resolveHashProject(id) {
96772
96855
  try {
96773
- const res = await fetch(`/api/projects/${encodeURIComponent(id)}`);
96856
+ const res = await studioApiFetch(`/api/projects/${encodeURIComponent(id)}`);
96774
96857
  if (res.ok) return { outcome: "ok", status: res.status };
96775
96858
  return { outcome: res.status === 404 ? "missing" : "unknown", status: res.status };
96776
96859
  } catch {
@@ -96782,7 +96865,7 @@ function replaceProjectHash(projectId) {
96782
96865
  }
96783
96866
  async function firstProjectId() {
96784
96867
  try {
96785
- const res = await fetch("/api/projects");
96868
+ const res = await studioApiFetch("/api/projects");
96786
96869
  const data = await res.json();
96787
96870
  return data.projects?.[0]?.id ?? null;
96788
96871
  } catch {
@@ -96809,7 +96892,7 @@ function useServerConnection() {
96809
96892
  retryTimer = window.setTimeout(tryConnect, 2e3);
96810
96893
  }
96811
96894
  function tryConnect() {
96812
- fetch("/api/projects").then((r) => r.json()).then(async (data) => {
96895
+ studioApiFetch("/api/projects").then((r) => r.json()).then(async (data) => {
96813
96896
  if (cancelled) return;
96814
96897
  if (hashProjectId) {
96815
96898
  const verdict = await resolveHashProject(hashProjectId);
@@ -96953,7 +97036,7 @@ function useCompositionContentLoader({
96953
97036
  if (!projectId) return;
96954
97037
  setActiveCompPath(comp.endsWith(".html") ? comp : null);
96955
97038
  setEditingFile({ path: comp, content: null });
96956
- fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`)).then(async (r) => {
97039
+ studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`)).then(async (r) => {
96957
97040
  if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
96958
97041
  return r.json();
96959
97042
  }).then((data) => {
@@ -97074,7 +97157,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97074
97157
  setRefreshKey
97075
97158
  });
97076
97159
  const fileManager = useStableHandlers(fileManagerResult, projectId);
97077
- const masterCompPath = useMemo88(
97160
+ const masterCompPath = useMemo89(
97078
97161
  () => resolveMasterCompositionPath(fileManager.compositions),
97079
97162
  [fileManager.compositions]
97080
97163
  );
@@ -97178,7 +97261,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97178
97261
  } = useStableHandlers(blockHandlersResult, projectId);
97179
97262
  const dismissBlockParams = useCallback194(() => setActiveBlockParams(null), [setActiveBlockParams]);
97180
97263
  const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
97181
- const layout = useMemo88(
97264
+ const layout = useMemo89(
97182
97265
  () => ({ ...panelLayout, setRightPanelTab }),
97183
97266
  [panelLayout, setRightPanelTab]
97184
97267
  );
@@ -97227,9 +97310,9 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97227
97310
  handleDuplicate,
97228
97311
  onResetKeyframes: () => resetKeyframesRef.current(),
97229
97312
  onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
97230
- onAfterUndoRedo: (restore) => {
97313
+ onAfterUndoRedo: (restore2) => {
97231
97314
  invalidateGsapCacheRef.current();
97232
- timelineEditing.restoreLiveLanes(restore);
97315
+ timelineEditing.restoreLiveLanes(restore2);
97233
97316
  },
97234
97317
  onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
97235
97318
  onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
@@ -97403,7 +97486,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
97403
97486
  handlePreviewIframeRef,
97404
97487
  refreshPreviewDocumentVersion
97405
97488
  });
97406
- const timelineToolbar = useMemo88(
97489
+ const timelineToolbar = useMemo89(
97407
97490
  () => /* @__PURE__ */ jsx244(
97408
97491
  TimelineToolbar,
97409
97492
  {
@@ -97912,7 +97995,7 @@ function findElementBlock(html2, elementId2) {
97912
97995
  }
97913
97996
 
97914
97997
  // src/hooks/useDomStyleCommit.ts
97915
- import { useCallback as useCallback196, useMemo as useMemo89, useRef as useRef189, useState as useState160 } from "react";
97998
+ import { useCallback as useCallback196, useMemo as useMemo90, useRef as useRef189, useState as useState160 } from "react";
97916
97999
  var noop = () => {
97917
98000
  };
97918
98001
  function useDomStyleCommit({
@@ -97928,7 +98011,7 @@ function useDomStyleCommit({
97928
98011
  const [queue2] = useState160(createDomEditSaveQueue);
97929
98012
  useMountEffect(() => () => queue2.destroy());
97930
98013
  const versions = useRef189(/* @__PURE__ */ new Map()).current;
97931
- const editHistory = useMemo89(() => ({ recordEdit }), [recordEdit]);
98014
+ const editHistory = useMemo90(() => ({ recordEdit }), [recordEdit]);
97932
98015
  const persistDomEditOperations = useDomEditPersist({
97933
98016
  activeCompPath,
97934
98017
  previewIframeRef: iframeRef,
@@ -97960,7 +98043,7 @@ function useDomStyleCommit({
97960
98043
  }
97961
98044
 
97962
98045
  // src/hooks/useDomGeometryCommit.ts
97963
- import { useCallback as useCallback197, useMemo as useMemo90, useRef as useRef190, useState as useState161 } from "react";
98046
+ import { useCallback as useCallback197, useMemo as useMemo91, useRef as useRef190, useState as useState161 } from "react";
97964
98047
  var noop2 = () => {
97965
98048
  };
97966
98049
  var NO_SELECTED_ANIMATIONS = [];
@@ -97976,7 +98059,7 @@ function useDomGeometryCommit({
97976
98059
  const projectIdRef = useRef190(projectId);
97977
98060
  projectIdRef.current = projectId;
97978
98061
  const pending2 = useRef190(/* @__PURE__ */ new Set()).current;
97979
- const editHistory = useMemo90(() => ({ recordEdit }), [recordEdit]);
98062
+ const editHistory = useMemo91(() => ({ recordEdit }), [recordEdit]);
97980
98063
  const reload = useCallback197(
97981
98064
  () => reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload(),
97982
98065
  [reloadPreview, iframeRef]
@@ -98033,6 +98116,7 @@ function useDomGeometryCommit({
98033
98116
  domEditSelection: null,
98034
98117
  selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
98035
98118
  gsapCommitMutation: gsap3.commitMutation,
98119
+ activeCompPath,
98036
98120
  previewIframeRef: iframeRef,
98037
98121
  showToast,
98038
98122
  bumpGsapCache,
@@ -98048,10 +98132,10 @@ function useDomGeometryCommit({
98048
98132
  updateArcSegment: gsap3.updateArcSegment
98049
98133
  });
98050
98134
  const saved2 = useCallback197(
98051
- async (commit, restore = noop2) => {
98135
+ async (commit, restore2 = noop2) => {
98052
98136
  const refusal2 = !projectIdRef.current ? "No project is open" : !isPreviewBooted(projectIdRef.current) ? "The preview for this project has not loaded yet" : null;
98053
98137
  if (refusal2) {
98054
- restore();
98138
+ restore2();
98055
98139
  showToast(refusal2, "error");
98056
98140
  throw new Error(refusal2);
98057
98141
  }
@@ -98065,11 +98149,11 @@ function useDomGeometryCommit({
98065
98149
  },
98066
98150
  [pending2, showToast]
98067
98151
  );
98068
- return useMemo90(
98152
+ return useMemo91(
98069
98153
  () => ({
98070
98154
  commitPathOffset: (selection, next, modifiers) => saved2(() => handleGsapAwarePathOffsetCommit(selection, next, modifiers)),
98071
98155
  commitGroupPathOffset: (updates) => saved2(() => handleGsapAwareGroupPathOffsetCommit(updates)),
98072
- commitBoxSize: (selection, next, offset, restore) => saved2(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore), restore),
98156
+ commitBoxSize: (selection, next, offset, restore2) => saved2(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore2), restore2),
98073
98157
  commitRotation: (selection, next) => saved2(() => handleGsapAwareRotationCommit(selection, next)),
98074
98158
  waitForPendingSaves: () => Promise.allSettled([...pending2]).then(noop2)
98075
98159
  }),
@@ -98093,6 +98177,7 @@ export {
98093
98177
  AudioMeterStrip,
98094
98178
  AudioWaveform,
98095
98179
  Button,
98180
+ ColorField,
98096
98181
  CompositionBreadcrumb,
98097
98182
  CompositionThumbnail,
98098
98183
  ConnectedDomEditOverlay,
@@ -98104,6 +98189,7 @@ export {
98104
98189
  EditorShell,
98105
98190
  ExternalFileConflictBanner,
98106
98191
  FileTree,
98192
+ GradientField,
98107
98193
  GridOverlay,
98108
98194
  HyperframesLoader,
98109
98195
  HyperframesLogo,
@@ -98223,6 +98309,7 @@ export {
98223
98309
  useProjectFileWriter,
98224
98310
  useRenderClipContent,
98225
98311
  useRenderQueue,
98312
+ useThumbnailStill,
98226
98313
  useTimelineContext,
98227
98314
  useTimelineEditing,
98228
98315
  useTimelinePlayer