hyperframes 0.8.127 → 0.8.129

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 (177) hide show
  1. package/dist/{add-7CE5XYK2.js → add-MFBQ4JQ3.js} +16 -16
  2. package/dist/{animatedGifPrep-4VAIRX7D.js → animatedGifPrep-P2T6YM7Z.js} +7 -7
  3. package/dist/{asset-22WQELZE.js → asset-LOZG7JHC.js} +2 -2
  4. package/dist/{assetCataloger-A2AITAHA.js → assetCataloger-VBLPHX24.js} +3 -3
  5. package/dist/{auth-33GV2LO2.js → auth-JKCXKQRS.js} +2 -2
  6. package/dist/{autoUpdate-OIXGTNLS.js → autoUpdate-5KHORE4C.js} +2 -2
  7. package/dist/{backgroundChecks-BVP36PXK.js → backgroundChecks-DTVWBC74.js} +3 -3
  8. package/dist/backgroundChecksWorker.js +3 -3
  9. package/dist/{batchRender-URT4LT44.js → batchRender-FT7AP7XH.js} +4 -4
  10. package/dist/{beats-U34XHTAA.js → beats-ZLLBJMPB.js} +15 -15
  11. package/dist/{benchmark-FNGERKUQ.js → benchmark-RU2FBIWR.js} +13 -13
  12. package/dist/{browser-35LEQMCO.js → browser-RQV6NSLN.js} +10 -10
  13. package/dist/{bundleWithLocalizedFonts-4CGUDHCA.js → bundleWithLocalizedFonts-5QOPOB5D.js} +2 -2
  14. package/dist/{capture-M2JJS4F2.js → capture-RLZA6EMR.js} +10 -10
  15. package/dist/{capture-GZKMVNKK.js → capture-SZZ4IYJK.js} +2 -2
  16. package/dist/{catalog-OVTLU2MC.js → catalog-LYPHWJDJ.js} +16 -16
  17. package/dist/{check-DBVGHGKO.js → check-R4UNOR5Z.js} +36 -36
  18. package/dist/{checkBrowser-2SQIUOXA.js → checkBrowser-RSUTDBXE.js} +37 -37
  19. package/dist/{chunk-AGNZ2QLE.js → chunk-2YN64IN5.js} +2 -2
  20. package/dist/{chunk-ZZAHIKFK.js → chunk-3ZP5DXE7.js} +1 -1
  21. package/dist/{chunk-RUYSBG3F.js → chunk-4NWOGZZS.js} +32 -30
  22. package/dist/{chunk-G33WTKF3.js → chunk-557OWRHF.js} +2 -2
  23. package/dist/{chunk-ZOUDTLLH.js → chunk-6CTIGJY7.js} +2 -2
  24. package/dist/{chunk-CSHKZ3SL.js → chunk-7HG4BFIN.js} +1 -1
  25. package/dist/{chunk-7YJ7CCKW.js → chunk-7JZ4FXP2.js} +1 -1
  26. package/dist/{chunk-6252EFFW.js → chunk-A2Z4Z4VL.js} +1 -1
  27. package/dist/{chunk-7DPBFAYT.js → chunk-AVUUN2XD.js} +33 -31
  28. package/dist/{chunk-HXGPETEM.js → chunk-B6YD6JAS.js} +1 -1
  29. package/dist/{chunk-RHBM33JF.js → chunk-BI6TVCDI.js} +7 -7
  30. package/dist/{chunk-F4WYYNNG.js → chunk-CFKU7HTK.js} +1 -1
  31. package/dist/{chunk-NDPSG5WQ.js → chunk-CWPVM2QV.js} +2 -2
  32. package/dist/{chunk-YSCN66NT.js → chunk-FCSIGXXP.js} +1 -1
  33. package/dist/{chunk-ZLJXATSQ.js → chunk-FI4I7CME.js} +1 -1
  34. package/dist/{chunk-KPY6JX4K.js → chunk-GO74M65A.js} +11 -11
  35. package/dist/{chunk-TL7Y6IBY.js → chunk-GOAUWA46.js} +3 -3
  36. package/dist/{chunk-6ACXAUBT.js → chunk-H64TB7NB.js} +4 -4
  37. package/dist/{chunk-S2WNVYLQ.js → chunk-HRUGC23L.js} +1 -1
  38. package/dist/chunk-HWKCIWQT.js +3578 -0
  39. package/dist/{chunk-MK2HBL6E.js → chunk-IUZFXWSV.js} +1 -1
  40. package/dist/{chunk-YII7JZ2C.js → chunk-IXCQU5RO.js} +1 -1
  41. package/dist/{chunk-TP4PR3SL.js → chunk-JO2J4S2L.js} +3 -3
  42. package/dist/{chunk-OE66T7R2.js → chunk-K2EQN4VU.js} +1 -1
  43. package/dist/{chunk-D4NPKZFY.js → chunk-K2FTXJYH.js} +3 -3
  44. package/dist/{chunk-CX7NQZDE.js → chunk-KBJUXVOJ.js} +3 -3
  45. package/dist/{chunk-QR7IRRUX.js → chunk-KHSCFD7K.js} +5 -5
  46. package/dist/{chunk-BYI2K4RN.js → chunk-LHSR3M4M.js} +2 -2
  47. package/dist/{chunk-PVVUMILD.js → chunk-LYNI3FOL.js} +1 -1
  48. package/dist/{chunk-HZHXHO4V.js → chunk-MWB3ZIFM.js} +1 -1
  49. package/dist/{chunk-OQN3AUOV.js → chunk-NTIV6QFJ.js} +12 -12
  50. package/dist/{chunk-7TSIAYXZ.js → chunk-PDAMK4Q5.js} +1 -1
  51. package/dist/{chunk-LLU2BJYY.js → chunk-Q7IU7MOR.js} +2 -2
  52. package/dist/{chunk-BE6X3G5C.js → chunk-QC4FJVLM.js} +1 -1
  53. package/dist/{chunk-EKGD52A2.js → chunk-QZDMFVY7.js} +2 -2
  54. package/dist/{chunk-IAKZWKNV.js → chunk-RMHUQGRK.js} +4 -4
  55. package/dist/{chunk-V6LLKVWS.js → chunk-RZAAVJ2F.js} +1 -1
  56. package/dist/{chunk-Q3I4JEGM.js → chunk-S7NPLFWV.js} +286 -143
  57. package/dist/{chunk-PZSNUSBM.js → chunk-TCLGPL65.js} +2 -2
  58. package/dist/{chunk-L32XKBOF.js → chunk-UBC3SMKR.js} +1 -1
  59. package/dist/{chunk-SC4T53X4.js → chunk-UDO4A3HY.js} +3 -3
  60. package/dist/{chunk-T6HJCRJC.js → chunk-UEZXMWG3.js} +2 -2
  61. package/dist/{chunk-32MY4AZN.js → chunk-WPHMNO44.js} +1345 -23
  62. package/dist/{chunk-GBTGTS35.js → chunk-WWTEA2BG.js} +1 -1
  63. package/dist/{chunk-P4PMYDLR.js → chunk-XS4EJSKU.js} +1 -1
  64. package/dist/{chunk-S4AMCVAK.js → chunk-YQZNILVH.js} +5 -5
  65. package/dist/{chunk-OLVR6P6R.js → chunk-Z6B37SNM.js} +2 -2
  66. package/dist/{chunk-TPWWGKU7.js → chunk-ZJUKFZ2P.js} +2 -2
  67. package/dist/{chunk-K3ZB5A3K.js → chunk-ZMXUQLI2.js} +55 -14
  68. package/dist/{clean-STQ2XPVL.js → clean-RDKBYQYS.js} +20 -20
  69. package/dist/cli.js +52 -52
  70. package/dist/{cloud-OQNTIR2N.js → cloud-DRCSH3RO.js} +4 -4
  71. package/dist/{cloudrun-ELOBBQHC.js → cloudrun-AJUTHUEA.js} +5 -5
  72. package/dist/{compare-7PHURXFI.js → compare-EJNCWA7A.js} +28 -28
  73. package/dist/{compiler-A3FX5DBC.js → compiler-QO36EXLS.js} +3 -3
  74. package/dist/{component-56DVF6XU.js → component-55TKGKMP.js} +3 -3
  75. package/dist/{compositions-2DMUUOFR.js → compositions-LDU2NXG3.js} +12 -12
  76. package/dist/{delete-EECQVAVJ.js → delete-M5FEYAQW.js} +3 -3
  77. package/dist/{deterministicFonts-64U7A25I.js → deterministicFonts-HAEMV4TM.js} +5 -4
  78. package/dist/{dist-OJYO32WN.js → dist-FNNGCKFS.js} +1 -1
  79. package/dist/{dist-IPEAV427.js → dist-S3PJNPXQ.js} +3 -3
  80. package/dist/{doctor-F3CXFLFV.js → doctor-AGGUO5NJ.js} +11 -11
  81. package/dist/{events-2LJGIZKG.js → events-CIWUMG4Z.js} +10 -10
  82. package/dist/{events-H5RGIYVK.js → events-LBZKC7L7.js} +9 -9
  83. package/dist/{feedback-VCMOOK2O.js → feedback-J7I7VS4Q.js} +13 -13
  84. package/dist/{figma-BL4HCJ5P.js → figma-2AIFPR3T.js} +3 -3
  85. package/dist/{fontData.generated-BW5QEPUT.js → fontData.generated-NXAOI4GH.js} +1 -1
  86. package/dist/fontLocalizeCli.js +11 -12
  87. package/dist/{get-HWN5Z3BL.js → get-ZJBDIWZJ.js} +3 -3
  88. package/dist/{gpuPolicy-37CE57HN.js → gpuPolicy-2ZUZE5D6.js} +7 -7
  89. package/dist/{grade-compare-3OPZYNNZ.js → grade-compare-XVYJT6ZR.js} +28 -28
  90. package/dist/{gsapParser-LOSAMHNR.js → gsapParser-DIYYX6JT.js} +63 -41
  91. package/dist/{gsapParserAcorn-ZO2LPEZY.js → gsapParserAcorn-E3GEIRZB.js} +1 -1
  92. package/dist/{help-X3EPTRKV.js → help-TBVVVEDS.js} +1 -1
  93. package/dist/{history-YR6YTOLT.js → history-NZPEQU4L.js} +20 -20
  94. package/dist/hyperframe-runtime.js +30 -30
  95. package/dist/hyperframe.manifest.json +1 -1
  96. package/dist/hyperframe.runtime.iife.js +30 -30
  97. package/dist/hyperframes-player.global.js +3 -3
  98. package/dist/{info-QG7OXNW6.js → info-T6BT2MFN.js} +12 -12
  99. package/dist/{init-JUHW2ZN3.js → init-4EPQ7SDV.js} +23 -23
  100. package/dist/{inspect-LIDYSXAG.js → inspect-BPZFMWWW.js} +29 -29
  101. package/dist/{installer-YH76H5ML.js → installer-K6O5WF44.js} +5 -5
  102. package/dist/{keyframes-ZLHVLMRA.js → keyframes-UL3XY52K.js} +45 -43
  103. package/dist/{lambda-T33I4AX6.js → lambda-D7UMN4DJ.js} +6 -6
  104. package/dist/{layout-VD36CWWV.js → layout-YYGOEQKI.js} +29 -29
  105. package/dist/{lint-JAWDGLYW.js → lint-OW2KOTEE.js} +15 -15
  106. package/dist/{lintProject-IOQ4DBS4.js → lintProject-OHBBM3DJ.js} +1 -1
  107. package/dist/{list-24TGFCB3.js → list-EIDAPK3I.js} +3 -3
  108. package/dist/{login-E2PE6CAD.js → login-DWU6OUCZ.js} +4 -4
  109. package/dist/{media-treatment-2BMUTUGD.js → media-treatment-I7BTOWX4.js} +15 -15
  110. package/dist/{models-TS5PWNPS.js → models-C4E4IWJD.js} +8 -8
  111. package/dist/{motionShot-RU4MTVOO.js → motionShot-S4MIGMT5.js} +13 -13
  112. package/dist/{normalize-audio-245DPF4X.js → normalize-audio-X2M4VF2O.js} +15 -15
  113. package/dist/{open-RH7YWUVH.js → open-35JKHN2G.js} +11 -11
  114. package/dist/{pipeline-ZK3JBXX5.js → pipeline-TNGWUZQC.js} +7 -7
  115. package/dist/{play-VK6IF765.js → play-F2LMNJEH.js} +31 -31
  116. package/dist/{preflight-H7YMOAYQ.js → preflight-SMNV6RW2.js} +8 -8
  117. package/dist/{present-CLKL6QSK.js → present-AD4N25LI.js} +21 -21
  118. package/dist/{preview-GEPWX2GX.js → preview-QWMTUNE2.js} +21 -21
  119. package/dist/{producer-HBP36IDM.js → producer-ECLEN5RP.js} +1 -1
  120. package/dist/{projectConfig-FPSBKFQO.js → projectConfig-HYJMJ5BY.js} +7 -7
  121. package/dist/{publication-ACSV54DD.js → publication-HQ5Y3KBM.js} +3 -3
  122. package/dist/{publish-L2NGI7MV.js → publish-VHDYQASC.js} +12 -12
  123. package/dist/{remove-background-JRRBJN4U.js → remove-background-W2YX2NVH.js} +8 -8
  124. package/dist/{render-RXMA4QYX.js → render-2HZ6WDVA.js} +18 -18
  125. package/dist/{render-LZEOB6JN.js → render-AO5VX3YI.js} +4 -4
  126. package/dist/{render-TDSGRTQQ.js → render-JJVVEVIZ.js} +27 -27
  127. package/dist/{render-batch-ZJQ5VHY6.js → render-batch-YH7CTWNY.js} +4 -4
  128. package/dist/{renderCancellation-MS5MAATT.js → renderCancellation-JPIHE47X.js} +7 -7
  129. package/dist/renderSetupWorker.js +1 -1
  130. package/dist/{resolver-WODAPAQC.js → resolver-IZ647GBZ.js} +5 -5
  131. package/dist/{sherpa-B3IHJHTK.js → sherpa-KKLNW6U2.js} +8 -8
  132. package/dist/{skills-56DFZVPS.js → skills-O7QZLKAG.js} +12 -12
  133. package/dist/{skillsUpdateCheck-AXKUBY44.js → skillsUpdateCheck-W563PHIX.js} +3 -3
  134. package/dist/{snapshot-NYQJL43J.js → snapshot-4M37T4WP.js} +34 -34
  135. package/dist/{src-UQ72QBPU.js → src-6YQBJGWR.js} +12 -13
  136. package/dist/{src-4K2RSSO5.js → src-PVYUYV3M.js} +6 -6
  137. package/dist/{staticProjectServer-OJH6TFA3.js → staticProjectServer-7EU666GS.js} +16 -16
  138. package/dist/{status-MZT3S6U2.js → status-YM52BQ6S.js} +7 -7
  139. package/dist/studio/assets/{hyperframes-player-uqzDaxVL.js → hyperframes-player-DfEcBl9r.js} +1 -1
  140. package/dist/studio/assets/{index-DXSYlqmV.js → index-9ZaZFCMZ.js} +1 -1
  141. package/dist/studio/assets/{index-CLBNYlDY.js → index-Bmy-KVib.js} +1 -1
  142. package/dist/studio/assets/{index-COhbL1xq.js → index-C9YkEyN4.js} +189 -189
  143. package/dist/studio/{chunk-3F47EFCT.js → chunk-7UQWVN52.js} +17 -10
  144. package/dist/studio/chunk-7UQWVN52.js.map +1 -0
  145. package/dist/studio/{chunk-246VB2G3.js → chunk-DRVWEPO5.js} +35 -7
  146. package/dist/studio/chunk-DRVWEPO5.js.map +1 -0
  147. package/dist/studio/{chunk-5EKWJZLM.js → chunk-TDWGXBUT.js} +1 -1
  148. package/dist/studio/chunk-TDWGXBUT.js.map +1 -0
  149. package/dist/studio/{domEditingLayers-OL7GLSS4.js → domEditingLayers-IEVLO54Z.js} +3 -3
  150. package/dist/studio/index.d.ts +8 -9
  151. package/dist/studio/index.html +1 -1
  152. package/dist/studio/index.js +1396 -917
  153. package/dist/studio/index.js.map +1 -1
  154. package/dist/studio/player.js +2 -2
  155. package/dist/{studio-api-IT5SYLXJ.js → studio-api-PNQZRPX5.js} +10 -10
  156. package/dist/{studioServer-KHYH2K3Q.js → studioServer-PTUEDJ4R.js} +56 -50
  157. package/dist/{system-4KER4Y62.js → system-EZYZNUFD.js} +7 -7
  158. package/dist/{telemetry-G3NYQGWI.js → telemetry-DTSHY4NX.js} +10 -10
  159. package/dist/{timeline-CXK4RVZE.js → timeline-OQEMNGIW.js} +20 -20
  160. package/dist/{tokens-RLU7WBIC.js → tokens-IGPEYHDJ.js} +2 -2
  161. package/dist/{transcribe-SOY2USBA.js → transcribe-BNSJRR4M.js} +8 -8
  162. package/dist/{transcribe-Y37DWCAV.js → transcribe-LC5JLRDF.js} +13 -13
  163. package/dist/{tts-CGUXR7R4.js → tts-P3LIPIL5.js} +1 -1
  164. package/dist/{updateCheck-EJ5SAGDC.js → updateCheck-OJAVDMMV.js} +2 -2
  165. package/dist/{upgrade-MBWWBNGK.js → upgrade-2G4MARM4.js} +2 -2
  166. package/dist/{urlDownloader-ZJ2TZQ7C.js → urlDownloader-H6WRI2UE.js} +6 -6
  167. package/dist/{validate-4J3OCFET.js → validate-MOUGAHM2.js} +17 -17
  168. package/dist/{video-43437E6M.js → video-FGSGKRSV.js} +7 -7
  169. package/package.json +1 -1
  170. package/dist/chunk-4C656PVL.js +0 -1393
  171. package/dist/chunk-AQIS2UNW.js +0 -3578
  172. package/dist/fontCompression-ISNK7STS.js +0 -15
  173. package/dist/studio/chunk-246VB2G3.js.map +0 -1
  174. package/dist/studio/chunk-3F47EFCT.js.map +0 -1
  175. package/dist/studio/chunk-5EKWJZLM.js.map +0 -1
  176. package/dist/studio/{domEditingLayers-OL7GLSS4.js.map → domEditingLayers-IEVLO54Z.js.map} +0 -0
  177. package/dist/{synthesize-BDXNRYZN.js → synthesize-K62TIZWI.js} +4 -4
@@ -9,6 +9,7 @@ import {
9
9
  STUDIO_PREVIEW_FPS,
10
10
  applyPreviewVariablesToUrl,
11
11
  clampToDuration,
12
+ collectElementPatches,
12
13
  computePinnedZoomPercent,
13
14
  createTimelineResetState,
14
15
  effectiveThumbnailMode,
@@ -26,6 +27,7 @@ import {
26
27
  liveTime,
27
28
  panelsInZone,
28
29
  parseDockLayout,
30
+ patchElements,
29
31
  readStudioUiPreferences,
30
32
  secondsToFrame,
31
33
  stepFrameTime,
@@ -37,7 +39,7 @@ import {
37
39
  usePreviewVariablesStore,
38
40
  whenPreviewBooted,
39
41
  writeStudioUiPreferences
40
- } from "./chunk-246VB2G3.js";
42
+ } from "./chunk-DRVWEPO5.js";
41
43
  import {
42
44
  Button,
43
45
  ContextMenu,
@@ -102,7 +104,7 @@ import {
102
104
  resolveDomEditSelection,
103
105
  serializeDomEditTextFields,
104
106
  studioApiFetch
105
- } from "./chunk-3F47EFCT.js";
107
+ } from "./chunk-7UQWVN52.js";
106
108
  import {
107
109
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
108
110
  LAYER_REVEAL_PRIOR_Z_ATTR,
@@ -162,7 +164,7 @@ import {
162
164
  splitTimelineElementKey,
163
165
  toHexColor,
164
166
  wrapTimeline
165
- } from "./chunk-5EKWJZLM.js";
167
+ } from "./chunk-TDWGXBUT.js";
166
168
  import "./chunk-PZ5AY32C.js";
167
169
 
168
170
  // src/components/EditorShell.tsx
@@ -2779,7 +2781,6 @@ function resolveTimelineResize(input, edge, clientX) {
2779
2781
  const clampedDelta = clamp2(deltaTime, minDelta, maxDelta);
2780
2782
  return applyClipStartTrimDelta(input, clampedDelta);
2781
2783
  }
2782
- var MAX_HAND_EDIT_CLIPS = 3;
2783
2784
  function resolveBlockedTimelineEditIntent(input) {
2784
2785
  if (input.capabilities.canMove) {
2785
2786
  return null;
@@ -4348,52 +4349,6 @@ var useLinkedClipPreferences = create2((set) => ({
4348
4349
  }));
4349
4350
  var isLinkedSelectionOn = () => useLinkedClipPreferences.getState().linkedSelection;
4350
4351
 
4351
- // src/player/components/timelineLinkSelection.ts
4352
- function selectClipWithLinks(key2, altKey, setSelectedElementId) {
4353
- const state = usePlayerStore.getState();
4354
- if (altKey || !isLinkedSelectionOn()) {
4355
- state.setSelection([key2], key2);
4356
- return;
4357
- }
4358
- setSelectedElementId(key2);
4359
- const { selectedElementIds, elements } = usePlayerStore.getState();
4360
- const expanded = expandToLinkedMembers(selectedElementIds, elements);
4361
- if (expanded.size > selectedElementIds.size) state.setSelection(expanded, key2);
4362
- }
4363
- function toggleClipWithLinks(key2, altKey) {
4364
- const state = usePlayerStore.getState();
4365
- const element = state.elements.find((el) => (el.key ?? el.id) === key2);
4366
- const members = element ? linkedMembersOf(element, state.elements, !altKey && isLinkedSelectionOn()) : [];
4367
- const memberKeys = members.length > 0 ? members.map((el) => el.key ?? el.id) : [key2];
4368
- const next = new Set(state.selectedElementIds);
4369
- const removing = next.has(key2);
4370
- for (const memberKey of memberKeys) {
4371
- if (removing) next.delete(memberKey);
4372
- else next.add(memberKey);
4373
- }
4374
- state.setSelection(next, removing ? state.selectedElementId : key2);
4375
- const primary = usePlayerStore.getState().selectedElementId;
4376
- return state.elements.find((el) => (el.key ?? el.id) === primary) ?? null;
4377
- }
4378
- function exceedsHandEditLimit(grabbed, altKey, edge) {
4379
- const { selectedElementIds, elements } = usePlayerStore.getState();
4380
- const grabbedKey = grabbed.key ?? grabbed.id;
4381
- const keys = linkedGestureKeys(
4382
- selectedElementIds,
4383
- grabbed,
4384
- elements,
4385
- altKey,
4386
- isLinkedSelectionOn()
4387
- );
4388
- const changed = edge ? buildTimelineGroupResizeMembers(
4389
- elements,
4390
- dropMisalignedTrimPartners(keys, grabbed, elements, edge),
4391
- grabbedKey,
4392
- edge
4393
- ) : resolveGroupMovers(elements, keys, grabbedKey);
4394
- return (changed?.length ?? 1) > MAX_HAND_EDIT_CLIPS;
4395
- }
4396
-
4397
4352
  // src/player/components/timelineReadOnly.ts
4398
4353
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useEffectEvent } from "react";
4399
4354
  var TimelineReadOnlyContext = createContext2(null);
@@ -4504,20 +4459,86 @@ function timelineEaseFocusId(elementId2, target) {
4504
4459
  import { useCallback as useCallback8, useMemo as useMemo6 } from "react";
4505
4460
 
4506
4461
  // src/player/components/TimelinePropertyLanes.tsx
4507
- import { useMemo as useMemo3 } from "react";
4462
+ import {
4463
+ useMemo as useMemo3
4464
+ } from "react";
4508
4465
  import {
4509
4466
  classifyPropertyGroup
4510
4467
  } from "@hyperframes/core/gsap-parser";
4511
4468
 
4512
4469
  // src/hooks/gsapShared.ts
4513
4470
  import { isHtmlElement as isHtmlElement2 } from "@hyperframes/core/runtime/dom-realm";
4471
+ import { getObjectArrayKeyframeTiming } from "@hyperframes/parsers/gsap-parser";
4472
+
4473
+ // src/utils/gsapKeyframeEases.ts
4474
+ import gsap2 from "gsap";
4475
+ var PERCENTAGE_SEGMENT_EASE = "power1.inOut";
4476
+ var ARRAY_STEP_EASE = "none";
4477
+ var MOTION_PATH_RUN_EASE = "power1.out";
4478
+ function keyframedTweenEases(anim) {
4479
+ const data = anim.keyframes;
4480
+ const fallback = data?.format === "object-array" ? ARRAY_STEP_EASE : data?.easeEach ?? PERCENTAGE_SEGMENT_EASE;
4481
+ return {
4482
+ run: data ? data.ease ?? anim.ease ?? (data.fromMotionPath ? MOTION_PATH_RUN_EASE : void 0) : void 0,
4483
+ segment: (arriving) => arriving.ease ?? fallback
4484
+ };
4485
+ }
4486
+ function easeFunction(name) {
4487
+ if (!name || name === "none" || name === "linear") return null;
4488
+ return gsap2.parseEase(name) ?? null;
4489
+ }
4490
+ var runEaseOf = (anim) => keyframedTweenEases(anim).run;
4491
+ var SCAN_STEPS = 200;
4492
+ var BISECT_STEPS = 40;
4493
+ var timingEases = /* @__PURE__ */ new Map();
4494
+ function timingEase(runEase) {
4495
+ if (!runEase) return null;
4496
+ if (!timingEases.has(runEase)) {
4497
+ const run = easeFunction(runEase);
4498
+ const samples = Array.from(
4499
+ { length: SCAN_STEPS + 1 },
4500
+ (_, step) => run?.(step / SCAN_STEPS) ?? 0
4501
+ );
4502
+ const oneToOne = samples.every((v, i) => v >= 0 && v <= 1 && (i === 0 || v > samples[i - 1]));
4503
+ timingEases.set(runEase, run && oneToOne ? run : null);
4504
+ }
4505
+ return timingEases.get(runEase) ?? null;
4506
+ }
4507
+ function progressAtTime(runEase, timePercentage) {
4508
+ const run = timingEase(runEase);
4509
+ return run ? run(timePercentage / 100) * 100 : timePercentage;
4510
+ }
4511
+ function timeAtProgress(runEase, percentage) {
4512
+ const run = timingEase(runEase);
4513
+ if (!run || percentage <= 0 || percentage >= 100) return percentage;
4514
+ const target = percentage / 100;
4515
+ let low = 0;
4516
+ let high = 1;
4517
+ for (let step = 1; step <= SCAN_STEPS; step++) {
4518
+ if (run(step / SCAN_STEPS) >= target) {
4519
+ low = (step - 1) / SCAN_STEPS;
4520
+ high = step / SCAN_STEPS;
4521
+ break;
4522
+ }
4523
+ }
4524
+ for (let step = 0; step < BISECT_STEPS; step++) {
4525
+ const mid = (low + high) / 2;
4526
+ if (run(mid) >= target) high = mid;
4527
+ else low = mid;
4528
+ }
4529
+ return high * 100;
4530
+ }
4514
4531
 
4515
4532
  // src/utils/globalTimeCompiler.ts
4533
+ import { GSAP_DEFAULT_DURATION } from "@hyperframes/parsers/gsap-parser";
4516
4534
  function absoluteToPercentage(time, tweenStart, tweenDuration) {
4517
4535
  if (tweenDuration <= 0) return 0;
4518
4536
  const raw = (time - tweenStart) / tweenDuration * 100;
4519
4537
  return Math.max(0, Math.min(100, Math.round(raw * 1e3) / 1e3));
4520
4538
  }
4539
+ function percentageToAbsolute(pct, tweenStart, tweenDuration) {
4540
+ return tweenStart + pct / 100 * tweenDuration;
4541
+ }
4521
4542
  function isTimeWithinTween(time, tweenStart, tweenDuration) {
4522
4543
  return time >= tweenStart && time <= tweenStart + tweenDuration;
4523
4544
  }
@@ -4528,8 +4549,8 @@ function resolveTweenStart(animation) {
4528
4549
  if (!Number.isNaN(parsed)) return parsed;
4529
4550
  return null;
4530
4551
  }
4531
- function resolveTweenDuration(animation, fallback = 0.5) {
4532
- return animation.duration ?? fallback;
4552
+ function resolveTweenDuration(animation) {
4553
+ return animation.duration ?? GSAP_DEFAULT_DURATION;
4533
4554
  }
4534
4555
  function findTweenAtTime(time, animations, selector) {
4535
4556
  for (const anim of animations) {
@@ -4545,7 +4566,13 @@ function absoluteToPercentageForAnimation(time, animation) {
4545
4566
  const start = resolveTweenStart(animation);
4546
4567
  if (start === null) return null;
4547
4568
  const duration = resolveTweenDuration(animation);
4548
- return absoluteToPercentage(time, start, duration);
4569
+ return progressAtTime(runEaseOf(animation), absoluteToPercentage(time, start, duration));
4570
+ }
4571
+ function percentageToAbsoluteForAnimation(pct, animation) {
4572
+ const start = resolveTweenStart(animation);
4573
+ if (start === null) return null;
4574
+ const duration = resolveTweenDuration(animation);
4575
+ return percentageToAbsolute(timeAtProgress(runEaseOf(animation), pct), start, duration);
4549
4576
  }
4550
4577
  function matchesSelector(tweenSelector, querySelector) {
4551
4578
  return tweenSelector.split(",").some((part) => part.trim() === querySelector);
@@ -4572,6 +4599,11 @@ function isInstantHold(animation) {
4572
4599
  }
4573
4600
  var SAFE_HASH_ID = /^-?[A-Za-z_][\w-]*$/;
4574
4601
  var KEYFRAME_PCT_MATCH = 1;
4602
+ function playsNear(anim, percentage, progress) {
4603
+ const runEase = runEaseOf(anim);
4604
+ const gap = timeAtProgress(runEase, percentage) - timeAtProgress(runEase, progress);
4605
+ return Math.abs(gap) <= KEYFRAME_PCT_MATCH;
4606
+ }
4575
4607
  function idSelector(id) {
4576
4608
  return SAFE_HASH_ID.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
4577
4609
  }
@@ -4682,20 +4714,16 @@ function tweenTargetsElement(targetSelector, selector, element) {
4682
4714
  if (!element || !doc) return false;
4683
4715
  return matchesExactlyOne(doc, targetSelector, element);
4684
4716
  }
4685
- function resolveEditableTweenDuration(animation, selection) {
4686
- const clipDuration2 = Number.parseFloat(selection.dataAttributes?.duration ?? "");
4687
- return resolveTweenDuration(
4688
- animation,
4689
- Number.isFinite(clipDuration2) && clipDuration2 > 0 ? clipDuration2 : 0.5
4690
- );
4691
- }
4692
4717
  function computeElementPercentage(currentTime, selection, animation) {
4693
4718
  if (animation) {
4694
4719
  const start = resolveTweenStart(animation);
4695
- const duration = resolveEditableTweenDuration(animation, selection);
4720
+ const duration = resolveTweenDuration(animation);
4696
4721
  if (duration <= 0) return 0;
4697
4722
  if (start !== null) {
4698
- return absoluteToPercentage(currentTime, start, duration);
4723
+ return progressAtTime(
4724
+ runEaseOf(animation),
4725
+ absoluteToPercentage(currentTime, start, duration)
4726
+ );
4699
4727
  }
4700
4728
  }
4701
4729
  const elStart = Number.parseFloat(selection.dataAttributes?.start ?? "0") || 0;
@@ -4732,11 +4760,16 @@ function parsePercentageKeyframes(kfObj) {
4732
4760
  let easeEach;
4733
4761
  if (Array.isArray(kfObj)) {
4734
4762
  const steps = kfObj;
4763
+ const timing = getObjectArrayKeyframeTiming(
4764
+ steps.map((entry2) => entry2?.duration)
4765
+ );
4766
+ if (!timing) return null;
4735
4767
  steps.forEach((entry2, i) => {
4736
4768
  if (!entry2 || typeof entry2 !== "object") return;
4737
- const percentage = steps.length > 1 ? Math.round(i / (steps.length - 1) * 1e3) / 10 : 0;
4738
4769
  const properties = collectAnimatableKeyframeProperties(entry2);
4739
- if (Object.keys(properties).length > 0) keyframes.push({ percentage, properties, step: i });
4770
+ if (Object.keys(properties).length > 0) {
4771
+ keyframes.push({ percentage: timing.percentages[i], properties, step: i });
4772
+ }
4740
4773
  });
4741
4774
  return keyframes.length > 0 ? { keyframes } : null;
4742
4775
  }
@@ -4783,11 +4816,15 @@ function toClipPercentage(absoluteTime, clipStart2, clipDuration2, fallbackPerce
4783
4816
  }
4784
4817
  function toClipKeyframes(source, anim, clipStart2, clipDuration2) {
4785
4818
  const tweenStart = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
4786
- const tweenDuration = anim.duration ?? clipDuration2;
4819
+ const tweenDuration = resolveTweenDuration(anim);
4787
4820
  return source.map((keyframe) => ({
4788
4821
  ...keyframe,
4789
4822
  percentage: toClipPercentage(
4790
- toAbsoluteTime(tweenStart, tweenDuration, keyframe.percentage),
4823
+ toAbsoluteTime(
4824
+ tweenStart,
4825
+ tweenDuration,
4826
+ timeAtProgress(runEaseOf(anim), keyframe.percentage)
4827
+ ),
4791
4828
  clipStart2,
4792
4829
  clipDuration2,
4793
4830
  keyframe.percentage
@@ -7109,10 +7146,22 @@ function subscribeTimelineKeyframeRetimePreview(source, listener2) {
7109
7146
  listener2(coordinator.preview);
7110
7147
  return () => coordinator.previewListeners.delete(listener2);
7111
7148
  }
7149
+ function retimedRunEase(actor) {
7150
+ const id = actor.target.animationId;
7151
+ if (id === void 0) return void 0;
7152
+ const { gsapAnimations } = usePlayerStore.getState();
7153
+ const animations = gsapAnimations.get(actor.elementId) ?? [...gsapAnimations.values()].flat();
7154
+ const animation = animations.find((candidate) => candidate.id === id);
7155
+ return animation ? runEaseOf(animation) : void 0;
7156
+ }
7112
7157
  function resolveRetimeTweenPercentage(actor, toClipPercentage2) {
7113
7158
  const animationKeyframes2 = actor.target.animationId === void 0 ? actor.keyframes : actor.keyframes.filter((keyframe) => keyframe.animationId === actor.target.animationId);
7114
7159
  const tweenPercentages = animationKeyframes2.map((keyframe) => keyframe.tweenPercentage).filter((value) => typeof value === "number");
7115
- const mapped = clipToTweenPercentage(animationKeyframes2, toClipPercentage2);
7160
+ const runEase = retimedRunEase(actor);
7161
+ const timed = animationKeyframes2.map(
7162
+ (keyframe) => keyframe.tweenPercentage === void 0 ? keyframe : { ...keyframe, tweenPercentage: timeAtProgress(runEase, keyframe.tweenPercentage) }
7163
+ );
7164
+ const mapped = progressAtTime(runEase, clipToTweenPercentage(timed, toClipPercentage2));
7116
7165
  if (tweenPercentages.length === 0) return mapped;
7117
7166
  return Math.max(Math.min(...tweenPercentages), Math.min(Math.max(...tweenPercentages), mapped));
7118
7167
  }
@@ -7819,6 +7868,7 @@ function getTimelinePropertyLanes(animations, clipStart2, clipDuration2) {
7819
7868
  return Array.from(sourceGroups(animations), ([group, groupAnimations]) => ({
7820
7869
  group,
7821
7870
  animations: groupAnimations,
7871
+ elementAnimations: animations,
7822
7872
  keyframes: groupKeyframes(groupAnimations, group, clipStart2, clipDuration2)
7823
7873
  })).filter((lane) => lane.keyframes.length > 0);
7824
7874
  }
@@ -7829,6 +7879,7 @@ function TimelinePropertyLanes({
7829
7879
  clipDuration: clipDuration2,
7830
7880
  clipLeftPx,
7831
7881
  clipWidthPx,
7882
+ passengerStyle,
7832
7883
  accentColor,
7833
7884
  isSelected: isSelected2,
7834
7885
  currentPercentage,
@@ -7868,7 +7919,8 @@ function TimelinePropertyLanes({
7868
7919
  left: clipLeftPx,
7869
7920
  top: getTimelineLaneTop(laneIndex),
7870
7921
  width: clipWidthPx,
7871
- height: LANE_H
7922
+ height: LANE_H,
7923
+ ...passengerStyle
7872
7924
  },
7873
7925
  children: /* @__PURE__ */ jsx16(
7874
7926
  TimelineDiamondLane,
@@ -9145,6 +9197,13 @@ function computeStackingPatches(elements, editedKeys) {
9145
9197
  return patches;
9146
9198
  }
9147
9199
 
9200
+ // src/player/store/batchElementUpdates.ts
9201
+ function batchElementUpdates(run) {
9202
+ const patches = collectElementPatches(run);
9203
+ if (!patches || patches.size === 0) return;
9204
+ usePlayerStore.setState((state) => ({ elements: patchElements(state.elements, patches) }));
9205
+ }
9206
+
9148
9207
  // src/player/components/timelineOptimisticRevision.ts
9149
9208
  var revisionsByUpdater = /* @__PURE__ */ new WeakMap();
9150
9209
  function beginTimelineOptimisticGesture(updateElement, keys, scope = "timing") {
@@ -9170,11 +9229,13 @@ function isLatestTimelineOptimisticGesture(updateElement, gesture, key2, scope =
9170
9229
  return gesture.has(key2) && revisionsByUpdater.get(updateElement)?.get(scope)?.get(key2) === gesture.get(key2);
9171
9230
  }
9172
9231
  function rollbackLatestTimelineOptimisticGesture(updateElement, gesture, rollbacks) {
9173
- for (const rollback of rollbacks) {
9174
- if (isLatestTimelineOptimisticGesture(updateElement, gesture, rollback.key)) {
9175
- updateElement(rollback.key, rollback.updates);
9232
+ batchElementUpdates(() => {
9233
+ for (const rollback of rollbacks) {
9234
+ if (isLatestTimelineOptimisticGesture(updateElement, gesture, rollback.key)) {
9235
+ updateElement(rollback.key, rollback.updates);
9236
+ }
9176
9237
  }
9177
- }
9238
+ });
9178
9239
  }
9179
9240
 
9180
9241
  // src/components/nle/zLaneGesture.ts
@@ -9225,6 +9286,400 @@ function refreshAfterDurableLaneMove(pending2, deps) {
9225
9286
  });
9226
9287
  }
9227
9288
 
9289
+ // src/utils/studioSaveDiagnostics.ts
9290
+ var STUDIO_SAVE_ATTEMPT_PROPERTY = "__studioSaveAttempt";
9291
+ var StudioSaveHttpError = class extends Error {
9292
+ statusCode;
9293
+ alreadyToasted;
9294
+ constructor(message, statusCode, options = {}) {
9295
+ super(message);
9296
+ this.name = "StudioSaveHttpError";
9297
+ this.statusCode = statusCode;
9298
+ this.alreadyToasted = options.alreadyToasted ?? false;
9299
+ }
9300
+ };
9301
+ var StudioSaveNetworkError = class extends Error {
9302
+ constructor(message, options) {
9303
+ super(message, options);
9304
+ this.name = "StudioSaveNetworkError";
9305
+ }
9306
+ };
9307
+ var StudioFileConflictError = class extends StudioSaveHttpError {
9308
+ filePath;
9309
+ currentVersion;
9310
+ currentContent;
9311
+ attemptedContent;
9312
+ constructor(input) {
9313
+ super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
9314
+ this.name = "StudioFileConflictError";
9315
+ this.filePath = input.filePath;
9316
+ this.currentVersion = input.currentVersion;
9317
+ this.currentContent = input.currentContent;
9318
+ this.attemptedContent = input.attemptedContent;
9319
+ }
9320
+ };
9321
+ function readNumericProperty(value, key2) {
9322
+ const record = value;
9323
+ const property = record[key2];
9324
+ return typeof property === "number" && Number.isFinite(property) ? property : void 0;
9325
+ }
9326
+ function createStudioSaveAbortError() {
9327
+ if (typeof DOMException !== "undefined") return new DOMException("Save aborted", "AbortError");
9328
+ const error = new Error("Save aborted");
9329
+ error.name = "AbortError";
9330
+ return error;
9331
+ }
9332
+ function throwIfStudioSaveAborted(signal) {
9333
+ if (signal?.aborted) throw createStudioSaveAbortError();
9334
+ }
9335
+ function attachStudioSaveAttempt(error, attempt) {
9336
+ if (!error || typeof error !== "object") return error;
9337
+ try {
9338
+ Object.defineProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY, {
9339
+ value: attempt,
9340
+ configurable: true
9341
+ });
9342
+ } catch {
9343
+ }
9344
+ return error;
9345
+ }
9346
+ function getStudioSaveErrorMessage(error) {
9347
+ if (error instanceof Error && error.message) return error.message;
9348
+ if (typeof error === "string" && error.trim()) return error;
9349
+ return "Unknown save failure";
9350
+ }
9351
+ function markStudioSaveErrorAlreadyToasted(error) {
9352
+ if (!error || typeof error !== "object") return error;
9353
+ try {
9354
+ Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
9355
+ } catch {
9356
+ }
9357
+ return error;
9358
+ }
9359
+ function isStudioSaveErrorAlreadyToasted(error) {
9360
+ if (!error || typeof error !== "object") return false;
9361
+ if (error.alreadyToasted === true) return true;
9362
+ const cause = error.cause;
9363
+ return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
9364
+ }
9365
+ function getStudioSaveStatusCode(error) {
9366
+ if (!error || typeof error !== "object") return void 0;
9367
+ const direct = readNumericProperty(error, "statusCode") ?? readNumericProperty(error, "status") ?? readNumericProperty(error, "status_code");
9368
+ if (direct != null) return direct;
9369
+ const cause = error.cause;
9370
+ if (cause && cause !== error) return getStudioSaveStatusCode(cause);
9371
+ return void 0;
9372
+ }
9373
+ function getStudioSaveAttempt(error) {
9374
+ if (!error || typeof error !== "object") return void 0;
9375
+ const direct = readNumericProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY);
9376
+ if (direct != null) return direct;
9377
+ const cause = error.cause;
9378
+ if (cause && cause !== error) return getStudioSaveAttempt(cause);
9379
+ return void 0;
9380
+ }
9381
+ function isStudioSaveAbortError(error) {
9382
+ return error instanceof Error && error.name === "AbortError";
9383
+ }
9384
+ function isRetryableStudioSaveError(error) {
9385
+ if (isStudioSaveAbortError(error)) return false;
9386
+ if (error instanceof StudioSaveNetworkError) return true;
9387
+ const statusCode = getStudioSaveStatusCode(error);
9388
+ if (statusCode == null) return false;
9389
+ return statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
9390
+ }
9391
+ function buildStudioSaveFailureProperties(input) {
9392
+ const statusCode = input.statusCode ?? getStudioSaveStatusCode(input.error) ?? null;
9393
+ const attempt = input.attempt ?? getStudioSaveAttempt(input.error) ?? void 0;
9394
+ return {
9395
+ source: input.source,
9396
+ error_message: getStudioSaveErrorMessage(input.error),
9397
+ status_code: statusCode,
9398
+ file_path: input.filePath ?? input.targetSourceFile ?? void 0,
9399
+ mutation_type: input.mutationType ?? void 0,
9400
+ attempt,
9401
+ label: input.label ?? void 0,
9402
+ target_id: input.targetId ?? void 0,
9403
+ target_selector: input.targetSelector ?? void 0,
9404
+ target_source_file: input.targetSourceFile ?? void 0,
9405
+ block_detail: gsapBlockDetail(input.error)
9406
+ };
9407
+ }
9408
+ function gsapBlockDetail(error) {
9409
+ if (!(error instanceof Error) || error.name !== "GsapEditBlockedError") return void 0;
9410
+ const detail = error.detail;
9411
+ return typeof detail === "string" ? detail : void 0;
9412
+ }
9413
+ function trackStudioSaveFailure(input) {
9414
+ trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
9415
+ }
9416
+ function trackStudioEditBlocked(input) {
9417
+ trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
9418
+ }
9419
+ async function createStudioSaveHttpError(response, fallbackMessage, options = {}) {
9420
+ let body = "";
9421
+ try {
9422
+ body = await response.text();
9423
+ } catch {
9424
+ body = "";
9425
+ }
9426
+ const detail = body.trim().slice(0, 300);
9427
+ const message = detail ? `${fallbackMessage} (${response.status}): ${detail}` : `${fallbackMessage} (${response.status})`;
9428
+ return new StudioSaveHttpError(message, response.status, options);
9429
+ }
9430
+ async function retryStudioSave(operation, options = {}) {
9431
+ const retries = options.retries ?? 3;
9432
+ const baseDelayMs = options.baseDelayMs ?? 500;
9433
+ const maxDelayMs = options.maxDelayMs ?? 8e3;
9434
+ const jitterRatio = options.jitterRatio ?? 0.25;
9435
+ const random = options.random ?? Math.random;
9436
+ const shouldRetry = options.shouldRetry ?? isRetryableStudioSaveError;
9437
+ const sleep = options.sleep ?? ((delayMs, signal) => new Promise((resolve, reject) => {
9438
+ throwIfStudioSaveAborted(signal);
9439
+ const onAbort = () => {
9440
+ globalThis.clearTimeout(timeout);
9441
+ reject(createStudioSaveAbortError());
9442
+ };
9443
+ const timeout = globalThis.setTimeout(() => {
9444
+ signal?.removeEventListener("abort", onAbort);
9445
+ resolve();
9446
+ }, delayMs);
9447
+ signal?.addEventListener("abort", onAbort, { once: true });
9448
+ }));
9449
+ const maxAttempts = retries + 1;
9450
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9451
+ try {
9452
+ throwIfStudioSaveAborted(options.signal);
9453
+ return await operation(attempt);
9454
+ } catch (error) {
9455
+ const failure = attachStudioSaveAttempt(error, attempt);
9456
+ if (attempt >= maxAttempts || !shouldRetry(failure, attempt)) throw failure;
9457
+ const retryIndex = attempt - 1;
9458
+ const exponentialDelay = Math.min(baseDelayMs * 2 ** retryIndex, maxDelayMs);
9459
+ const jitterSpan = exponentialDelay * jitterRatio;
9460
+ const jitteredDelay = Math.round(exponentialDelay + (random() * 2 - 1) * jitterSpan);
9461
+ const delayMs = Math.max(0, Math.min(maxDelayMs, jitteredDelay));
9462
+ await sleep(delayMs, options.signal);
9463
+ }
9464
+ }
9465
+ throw new Error("Save retry loop exited unexpectedly");
9466
+ }
9467
+
9468
+ // src/utils/typingTarget.ts
9469
+ function isTypingTarget(target) {
9470
+ const element = asElement(target);
9471
+ if (!element) return false;
9472
+ if (element.isContentEditable) return true;
9473
+ return element.closest(TYPING_SELECTOR) !== null;
9474
+ }
9475
+ function ownsPlainKeys(target) {
9476
+ return isTypingTarget(target) || asElement(target)?.closest(KEY_OWNING_CONTROLS) != null;
9477
+ }
9478
+ var KEY_OWNING_CONTROLS = "[role='slider'],video[controls],audio[controls]";
9479
+ var TYPING_SELECTOR = [
9480
+ "input",
9481
+ "textarea",
9482
+ "select",
9483
+ "[contenteditable]:not([contenteditable='false'])",
9484
+ "[role='textbox']",
9485
+ "[role='searchbox']",
9486
+ "[role='combobox']",
9487
+ // A switch is a button in the DOM, but the native checkbox it replaces was an
9488
+ // `input` and matched above. Without this row the shared Toggle would let a
9489
+ // global shortcut claim Space instead of flipping the control (KTD13).
9490
+ "[role='switch']",
9491
+ "[role='dialog'][aria-modal='true']",
9492
+ ".cm-editor"
9493
+ ].join(",");
9494
+ function asElement(target) {
9495
+ if (!target || typeof target !== "object") return null;
9496
+ const candidate = target;
9497
+ return typeof candidate.closest === "function" ? target : null;
9498
+ }
9499
+
9500
+ // src/utils/studioPendingEdits.ts
9501
+ var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
9502
+ var pendingEdits = /* @__PURE__ */ new Map();
9503
+ var historyClaims = () => 0;
9504
+ function setStudioPendingEditClaimClock(read2) {
9505
+ historyClaims = read2 ?? (() => 0);
9506
+ }
9507
+ var NOT_SAVED = () => Promise.resolve(false);
9508
+ var adopting = null;
9509
+ function adoptingStudioPendingEdit() {
9510
+ return adopting;
9511
+ }
9512
+ function waitForPostBlurEffects() {
9513
+ return new Promise((resolve) => setTimeout(resolve, 0));
9514
+ }
9515
+ function inspectDrainFailures(results) {
9516
+ let firstFailure;
9517
+ for (const result of results) {
9518
+ if (result.status !== "rejected") continue;
9519
+ if (result.reason instanceof StudioFileConflictError) return { conflict: result.reason };
9520
+ firstFailure ??= result;
9521
+ }
9522
+ return { firstFailure };
9523
+ }
9524
+ function focusedField() {
9525
+ const active = document.activeElement;
9526
+ return active instanceof HTMLElement && isTypingTarget(active) ? active : null;
9527
+ }
9528
+ function hasStudioPendingEdits() {
9529
+ return pendingEdits.size > 0 || focusedField() !== null;
9530
+ }
9531
+ function isStudioEditSaving() {
9532
+ return pendingEdits.size > 0;
9533
+ }
9534
+ function afterStudioPendingEdits(run) {
9535
+ let waiting = true;
9536
+ const check = () => {
9537
+ if (!waiting) return;
9538
+ if (isStudioEditSaving()) {
9539
+ void Promise.allSettled([...pendingEdits.keys()]).then(check);
9540
+ return;
9541
+ }
9542
+ waiting = false;
9543
+ run();
9544
+ };
9545
+ check();
9546
+ return () => {
9547
+ waiting = false;
9548
+ };
9549
+ }
9550
+ function trackStudioPendingEdit(result) {
9551
+ if (!result) return void 0;
9552
+ const promise = Promise.resolve(result);
9553
+ if (adopting) return promise;
9554
+ pendingEdits.set(promise, {
9555
+ revert: null,
9556
+ landed: NOT_SAVED,
9557
+ redraws: [],
9558
+ showAgain: null,
9559
+ claimsAtBegin: historyClaims()
9560
+ });
9561
+ promise.then(
9562
+ () => pendingEdits.delete(promise),
9563
+ () => pendingEdits.delete(promise)
9564
+ );
9565
+ return promise;
9566
+ }
9567
+ function trackedStudioEdit(edit) {
9568
+ return (...args) => {
9569
+ const result = edit(...args);
9570
+ if (result instanceof Promise) trackStudioPendingEdit(result);
9571
+ return result;
9572
+ };
9573
+ }
9574
+ function beginStudioPendingEdit(revert) {
9575
+ let settle;
9576
+ const promise = trackStudioPendingEdit(new Promise((resolve) => settle = resolve));
9577
+ const entry2 = pendingEdits.get(promise);
9578
+ entry2.revert = revert;
9579
+ let landed = Promise.resolve(false);
9580
+ let saved2 = false;
9581
+ entry2.landed = () => landed;
9582
+ const inFlight = {
9583
+ reverted: () => entry2.revert === null && revert !== null,
9584
+ within(run) {
9585
+ const outer = adopting;
9586
+ adopting = inFlight;
9587
+ try {
9588
+ return run();
9589
+ } finally {
9590
+ adopting = outer;
9591
+ }
9592
+ },
9593
+ drawUnlessUndone(draw) {
9594
+ if (inFlight.reverted()) entry2.redraws.push(draw);
9595
+ else draw();
9596
+ },
9597
+ drawKeepingUndone(draw) {
9598
+ if (!inFlight.reverted()) return draw();
9599
+ entry2.showAgain?.();
9600
+ try {
9601
+ return draw();
9602
+ } finally {
9603
+ entry2.showAgain = revert();
9604
+ }
9605
+ },
9606
+ markSaved: () => void (saved2 = true)
9607
+ };
9608
+ return {
9609
+ settle,
9610
+ reverted: inFlight.reverted,
9611
+ // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
9612
+ adopt(start) {
9613
+ try {
9614
+ const committed = inFlight.within(start);
9615
+ landed = Promise.resolve(committed).then(
9616
+ () => true,
9617
+ () => saved2
9618
+ );
9619
+ return committed;
9620
+ } catch (error) {
9621
+ settle();
9622
+ throw error;
9623
+ }
9624
+ }
9625
+ };
9626
+ }
9627
+ function paintBackNewestStudioPendingEdit() {
9628
+ const newest = [...pendingEdits.values()].at(-1);
9629
+ const revert = newest?.revert;
9630
+ if (!newest || !revert) return null;
9631
+ newest.revert = null;
9632
+ newest.showAgain = revert();
9633
+ return {
9634
+ showAgain: () => {
9635
+ newest.showAgain?.();
9636
+ for (const redraw of newest.redraws.splice(0)) redraw();
9637
+ },
9638
+ landed: newest.landed,
9639
+ claimsAtBegin: newest.claimsAtBegin
9640
+ };
9641
+ }
9642
+ function revertNewestStudioPendingEdit() {
9643
+ return paintBackNewestStudioPendingEdit()?.showAgain ?? null;
9644
+ }
9645
+ async function flushStudioPendingEdits({
9646
+ onlyCurrent = false
9647
+ } = {}) {
9648
+ const active = focusedField();
9649
+ if (active) {
9650
+ active.blur();
9651
+ await Promise.resolve();
9652
+ await waitForPostBlurEffects();
9653
+ }
9654
+ const detail = { promises: [] };
9655
+ window.dispatchEvent(
9656
+ new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
9657
+ );
9658
+ const current = onlyCurrent ? new Set(pendingEdits.keys()) : null;
9659
+ const waiting = () => [...pendingEdits.keys()].filter((edit) => !current || current.has(edit));
9660
+ let conflict;
9661
+ let firstFailure;
9662
+ while (detail.promises.length > 0 || waiting().length > 0) {
9663
+ const promises = [...detail.promises, ...waiting()];
9664
+ detail.promises = [];
9665
+ const batchFailures = inspectDrainFailures(await Promise.allSettled(promises));
9666
+ conflict ??= batchFailures.conflict;
9667
+ firstFailure ??= batchFailures.firstFailure;
9668
+ }
9669
+ if (conflict) return { status: "conflict", error: conflict };
9670
+ return firstFailure ? { status: "failed", error: firstFailure.reason } : { status: "clean" };
9671
+ }
9672
+ function addStudioPendingEditFlushListener(handler) {
9673
+ const listener2 = (event) => {
9674
+ const detail = event.detail;
9675
+ if (!detail?.promises) return;
9676
+ const promise = trackStudioPendingEdit(handler());
9677
+ if (promise) detail.promises.push(promise);
9678
+ };
9679
+ window.addEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
9680
+ return () => window.removeEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
9681
+ }
9682
+
9228
9683
  // src/player/components/timelineClipDragCommit.ts
9229
9684
  var keyOf6 = (e) => e.key ?? e.id;
9230
9685
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
@@ -9237,11 +9692,11 @@ function moveStoreUpdates(edit) {
9237
9692
  detach: audioGroup === null
9238
9693
  };
9239
9694
  }
9240
- function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coalesceMs) {
9695
+ function persistMoveEdits(edits, deps, coalesceKey, operation = "timing") {
9241
9696
  if (edits.length === 0) return Promise.resolve(true);
9242
9697
  edits = edits.map((e) => {
9243
- const start = clampToHostStart(e.element, e.updates.start);
9244
- return start === e.updates.start ? e : { ...e, updates: { ...e.updates, start } };
9698
+ const start2 = clampToHostStart(e.element, e.updates.start);
9699
+ return start2 === e.updates.start ? e : { ...e, updates: { ...e.updates, start: start2 } };
9245
9700
  });
9246
9701
  const { updateElement, onMoveElement, onMoveElements } = deps;
9247
9702
  if (!onMoveElements) {
@@ -9279,31 +9734,44 @@ function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coales
9279
9734
  }
9280
9735
  if (Object.keys(updates).length) updateElement(key2, updates);
9281
9736
  };
9282
- for (const e of edits) applyEdit(e);
9737
+ const applyEdits = (reassert = false) => batchElementUpdates(() => edits.forEach((e) => applyEdit(e, reassert)));
9738
+ applyEdits();
9283
9739
  const persistEdits = edits.map(
9284
9740
  (e) => e.persistTrack == null || e.persistTrack === e.updates.track ? e : { element: e.element, updates: { ...e.updates, track: e.persistTrack } }
9285
9741
  );
9286
- const persisted = onMoveElements ? onMoveElements(persistEdits, coalesceKey, operation, coalesceMs) : Promise.all(persistEdits.map((e) => Promise.resolve(onMoveElement?.(e.element, e.updates))));
9287
- return Promise.resolve(persisted).then(
9742
+ const restorePrev = () => batchElementUpdates(() => {
9743
+ for (const p of prev) {
9744
+ const { audioGroup, ...timing } = p.updates;
9745
+ const updates = {};
9746
+ if (isLatestTimelineOptimisticGesture(updateElement, revision, p.key)) {
9747
+ Object.assign(updates, timing);
9748
+ }
9749
+ if (isLatestTimelineOptimisticGesture(updateElement, membershipRevision, p.key, "membership"))
9750
+ updates.audioGroup = audioGroup;
9751
+ if (Object.keys(updates).length) updateElement(p.key, updates);
9752
+ }
9753
+ });
9754
+ const saving = onMoveElements ? beginStudioPendingEdit(() => {
9755
+ restorePrev();
9756
+ return () => applyEdits(true);
9757
+ }) : null;
9758
+ const start = () => onMoveElements ? onMoveElements(persistEdits, coalesceKey, operation) : Promise.all(
9759
+ persistEdits.map((e) => Promise.resolve(onMoveElement?.(e.element, e.updates)))
9760
+ );
9761
+ const persisted = saving ? saving.adopt(start) : start();
9762
+ const done = Promise.resolve(persisted).then(
9288
9763
  () => {
9289
- for (const e of edits) applyEdit(e, true);
9764
+ if (!saving?.reverted()) applyEdits(true);
9290
9765
  return true;
9291
9766
  },
9292
9767
  (error) => {
9293
- for (const p of prev) {
9294
- const { audioGroup, ...timing } = p.updates;
9295
- const updates = {};
9296
- if (isLatestTimelineOptimisticGesture(updateElement, revision, p.key)) {
9297
- Object.assign(updates, timing);
9298
- }
9299
- if (isLatestTimelineOptimisticGesture(updateElement, membershipRevision, p.key, "membership"))
9300
- updates.audioGroup = audioGroup;
9301
- if (Object.keys(updates).length) updateElement(p.key, updates);
9302
- }
9768
+ if (!saving?.reverted()) restorePrev();
9303
9769
  console.error("[Timeline] Failed to persist clip edits", error);
9304
9770
  return false;
9305
9771
  }
9306
9772
  );
9773
+ saving?.settle(done);
9774
+ return done;
9307
9775
  }
9308
9776
  function resolveMultiSelection(drag, deps) {
9309
9777
  const dragKey = keyOf6(drag.element);
@@ -9488,7 +9956,7 @@ function commitTrackInsert(drag, deps, multi) {
9488
9956
  deps
9489
9957
  ).catch(() => void 0);
9490
9958
  }
9491
- function commitZMirrorLaneMove(element, move, deps, coalesceKey, coalesceMs) {
9959
+ function commitZMirrorLaneMove(element, move, deps, coalesceKey) {
9492
9960
  if (move.kind === "move") {
9493
9961
  const edit = {
9494
9962
  element,
@@ -9496,14 +9964,14 @@ function commitZMirrorLaneMove(element, move, deps, coalesceKey, coalesceMs) {
9496
9964
  persistTrack: move.persistTrack
9497
9965
  };
9498
9966
  return refreshAfterDurableLaneMove(
9499
- persistMoveEdits([edit], deps, coalesceKey, "lane-reorder", coalesceMs),
9967
+ persistMoveEdits([edit], deps, coalesceKey, "lane-reorder"),
9500
9968
  deps
9501
9969
  );
9502
9970
  }
9503
9971
  const built = buildTrackInsertEdits(element, element.start, move.insertRow, null, deps);
9504
9972
  if (!built || built.edits.length === 0) return Promise.resolve(false);
9505
9973
  return refreshAfterDurableLaneMove(
9506
- persistMoveEdits(built.edits, deps, coalesceKey, "track-insert", coalesceMs),
9974
+ persistMoveEdits(built.edits, deps, coalesceKey, "track-insert"),
9507
9975
  deps
9508
9976
  );
9509
9977
  }
@@ -9539,13 +10007,15 @@ function commitTimelineGroupResize(session, updateElement, persist2) {
9539
10007
  updateElement,
9540
10008
  changes.map((change) => change.key)
9541
10009
  );
9542
- for (const change of changes) {
9543
- updateElement(change.key, {
9544
- start: change.start,
9545
- duration: change.duration,
9546
- playbackStart: change.playbackStart
9547
- });
9548
- }
10010
+ batchElementUpdates(() => {
10011
+ for (const change of changes) {
10012
+ updateElement(change.key, {
10013
+ start: change.start,
10014
+ duration: change.duration,
10015
+ playbackStart: change.playbackStart
10016
+ });
10017
+ }
10018
+ });
9549
10019
  if (!persist2) {
9550
10020
  rollbackLatestTimelineOptimisticGesture(
9551
10021
  updateElement,
@@ -9554,7 +10024,6 @@ function commitTimelineGroupResize(session, updateElement, persist2) {
9554
10024
  );
9555
10025
  return;
9556
10026
  }
9557
- const coalesceKey = `clip-group-resize:${changes.map((change) => change.key).join(":")}`;
9558
10027
  Promise.resolve(
9559
10028
  persist2(
9560
10029
  changes.map((change) => ({
@@ -9562,8 +10031,7 @@ function commitTimelineGroupResize(session, updateElement, persist2) {
9562
10031
  start: change.start,
9563
10032
  duration: change.duration,
9564
10033
  playbackStart: change.playbackStart
9565
- })),
9566
- { coalesceKey }
10034
+ }))
9567
10035
  )
9568
10036
  ).catch((error) => {
9569
10037
  rollbackLatestTimelineOptimisticGesture(
@@ -9684,7 +10152,7 @@ function mountTimelineClipDragGestureLifecycle({
9684
10152
  event.clientX - blocked.originClientX,
9685
10153
  event.clientY - blocked.originClientY
9686
10154
  );
9687
- const threshold = blocked.intent === "move" || blocked.intent === "edit-many" ? 4 : 2;
10155
+ const threshold = blocked.intent === "move" ? 4 : 2;
9688
10156
  if (!blocked.started && distance < threshold) return;
9689
10157
  if (!blocked.started) {
9690
10158
  blocked.started = true;
@@ -10273,8 +10741,6 @@ function useTimelineClipDrag({
10273
10741
  if (!element || !getClipCapabilities(element).canMove) return;
10274
10742
  event.preventDefault();
10275
10743
  event.stopImmediatePropagation();
10276
- if (exceedsHandEditLimit(element, false, null))
10277
- return void onBlockedEditAttemptRef.current?.(element, "edit-many");
10278
10744
  setShowPopover(false);
10279
10745
  setRangeSelectionRef.current?.(null);
10280
10746
  const row = keyboardPickupInsertRow(
@@ -10544,6 +11010,36 @@ function useTimelineDragPreviewState(draggedClip, resizingClip, selectedElementI
10544
11010
 
10545
11011
  // src/player/components/useTimelineClipContextMenu.ts
10546
11012
  import { useCallback as useCallback12 } from "react";
11013
+
11014
+ // src/player/components/timelineLinkSelection.ts
11015
+ function selectClipWithLinks(key2, altKey, setSelectedElementId) {
11016
+ const state = usePlayerStore.getState();
11017
+ if (altKey || !isLinkedSelectionOn()) {
11018
+ state.setSelection([key2], key2);
11019
+ return;
11020
+ }
11021
+ setSelectedElementId(key2);
11022
+ const { selectedElementIds, elements } = usePlayerStore.getState();
11023
+ const expanded = expandToLinkedMembers(selectedElementIds, elements);
11024
+ if (expanded.size > selectedElementIds.size) state.setSelection(expanded, key2);
11025
+ }
11026
+ function toggleClipWithLinks(key2, altKey) {
11027
+ const state = usePlayerStore.getState();
11028
+ const element = state.elements.find((el) => (el.key ?? el.id) === key2);
11029
+ const members = element ? linkedMembersOf(element, state.elements, !altKey && isLinkedSelectionOn()) : [];
11030
+ const memberKeys = members.length > 0 ? members.map((el) => el.key ?? el.id) : [key2];
11031
+ const next = new Set(state.selectedElementIds);
11032
+ const removing = next.has(key2);
11033
+ for (const memberKey of memberKeys) {
11034
+ if (removing) next.delete(memberKey);
11035
+ else next.add(memberKey);
11036
+ }
11037
+ state.setSelection(next, removing ? state.selectedElementId : key2);
11038
+ const primary = usePlayerStore.getState().selectedElementId;
11039
+ return state.elements.find((el) => (el.key ?? el.id) === primary) ?? null;
11040
+ }
11041
+
11042
+ // src/player/components/useTimelineClipContextMenu.ts
10547
11043
  function useClipContextMenu(onSelectElement, dismissGapMenu, setClipContextMenu) {
10548
11044
  const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
10549
11045
  const setSelectedElementId = usePlayerStore((s) => s.setSelectedElementId);
@@ -11620,391 +12116,6 @@ import { useCallback as useCallback17 } from "react";
11620
12116
 
11621
12117
  // src/contexts/DomEditContext.tsx
11622
12118
  import { useCallback as useCallback16, useContext as useContext3, useMemo as useMemo12, useRef as useRef17 } from "react";
11623
-
11624
- // src/utils/studioSaveDiagnostics.ts
11625
- var STUDIO_SAVE_ATTEMPT_PROPERTY = "__studioSaveAttempt";
11626
- var StudioSaveHttpError = class extends Error {
11627
- statusCode;
11628
- alreadyToasted;
11629
- constructor(message, statusCode, options = {}) {
11630
- super(message);
11631
- this.name = "StudioSaveHttpError";
11632
- this.statusCode = statusCode;
11633
- this.alreadyToasted = options.alreadyToasted ?? false;
11634
- }
11635
- };
11636
- var StudioSaveNetworkError = class extends Error {
11637
- constructor(message, options) {
11638
- super(message, options);
11639
- this.name = "StudioSaveNetworkError";
11640
- }
11641
- };
11642
- var StudioFileConflictError = class extends StudioSaveHttpError {
11643
- filePath;
11644
- currentVersion;
11645
- currentContent;
11646
- attemptedContent;
11647
- constructor(input) {
11648
- super(`Save conflict: ${input.filePath} changed outside this Studio session`, 409);
11649
- this.name = "StudioFileConflictError";
11650
- this.filePath = input.filePath;
11651
- this.currentVersion = input.currentVersion;
11652
- this.currentContent = input.currentContent;
11653
- this.attemptedContent = input.attemptedContent;
11654
- }
11655
- };
11656
- function readNumericProperty(value, key2) {
11657
- const record = value;
11658
- const property = record[key2];
11659
- return typeof property === "number" && Number.isFinite(property) ? property : void 0;
11660
- }
11661
- function createStudioSaveAbortError() {
11662
- if (typeof DOMException !== "undefined") return new DOMException("Save aborted", "AbortError");
11663
- const error = new Error("Save aborted");
11664
- error.name = "AbortError";
11665
- return error;
11666
- }
11667
- function throwIfStudioSaveAborted(signal) {
11668
- if (signal?.aborted) throw createStudioSaveAbortError();
11669
- }
11670
- function attachStudioSaveAttempt(error, attempt) {
11671
- if (!error || typeof error !== "object") return error;
11672
- try {
11673
- Object.defineProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY, {
11674
- value: attempt,
11675
- configurable: true
11676
- });
11677
- } catch {
11678
- }
11679
- return error;
11680
- }
11681
- function getStudioSaveErrorMessage(error) {
11682
- if (error instanceof Error && error.message) return error.message;
11683
- if (typeof error === "string" && error.trim()) return error;
11684
- return "Unknown save failure";
11685
- }
11686
- function markStudioSaveErrorAlreadyToasted(error) {
11687
- if (!error || typeof error !== "object") return error;
11688
- try {
11689
- Object.defineProperty(error, "alreadyToasted", { value: true, configurable: true });
11690
- } catch {
11691
- }
11692
- return error;
11693
- }
11694
- function isStudioSaveErrorAlreadyToasted(error) {
11695
- if (!error || typeof error !== "object") return false;
11696
- if (error.alreadyToasted === true) return true;
11697
- const cause = error.cause;
11698
- return cause !== error && isStudioSaveErrorAlreadyToasted(cause);
11699
- }
11700
- function getStudioSaveStatusCode(error) {
11701
- if (!error || typeof error !== "object") return void 0;
11702
- const direct = readNumericProperty(error, "statusCode") ?? readNumericProperty(error, "status") ?? readNumericProperty(error, "status_code");
11703
- if (direct != null) return direct;
11704
- const cause = error.cause;
11705
- if (cause && cause !== error) return getStudioSaveStatusCode(cause);
11706
- return void 0;
11707
- }
11708
- function getStudioSaveAttempt(error) {
11709
- if (!error || typeof error !== "object") return void 0;
11710
- const direct = readNumericProperty(error, STUDIO_SAVE_ATTEMPT_PROPERTY);
11711
- if (direct != null) return direct;
11712
- const cause = error.cause;
11713
- if (cause && cause !== error) return getStudioSaveAttempt(cause);
11714
- return void 0;
11715
- }
11716
- function isStudioSaveAbortError(error) {
11717
- return error instanceof Error && error.name === "AbortError";
11718
- }
11719
- function isRetryableStudioSaveError(error) {
11720
- if (isStudioSaveAbortError(error)) return false;
11721
- if (error instanceof StudioSaveNetworkError) return true;
11722
- const statusCode = getStudioSaveStatusCode(error);
11723
- if (statusCode == null) return false;
11724
- return statusCode === 408 || statusCode === 425 || statusCode === 429 || statusCode >= 500;
11725
- }
11726
- function buildStudioSaveFailureProperties(input) {
11727
- const statusCode = input.statusCode ?? getStudioSaveStatusCode(input.error) ?? null;
11728
- const attempt = input.attempt ?? getStudioSaveAttempt(input.error) ?? void 0;
11729
- return {
11730
- source: input.source,
11731
- error_message: getStudioSaveErrorMessage(input.error),
11732
- status_code: statusCode,
11733
- file_path: input.filePath ?? input.targetSourceFile ?? void 0,
11734
- mutation_type: input.mutationType ?? void 0,
11735
- attempt,
11736
- label: input.label ?? void 0,
11737
- target_id: input.targetId ?? void 0,
11738
- target_selector: input.targetSelector ?? void 0,
11739
- target_source_file: input.targetSourceFile ?? void 0,
11740
- block_detail: gsapBlockDetail(input.error)
11741
- };
11742
- }
11743
- function gsapBlockDetail(error) {
11744
- if (!(error instanceof Error) || error.name !== "GsapEditBlockedError") return void 0;
11745
- const detail = error.detail;
11746
- return typeof detail === "string" ? detail : void 0;
11747
- }
11748
- function trackStudioSaveFailure(input) {
11749
- trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
11750
- }
11751
- function trackStudioEditBlocked(input) {
11752
- trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
11753
- }
11754
- async function createStudioSaveHttpError(response, fallbackMessage, options = {}) {
11755
- let body = "";
11756
- try {
11757
- body = await response.text();
11758
- } catch {
11759
- body = "";
11760
- }
11761
- const detail = body.trim().slice(0, 300);
11762
- const message = detail ? `${fallbackMessage} (${response.status}): ${detail}` : `${fallbackMessage} (${response.status})`;
11763
- return new StudioSaveHttpError(message, response.status, options);
11764
- }
11765
- async function retryStudioSave(operation, options = {}) {
11766
- const retries = options.retries ?? 3;
11767
- const baseDelayMs = options.baseDelayMs ?? 500;
11768
- const maxDelayMs = options.maxDelayMs ?? 8e3;
11769
- const jitterRatio = options.jitterRatio ?? 0.25;
11770
- const random = options.random ?? Math.random;
11771
- const shouldRetry = options.shouldRetry ?? isRetryableStudioSaveError;
11772
- const sleep = options.sleep ?? ((delayMs, signal) => new Promise((resolve, reject) => {
11773
- throwIfStudioSaveAborted(signal);
11774
- const onAbort = () => {
11775
- globalThis.clearTimeout(timeout);
11776
- reject(createStudioSaveAbortError());
11777
- };
11778
- const timeout = globalThis.setTimeout(() => {
11779
- signal?.removeEventListener("abort", onAbort);
11780
- resolve();
11781
- }, delayMs);
11782
- signal?.addEventListener("abort", onAbort, { once: true });
11783
- }));
11784
- const maxAttempts = retries + 1;
11785
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
11786
- try {
11787
- throwIfStudioSaveAborted(options.signal);
11788
- return await operation(attempt);
11789
- } catch (error) {
11790
- const failure = attachStudioSaveAttempt(error, attempt);
11791
- if (attempt >= maxAttempts || !shouldRetry(failure, attempt)) throw failure;
11792
- const retryIndex = attempt - 1;
11793
- const exponentialDelay = Math.min(baseDelayMs * 2 ** retryIndex, maxDelayMs);
11794
- const jitterSpan = exponentialDelay * jitterRatio;
11795
- const jitteredDelay = Math.round(exponentialDelay + (random() * 2 - 1) * jitterSpan);
11796
- const delayMs = Math.max(0, Math.min(maxDelayMs, jitteredDelay));
11797
- await sleep(delayMs, options.signal);
11798
- }
11799
- }
11800
- throw new Error("Save retry loop exited unexpectedly");
11801
- }
11802
-
11803
- // src/utils/typingTarget.ts
11804
- function isTypingTarget(target) {
11805
- const element = asElement(target);
11806
- if (!element) return false;
11807
- if (element.isContentEditable) return true;
11808
- return element.closest(TYPING_SELECTOR) !== null;
11809
- }
11810
- function ownsPlainKeys(target) {
11811
- return isTypingTarget(target) || asElement(target)?.closest(KEY_OWNING_CONTROLS) != null;
11812
- }
11813
- var KEY_OWNING_CONTROLS = "[role='slider'],video[controls],audio[controls]";
11814
- var TYPING_SELECTOR = [
11815
- "input",
11816
- "textarea",
11817
- "select",
11818
- "[contenteditable]:not([contenteditable='false'])",
11819
- "[role='textbox']",
11820
- "[role='searchbox']",
11821
- "[role='combobox']",
11822
- // A switch is a button in the DOM, but the native checkbox it replaces was an
11823
- // `input` and matched above. Without this row the shared Toggle would let a
11824
- // global shortcut claim Space instead of flipping the control (KTD13).
11825
- "[role='switch']",
11826
- "[role='dialog'][aria-modal='true']",
11827
- ".cm-editor"
11828
- ].join(",");
11829
- function asElement(target) {
11830
- if (!target || typeof target !== "object") return null;
11831
- const candidate = target;
11832
- return typeof candidate.closest === "function" ? target : null;
11833
- }
11834
-
11835
- // src/utils/studioPendingEdits.ts
11836
- var STUDIO_FLUSH_PENDING_EDITS_EVENT = "hf-studio-flush-pending-edits";
11837
- var pendingEdits = /* @__PURE__ */ new Map();
11838
- var NOT_SAVED = () => Promise.resolve(false);
11839
- var adopting = null;
11840
- function adoptingStudioPendingEdit() {
11841
- return adopting;
11842
- }
11843
- function waitForPostBlurEffects() {
11844
- return new Promise((resolve) => setTimeout(resolve, 0));
11845
- }
11846
- function inspectDrainFailures(results) {
11847
- let firstFailure;
11848
- for (const result of results) {
11849
- if (result.status !== "rejected") continue;
11850
- if (result.reason instanceof StudioFileConflictError) return { conflict: result.reason };
11851
- firstFailure ??= result;
11852
- }
11853
- return { firstFailure };
11854
- }
11855
- function focusedField() {
11856
- const active = document.activeElement;
11857
- return active instanceof HTMLElement && isTypingTarget(active) ? active : null;
11858
- }
11859
- function hasStudioPendingEdits() {
11860
- return pendingEdits.size > 0 || focusedField() !== null;
11861
- }
11862
- function isStudioEditSaving() {
11863
- return pendingEdits.size > 0;
11864
- }
11865
- function afterStudioPendingEdits(run) {
11866
- let waiting = true;
11867
- const check = () => {
11868
- if (!waiting) return;
11869
- if (isStudioEditSaving()) {
11870
- void Promise.allSettled([...pendingEdits.keys()]).then(check);
11871
- return;
11872
- }
11873
- waiting = false;
11874
- run();
11875
- };
11876
- check();
11877
- return () => {
11878
- waiting = false;
11879
- };
11880
- }
11881
- function trackStudioPendingEdit(result) {
11882
- if (!result) return void 0;
11883
- const promise = Promise.resolve(result);
11884
- if (adopting) return promise;
11885
- pendingEdits.set(promise, { revert: null, landed: NOT_SAVED, redraws: [], showAgain: null });
11886
- promise.then(
11887
- () => pendingEdits.delete(promise),
11888
- () => pendingEdits.delete(promise)
11889
- );
11890
- return promise;
11891
- }
11892
- function trackedStudioEdit(edit) {
11893
- return (...args) => {
11894
- const result = edit(...args);
11895
- if (result instanceof Promise) trackStudioPendingEdit(result);
11896
- return result;
11897
- };
11898
- }
11899
- function beginStudioPendingEdit(revert) {
11900
- let settle;
11901
- const promise = trackStudioPendingEdit(new Promise((resolve) => settle = resolve));
11902
- const entry2 = pendingEdits.get(promise);
11903
- entry2.revert = revert;
11904
- let landed = Promise.resolve(false);
11905
- let saved2 = false;
11906
- entry2.landed = () => landed;
11907
- const inFlight = {
11908
- reverted: () => entry2.revert === null && revert !== null,
11909
- within(run) {
11910
- const outer = adopting;
11911
- adopting = inFlight;
11912
- try {
11913
- return run();
11914
- } finally {
11915
- adopting = outer;
11916
- }
11917
- },
11918
- drawUnlessUndone(draw) {
11919
- if (inFlight.reverted()) entry2.redraws.push(draw);
11920
- else draw();
11921
- },
11922
- drawKeepingUndone(draw) {
11923
- if (!inFlight.reverted()) return draw();
11924
- entry2.showAgain?.();
11925
- try {
11926
- return draw();
11927
- } finally {
11928
- entry2.showAgain = revert();
11929
- }
11930
- },
11931
- markSaved: () => void (saved2 = true)
11932
- };
11933
- return {
11934
- settle,
11935
- reverted: inFlight.reverted,
11936
- // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
11937
- adopt(start) {
11938
- try {
11939
- const committed = inFlight.within(start);
11940
- landed = Promise.resolve(committed).then(
11941
- () => true,
11942
- () => saved2
11943
- );
11944
- return committed;
11945
- } catch (error) {
11946
- settle();
11947
- throw error;
11948
- }
11949
- }
11950
- };
11951
- }
11952
- function paintBackNewestStudioPendingEdit() {
11953
- const newest = [...pendingEdits.values()].at(-1);
11954
- const revert = newest?.revert;
11955
- if (!newest || !revert) return null;
11956
- newest.revert = null;
11957
- newest.showAgain = revert();
11958
- return {
11959
- showAgain: () => {
11960
- newest.showAgain?.();
11961
- for (const redraw of newest.redraws.splice(0)) redraw();
11962
- },
11963
- landed: newest.landed
11964
- };
11965
- }
11966
- function revertNewestStudioPendingEdit() {
11967
- return paintBackNewestStudioPendingEdit()?.showAgain ?? null;
11968
- }
11969
- async function flushStudioPendingEdits({
11970
- onlyCurrent = false
11971
- } = {}) {
11972
- const active = focusedField();
11973
- if (active) {
11974
- active.blur();
11975
- await Promise.resolve();
11976
- await waitForPostBlurEffects();
11977
- }
11978
- const detail = { promises: [] };
11979
- window.dispatchEvent(
11980
- new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
11981
- );
11982
- const current = onlyCurrent ? new Set(pendingEdits.keys()) : null;
11983
- const waiting = () => [...pendingEdits.keys()].filter((edit) => !current || current.has(edit));
11984
- let conflict;
11985
- let firstFailure;
11986
- while (detail.promises.length > 0 || waiting().length > 0) {
11987
- const promises = [...detail.promises, ...waiting()];
11988
- detail.promises = [];
11989
- const batchFailures = inspectDrainFailures(await Promise.allSettled(promises));
11990
- conflict ??= batchFailures.conflict;
11991
- firstFailure ??= batchFailures.firstFailure;
11992
- }
11993
- if (conflict) return { status: "conflict", error: conflict };
11994
- return firstFailure ? { status: "failed", error: firstFailure.reason } : { status: "clean" };
11995
- }
11996
- function addStudioPendingEditFlushListener(handler) {
11997
- const listener2 = (event) => {
11998
- const detail = event.detail;
11999
- if (!detail?.promises) return;
12000
- const promise = trackStudioPendingEdit(handler());
12001
- if (promise) detail.promises.push(promise);
12002
- };
12003
- window.addEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
12004
- return () => window.removeEventListener(STUDIO_FLUSH_PENDING_EDITS_EVENT, listener2);
12005
- }
12006
-
12007
- // src/contexts/DomEditContext.tsx
12008
12119
  import { jsx as jsx19 } from "react/jsx-runtime";
12009
12120
  var SAVES_AN_EDIT = {
12010
12121
  handleTimelineElementSelect: false,
@@ -17284,6 +17395,14 @@ function applySoftReloadFinalization(iframe, currentTime) {
17284
17395
  function timelineKeys(scriptText) {
17285
17396
  return [...scriptText.matchAll(/__timelines\s*\[\s*["'`]([^"'`]+)["'`]\s*\]/g)].map((m) => m[1]).filter((key2) => key2 !== "__proxied");
17286
17397
  }
17398
+ function scriptsRegistering(scripts, keys) {
17399
+ return scripts.filter((script) => {
17400
+ const text = script.textContent || "";
17401
+ return keys.some(
17402
+ (key2) => text.includes(`__timelines["${key2}"]`) || text.includes(`__timelines['${key2}']`)
17403
+ );
17404
+ });
17405
+ }
17287
17406
  function applySoftReload(iframe, scriptText, options = {}) {
17288
17407
  const { onAsyncFailure, currentTimeOverride, authoredHtml } = options;
17289
17408
  if (!iframe || !scriptText) return "cannot-soft-reload";
@@ -17296,12 +17415,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
17296
17415
  if (targetKeys.length === 0) return "cannot-soft-reload";
17297
17416
  const gsapScripts = findGsapScriptElements(doc);
17298
17417
  if (gsapScripts.length === 0) return "cannot-soft-reload";
17299
- const staleScripts = gsapScripts.filter(
17300
- (script) => targetKeys.some((key2) => {
17301
- const text = script.textContent || "";
17302
- return text.includes(`__timelines["${key2}"]`) || text.includes(`__timelines['${key2}']`);
17303
- })
17304
- );
17418
+ const staleScripts = scriptsRegistering(gsapScripts, targetKeys);
17305
17419
  if (gsapScripts.length > 1 && staleScripts.length === 0) return "cannot-soft-reload";
17306
17420
  const currentTime = currentTimeOverride ?? win.__player?.getTime?.() ?? 0;
17307
17421
  let deferredToAsync = false;
@@ -17540,25 +17654,190 @@ function requestGsapMutation(projectId, route, filePath, body) {
17540
17654
  }
17541
17655
  );
17542
17656
  }
17543
- async function postGsapMutation(projectId, filePath, mutation, fallback) {
17657
+ async function postOwnedGsapMutation(route, projectId, filePath, body, fallback) {
17544
17658
  let response;
17545
17659
  try {
17546
- response = await requestGsapMutation(projectId, "gsap-mutations", filePath, mutation);
17660
+ response = await requestGsapMutation(projectId, route, filePath, body);
17547
17661
  } catch (error) {
17548
17662
  throw new GsapPreviewConvergenceError(`${fallback}: mutation outcome unknown`, {
17549
17663
  cause: error
17550
17664
  });
17551
17665
  }
17552
- const body = await response.json().catch(() => null);
17666
+ const payload = await response.json().catch(() => null);
17553
17667
  if (!response.ok) {
17554
- throw new GsapPreviewConvergenceError(readMutationError(body, fallback));
17668
+ throw new GsapPreviewConvergenceError(readMutationError(payload, fallback));
17555
17669
  }
17556
- return readMutationStatus(body);
17670
+ return readMutationStatus(payload);
17671
+ }
17672
+ function postGsapMutation(projectId, filePath, mutation, fallback) {
17673
+ return postOwnedGsapMutation("gsap-mutations", projectId, filePath, mutation, fallback);
17674
+ }
17675
+ function postGsapMutations(projectId, filePath, mutations, fallback) {
17676
+ return postOwnedGsapMutation(
17677
+ "gsap-mutations-batch",
17678
+ projectId,
17679
+ filePath,
17680
+ { mutations },
17681
+ fallback
17682
+ );
17557
17683
  }
17558
17684
 
17559
17685
  // src/hooks/timelineTimingSync.ts
17560
17686
  import { findTimelineScript } from "@hyperframes/core/gsap-parser-acorn";
17561
17687
  import { walkCompositionDescendants } from "@hyperframes/parsers/hf-ids";
17688
+
17689
+ // src/utils/gsapLiveRetime.ts
17690
+ import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
17691
+ import { hasExplicitTime } from "@hyperframes/parsers/gsap-writer-acorn";
17692
+ import { RUNTIME_FILLER } from "@hyperframes/core/runtime/protocol";
17693
+ var RERUN = { kind: "rerun" };
17694
+ var withoutLayout = (code) => code.replace(/\s+/g, " ").replace(/ ?([^\w$ ]) ?/g, "$1").trim();
17695
+ function untimedShape(animation) {
17696
+ const { id: _id, position: _p, resolvedStart: _s, duration: _d, ...rest } = animation;
17697
+ const provenance = rest.provenance && { ...rest.provenance, sourceRange: void 0 };
17698
+ return JSON.stringify(
17699
+ { ...rest, provenance },
17700
+ (_key, value) => typeof value === "string" && value.startsWith("__raw:") ? withoutLayout(value) : value
17701
+ );
17702
+ }
17703
+ var onTimeline = (animation) => !animation.global;
17704
+ var placedAlone = (animation) => hasExplicitTime(animation) && typeof animation.resolvedStart === "number";
17705
+ var lengthOf = (animation) => animation.method === "set" ? 0 : animation.durationUnresolved ? void 0 : animation.duration;
17706
+ var withoutNumbers = (script) => withoutLayout(script.replace(/(?<![\w$#.])\d*\.?\d+/g, "0"));
17707
+ function planLiveRetime(before, after) {
17708
+ if (withoutNumbers(before) !== withoutNumbers(after)) return RERUN;
17709
+ const keys = timelineKeys(after);
17710
+ if (keys.length !== 1 || timelineKeys(before).join() !== keys.join()) return RERUN;
17711
+ const was = parseGsapScriptAcorn(before).animations.filter(onTimeline);
17712
+ const now2 = parseGsapScriptAcorn(after).animations.filter(onTimeline);
17713
+ if (was.length !== now2.length) return RERUN;
17714
+ const tweens = [];
17715
+ for (const [index, next] of now2.entries()) {
17716
+ const prev = was[index];
17717
+ if (untimedShape(prev) !== untimedShape(next)) return RERUN;
17718
+ if (!placedAlone(next) || !placedAlone(prev)) return RERUN;
17719
+ const duration = lengthOf(next);
17720
+ const wasDuration = lengthOf(prev);
17721
+ if (duration === void 0 || wasDuration === void 0) return RERUN;
17722
+ tweens.push({
17723
+ selector: next.targetSelector,
17724
+ start: next.resolvedStart,
17725
+ duration,
17726
+ wasStart: prev.resolvedStart,
17727
+ wasDuration,
17728
+ moved: next.resolvedStart !== prev.resolvedStart || duration !== wasDuration,
17729
+ source: index
17730
+ });
17731
+ }
17732
+ tweens.sort((x, y) => x.wasStart - y.wasStart || x.source - y.source);
17733
+ return { kind: "retime", key: keys[0], after, tweens };
17734
+ }
17735
+ function planLiveRetimeFromPreview(iframe, after) {
17736
+ const doc = iframe?.contentDocument;
17737
+ const [key2] = timelineKeys(after);
17738
+ if (!doc || !key2) return RERUN;
17739
+ const [script, ...extra] = scriptsRegistering(findGsapScriptElements(doc), [key2]);
17740
+ if (!script || extra.length > 0) return RERUN;
17741
+ const plan = planLiveRetime(script.textContent ?? "", after);
17742
+ return plan.kind === "retime" ? { ...plan, script } : RERUN;
17743
+ }
17744
+ var same = (a, b) => Math.abs(a - b) < 1e-6;
17745
+ var targetsMatch = (tween, selector) => {
17746
+ const targets = tween.targets?.() ?? [];
17747
+ try {
17748
+ return targets.length > 0 && targets.every((t2) => t2.matches?.(selector) === true);
17749
+ } catch {
17750
+ return false;
17751
+ }
17752
+ };
17753
+ function pairedLiveTweens(timeline, plan) {
17754
+ const children = timeline.getChildren(false, true, false).filter((tween) => tween.data !== RUNTIME_FILLER);
17755
+ if (children.length !== plan.tweens.length) return null;
17756
+ const paired = plan.tweens.every((t2, i) => {
17757
+ const live = children[i];
17758
+ return targetsMatch(live, t2.selector) && same(live.startTime() - live.delay(), t2.wasStart) && same(live.duration(), t2.wasDuration);
17759
+ });
17760
+ return paired ? children : null;
17761
+ }
17762
+ var near2 = (a, b) => Math.abs(a - b) <= 2e-3;
17763
+ function sharesOneStretch(tweens) {
17764
+ const first = tweens[0];
17765
+ const last = tweens.at(-1);
17766
+ const longest = tweens.reduce((x, y) => y.wasDuration > x.wasDuration ? y : x);
17767
+ const spread = last.wasStart - first.wasStart;
17768
+ const stretch = spread > longest.wasDuration ? (last.start - first.start) / spread : longest.wasDuration > 0 ? longest.duration / longest.wasDuration : 1;
17769
+ const fits = tweens.every(
17770
+ (t2) => near2(t2.start, first.start + stretch * (t2.wasStart - first.wasStart)) && near2(t2.duration, stretch * t2.wasDuration)
17771
+ );
17772
+ return stretch > 0 && fits;
17773
+ }
17774
+ function inOrHostsSubComposition(target, ownId) {
17775
+ const element = target;
17776
+ const owner = element.closest?.("[data-composition-id]");
17777
+ return owner != null && owner.getAttribute("data-composition-id") !== ownId || element.querySelector?.("[data-composition-id]") != null;
17778
+ }
17779
+ var keepsOrder = (tweens) => tweens.every(
17780
+ (t2, k) => k === 0 || (tweens[k - 1].start - t2.start || tweens[k - 1].source - t2.source) < 0
17781
+ );
17782
+ function movesKeepEachElementsHistory(timeline, plan, children) {
17783
+ const byElement = /* @__PURE__ */ new Map();
17784
+ children.forEach((child, i) => {
17785
+ for (const target of child.targets?.() ?? []) {
17786
+ const indexes = byElement.get(target);
17787
+ if (indexes) indexes.push(i);
17788
+ else byElement.set(target, [i]);
17789
+ }
17790
+ });
17791
+ const moved = /* @__PURE__ */ new Set();
17792
+ for (const [target, indexes] of byElement) {
17793
+ if (!indexes.some((i) => plan.tweens[i].moved)) continue;
17794
+ if (inOrHostsSubComposition(target, plan.key)) return false;
17795
+ const tweens = indexes.map((i) => plan.tweens[i]);
17796
+ const shifted = tweens.every((t2) => t2.duration === t2.wasDuration);
17797
+ const undelayed = indexes.every((i) => !children[i].delay() && !children[i].repeatDelay?.());
17798
+ if (!keepsOrder(tweens) || !sharesOneStretch(tweens) || !(shifted || undelayed)) return false;
17799
+ moved.add(target);
17800
+ }
17801
+ return !timeline.getChildren(false, false, true).some(
17802
+ (sub) => sub.getChildren(true, true, false).some((tween) => tween.targets?.().some((t2) => moved.has(t2)))
17803
+ );
17804
+ }
17805
+ function applyLiveRetime(iframe, plan) {
17806
+ const win = iframe?.contentWindow;
17807
+ const timeline = win?.__timelines?.[plan.key];
17808
+ if (typeof timeline?.getChildren !== "function") return false;
17809
+ const children = pairedLiveTweens(timeline, plan);
17810
+ if (!children) return false;
17811
+ if (plan.tweens.some((t2) => t2.moved)) {
17812
+ if (!movesKeepEachElementsHistory(timeline, plan, children)) return false;
17813
+ const bySource = plan.tweens.map((t2, i) => ({ ...t2, tween: children[i] })).sort((a, b) => a.source - b.source);
17814
+ for (const { tween } of bySource) timeline.remove(tween);
17815
+ for (const { tween, start, duration } of bySource) {
17816
+ if (tween.duration() !== duration) tween.duration(duration);
17817
+ timeline.add(tween, start);
17818
+ }
17819
+ const at = timeline.time();
17820
+ timeline.progress(1e-4, true);
17821
+ timeline.totalTime(at, false);
17822
+ }
17823
+ plan.script.textContent = plan.after;
17824
+ return true;
17825
+ }
17826
+ function moveLiveTweens(iframe, scriptText, currentTime, reloadPreview) {
17827
+ const plan = planLiveRetimeFromPreview(iframe, scriptText);
17828
+ if (plan.kind !== "retime") return false;
17829
+ try {
17830
+ if (!applyLiveRetime(iframe, plan)) return false;
17831
+ } catch (error) {
17832
+ console.error("[Studio] Moving the live tweens threw; reloading the preview", error);
17833
+ reloadPreview();
17834
+ return true;
17835
+ }
17836
+ if (!applySoftReloadFinalization(iframe, currentTime)) reloadPreview();
17837
+ return true;
17838
+ }
17839
+
17840
+ // src/hooks/timelineTimingSync.ts
17562
17841
  async function readFileContent(projectId, targetPath) {
17563
17842
  if (targetPath.includes("\0") || targetPath.includes("..")) {
17564
17843
  throw new Error(`Unsafe path: ${targetPath}`);
@@ -17599,25 +17878,32 @@ function captureDurationRollback(iframe) {
17599
17878
  function rebindPreviewTiming(iframe, currentTime) {
17600
17879
  return applySoftReloadFinalization(iframe, currentTime);
17601
17880
  }
17602
- function syncTimingEditPreview(iframe, outcome, currentTime, reloadPreview, rebindWhenUnmutated, nestedFiles) {
17881
+ async function syncTimingEditPreview(iframe, outcome, projectId, reloadPreview, rebindWhenUnmutated) {
17882
+ const currentTime = () => usePlayerStore.getState().currentTime;
17603
17883
  if (!outcome.mutated && rebindWhenUnmutated) {
17604
- if (!rebindPreviewTiming(iframe, currentTime)) reloadPreview();
17884
+ if (!rebindPreviewTiming(iframe, currentTime())) reloadPreview();
17605
17885
  return;
17606
17886
  }
17607
- if (!iframe || !outcome.scriptText) {
17887
+ const { scriptText } = outcome;
17888
+ if (!iframe || !scriptText) {
17608
17889
  reloadPreview();
17609
17890
  return;
17610
17891
  }
17611
- const result = applySoftReload(iframe, outcome.scriptText, {
17892
+ if (moveLiveTweens(iframe, scriptText, currentTime(), reloadPreview)) return;
17893
+ const nestedFiles = await settleNestedReads(
17894
+ projectId ? readNestedFiles(iframe, scriptText, (path) => readFileContent(projectId, path)) : null
17895
+ );
17896
+ const result = applySoftReload(iframe, scriptText, {
17612
17897
  onAsyncFailure: reloadPreview,
17613
- currentTimeOverride: currentTime,
17898
+ currentTimeOverride: currentTime(),
17614
17899
  authoredHtml: outcome.after,
17615
17900
  nestedFiles
17616
17901
  });
17617
17902
  if (result === "cannot-soft-reload") reloadPreview();
17618
17903
  }
17904
+ var UNMUTATED = { mutated: false, scriptText: null };
17619
17905
  async function finishTimelineTimingFallback(input) {
17620
- let outcome = { mutated: false, scriptText: null };
17906
+ let outcome = UNMUTATED;
17621
17907
  if (input.gsapMutation) {
17622
17908
  try {
17623
17909
  outcome = await input.gsapMutation();
@@ -17627,24 +17913,22 @@ async function finishTimelineTimingFallback(input) {
17627
17913
  return;
17628
17914
  }
17629
17915
  }
17630
- const { projectId } = input;
17631
- const nestedFiles = await settleNestedReads(
17632
- projectId && outcome.scriptText ? readNestedFiles(
17633
- input.iframe,
17634
- outcome.scriptText,
17635
- (path) => readFileContent(projectId, path)
17636
- ) : null
17637
- );
17638
- syncTimingEditPreview(
17916
+ await syncTimingEditPreview(
17639
17917
  input.iframe,
17640
17918
  outcome,
17641
- usePlayerStore.getState().currentTime,
17919
+ input.projectId,
17642
17920
  input.reloadPreview,
17643
- input.rebindWhenUnmutated,
17644
- nestedFiles
17921
+ input.rebindWhenUnmutated
17645
17922
  );
17646
17923
  }
17647
- var GSAP_HISTORY_COALESCE_MS = 1e4;
17924
+ var UNTIL_NEXT_EDIT = Number.POSITIVE_INFINITY;
17925
+ var timingGestures = 0;
17926
+ function timingGestureStep(kind, given) {
17927
+ return {
17928
+ coalesceKey: given?.coalesceKey ?? `${kind}:${++timingGestures}`,
17929
+ coalesceMs: UNTIL_NEXT_EDIT
17930
+ };
17931
+ }
17648
17932
  async function rollbackMutatedFiles(projectId, ownedSteps, onError) {
17649
17933
  let convergenceRequired = false;
17650
17934
  for (let index = ownedSteps.length - 1; index >= 0; index -= 1) {
@@ -17675,7 +17959,7 @@ async function foldGsapMutationInQueue(input) {
17675
17959
  const ownedSteps = [];
17676
17960
  const runOwnedMutation = async (path, mutation) => {
17677
17961
  const pending2 = mutation();
17678
- if (!pending2) return { mutated: false, scriptText: null };
17962
+ if (!pending2) return UNMUTATED;
17679
17963
  const status2 = await pending2;
17680
17964
  if (!status2.mutated) return status2;
17681
17965
  if (status2.before === void 0 || status2.after === void 0) {
@@ -17720,7 +18004,7 @@ async function foldGsapMutationInQueue(input) {
17720
18004
  await input.recordEdit({
17721
18005
  label: input.label,
17722
18006
  coalesceKey: input.coalesceKey,
17723
- coalesceMs: GSAP_HISTORY_COALESCE_MS,
18007
+ coalesceMs: UNTIL_NEXT_EDIT,
17724
18008
  files
17725
18009
  });
17726
18010
  }
@@ -17737,37 +18021,31 @@ function foldGsapMutationIntoHistory(input) {
17737
18021
  () => foldGsapMutationInQueue(input)
17738
18022
  );
17739
18023
  }
18024
+ function shiftGsapMutation(elementId2, delta) {
18025
+ if (delta === 0 || !elementId2) return null;
18026
+ return { type: "shift-positions", targetSelector: `#${elementId2}`, delta };
18027
+ }
18028
+ function scaleGsapMutation(elementId2, oldStart, oldDuration, newStart, newDuration) {
18029
+ if (!elementId2 || oldDuration <= 0 || newDuration <= 0) return null;
18030
+ if (oldStart === newStart && oldDuration === newDuration) return null;
18031
+ return {
18032
+ type: "scale-positions",
18033
+ targetSelector: `#${elementId2}`,
18034
+ oldStart,
18035
+ oldDuration,
18036
+ newStart,
18037
+ newDuration
18038
+ };
18039
+ }
17740
18040
  async function shiftGsapPositions(projectId, filePath, elementId2, delta) {
17741
- if (delta === 0 || !elementId2) return { mutated: false, scriptText: null };
17742
- return postGsapMutation(
17743
- projectId,
17744
- filePath,
17745
- {
17746
- type: "shift-positions",
17747
- targetSelector: `#${elementId2}`,
17748
- delta
17749
- },
17750
- "shift-positions failed"
17751
- );
18041
+ const mutation = shiftGsapMutation(elementId2, delta);
18042
+ if (!mutation) return UNMUTATED;
18043
+ return postGsapMutation(projectId, filePath, mutation, "shift-positions failed");
17752
18044
  }
17753
18045
  async function scaleGsapPositions(projectId, filePath, elementId2, oldStart, oldDuration, newStart, newDuration) {
17754
- if (!elementId2 || oldDuration <= 0 || newDuration <= 0)
17755
- return { mutated: false, scriptText: null };
17756
- if (oldStart === newStart && oldDuration === newDuration)
17757
- return { mutated: false, scriptText: null };
17758
- return postGsapMutation(
17759
- projectId,
17760
- filePath,
17761
- {
17762
- type: "scale-positions",
17763
- targetSelector: `#${elementId2}`,
17764
- oldStart,
17765
- oldDuration,
17766
- newStart,
17767
- newDuration
17768
- },
17769
- "scale-positions failed"
17770
- );
18046
+ const mutation = scaleGsapMutation(elementId2, oldStart, oldDuration, newStart, newDuration);
18047
+ if (!mutation) return UNMUTATED;
18048
+ return postGsapMutation(projectId, filePath, mutation, "scale-positions failed");
17771
18049
  }
17772
18050
  function sdkTimingGsapSync(result) {
17773
18051
  const timelineText = (html2) => {
@@ -17844,11 +18122,17 @@ async function finishGroupTimingGsapFallback(input) {
17844
18122
  let mutated = false;
17845
18123
  let scriptText = null;
17846
18124
  let after;
18125
+ const byPath = /* @__PURE__ */ new Map();
17847
18126
  for (const change of input.changes) {
18127
+ const mutation = input.mutationFor(change);
18128
+ if (!mutation) continue;
17848
18129
  const changePath = input.resolveChangePath(change.element);
18130
+ byPath.set(changePath, [...byPath.get(changePath) ?? [], mutation]);
18131
+ }
18132
+ for (const [changePath, mutations] of byPath) {
17849
18133
  const status = await runOwnedMutation(
17850
18134
  changePath,
17851
- () => input.mutateChange(change, changePath)
18135
+ () => postGsapMutations(input.projectId, changePath, mutations, input.errorLabel)
17852
18136
  );
17853
18137
  mutated = mutated || status.mutated;
17854
18138
  if (changePath === activePath) ({ scriptText, after } = status);
@@ -18015,10 +18299,10 @@ function resolveResizePlaybackStart(original, target, element, updates) {
18015
18299
  )
18016
18300
  };
18017
18301
  }
18018
- function buildTimelineMoveTimingPatch(original, target, start, duration, track2, audioGroup) {
18302
+ function applyTimelineMoveAttributes(original, target, start, duration, track2, audioGroup) {
18019
18303
  if (!Number.isFinite(start) || !Number.isFinite(duration)) {
18020
18304
  console.warn(
18021
- `[Timeline] buildTimelineMoveTimingPatch: non-finite timing (start=${start}, duration=${duration}) \u2014 patch skipped`
18305
+ `[Timeline] applyTimelineMoveAttributes: non-finite timing (start=${start}, duration=${duration}) \u2014 patch skipped`
18022
18306
  );
18023
18307
  return original;
18024
18308
  }
@@ -18040,9 +18324,16 @@ function buildTimelineMoveTimingPatch(original, target, start, duration, track2,
18040
18324
  property: "data-audio-group",
18041
18325
  value: null
18042
18326
  });
18043
- return setCompositionDurationToContent(patched, furthestClipEndFromSource(patched));
18327
+ return patched;
18044
18328
  }
18045
- function buildTimelineResizeTimingPatch(original, target, element, updates) {
18329
+ function syncCompositionDurationToContent(source) {
18330
+ return setCompositionDurationToContent(source, furthestClipEndFromSource(source));
18331
+ }
18332
+ function buildTimelineMoveTimingPatch(original, target, start, duration, track2, audioGroup) {
18333
+ const patched = applyTimelineMoveAttributes(original, target, start, duration, track2, audioGroup);
18334
+ return !(Number.isFinite(start) && Number.isFinite(duration)) ? patched : syncCompositionDurationToContent(patched);
18335
+ }
18336
+ function applyTimelineResizeAttributes(original, target, element, updates) {
18046
18337
  const pbs = resolveResizePlaybackStart(original, target, element, updates);
18047
18338
  let patched = applyPatchByTarget(original, target, {
18048
18339
  type: "attribute",
@@ -18061,7 +18352,10 @@ function buildTimelineResizeTimingPatch(original, target, element, updates) {
18061
18352
  value: formatTimelineMediaOffset(pbs.value)
18062
18353
  });
18063
18354
  }
18064
- return setCompositionDurationToContent(patched, furthestClipEndFromSource(patched));
18355
+ return patched;
18356
+ }
18357
+ function buildTimelineResizeTimingPatch(...args) {
18358
+ return syncCompositionDurationToContent(applyTimelineResizeAttributes(...args));
18065
18359
  }
18066
18360
  async function persistTimelineEdit(input) {
18067
18361
  const targetPath = input.element.sourceFile || input.activeCompPath || "index.html";
@@ -18074,6 +18368,7 @@ async function persistTimelineEdit(input) {
18074
18368
  projectId: input.projectId,
18075
18369
  label: input.label,
18076
18370
  coalesceKey: input.coalesceKey,
18371
+ coalesceMs: input.coalesceMs,
18077
18372
  files: {
18078
18373
  [targetPath]: (current) => {
18079
18374
  const patched = input.buildPatches(current, patchTarget);
@@ -18113,7 +18408,12 @@ async function persistTimelineBatchEdit(input) {
18113
18408
  changesByPath.set(targetPath, [...changesByPath.get(targetPath) ?? [], change]);
18114
18409
  }
18115
18410
  const buildFile = (targetPath) => (original) => {
18116
- const next = patchTimelineChangesInSource(original, targetPath, changesByPath.get(targetPath));
18411
+ const patched = patchTimelineChangesInSource(
18412
+ original,
18413
+ targetPath,
18414
+ changesByPath.get(targetPath)
18415
+ );
18416
+ const next = syncCompositionDurationToContent(patched);
18117
18417
  if (next !== original) input.pendingTimelineEditPathRef.current.add(targetPath);
18118
18418
  return next;
18119
18419
  };
@@ -18521,6 +18821,7 @@ import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
18521
18821
  var TimelineClip = memo13(function TimelineClip2({
18522
18822
  el,
18523
18823
  pps,
18824
+ passengerStyle,
18524
18825
  clipY,
18525
18826
  clipHeight,
18526
18827
  isSelected: isSelected2,
@@ -18592,7 +18893,8 @@ var TimelineClip = memo13(function TimelineClip2({
18592
18893
  font: "inherit",
18593
18894
  padding: 0,
18594
18895
  textAlign: "left",
18595
- transform: isDragging ? "translateY(-1px)" : void 0
18896
+ transform: isDragging ? "translateY(-1px)" : void 0,
18897
+ ...passengerStyle
18596
18898
  };
18597
18899
  return /* @__PURE__ */ jsxs20(
18598
18900
  "button",
@@ -18728,6 +19030,7 @@ function TimelineCompactDiamonds({
18728
19030
  beatsActive,
18729
19031
  accentColor,
18730
19032
  isSelected: isSelected2,
19033
+ passengerStyle,
18731
19034
  currentTime,
18732
19035
  selectedKeyframes,
18733
19036
  rovingTargetId,
@@ -18748,7 +19051,8 @@ function TimelineCompactDiamonds({
18748
19051
  top: CLIP_Y,
18749
19052
  width,
18750
19053
  height: rowHeight - 2 * CLIP_Y,
18751
- zIndex: isSelected2 ? 11 : 6
19054
+ zIndex: isSelected2 ? 11 : 6,
19055
+ ...passengerStyle
18752
19056
  },
18753
19057
  children: /* @__PURE__ */ jsx34(
18754
19058
  TimelineClipDiamonds,
@@ -21850,7 +22154,6 @@ function LayerDisclosureRow({
21850
22154
  }
21851
22155
 
21852
22156
  // src/player/components/trackHeaderLaneValues.ts
21853
- import gsap2 from "gsap";
21854
22157
  import {
21855
22158
  classifyPropertyGroup as classifyPropertyGroup2
21856
22159
  } from "@hyperframes/core/gsap-parser";
@@ -21858,24 +22161,25 @@ function roundValue(value) {
21858
22161
  return String(Math.round(value * 100) / 100);
21859
22162
  }
21860
22163
  function easedProgress(progress, animation, ease) {
21861
- const resolved2 = ease ?? animation.keyframes?.easeEach ?? animation.ease;
21862
- if (!resolved2 || resolved2 === "none") return progress;
21863
- return gsap2.parseEase(resolved2)?.(progress) ?? progress;
22164
+ return easeFunction(keyframedTweenEases(animation).segment({ ease }))?.(progress) ?? progress;
21864
22165
  }
21865
- function propertyStops(animation, property) {
21866
- return (animation.keyframes?.keyframes ?? []).filter((keyframe) => property in keyframe.properties).map((keyframe) => ({
22166
+ function propertyStops(animation, property, start) {
22167
+ const stops = (animation.keyframes?.keyframes ?? []).filter((keyframe) => property in keyframe.properties).map((keyframe) => ({
21867
22168
  percentage: keyframe.percentage,
21868
22169
  value: keyframe.properties[property],
21869
22170
  ease: keyframe.ease
21870
22171
  }));
22172
+ const first = stops[0];
22173
+ if (!first || first.percentage <= 0 || typeof first.value !== "number") return stops;
22174
+ return [{ percentage: 0, value: start ?? PROPERTY_DEFAULTS[property] ?? 0 }, ...stops];
21871
22175
  }
21872
22176
  function interpolableEnds(before, after) {
21873
22177
  if (typeof before.value !== "number" || typeof after.value !== "number") return null;
21874
22178
  if (before.percentage === after.percentage) return null;
21875
22179
  return { from: before.value, to: after.value };
21876
22180
  }
21877
- function propertyValueAt(animation, property, tweenPercentage) {
21878
- const stops = propertyStops(animation, property);
22181
+ function propertyValueAt(animation, property, tweenPercentage, start) {
22182
+ const stops = propertyStops(animation, property, start);
21879
22183
  const before = stops.filter((stop2) => stop2.percentage <= tweenPercentage).at(-1);
21880
22184
  const after = stops.find((stop2) => stop2.percentage >= tweenPercentage);
21881
22185
  if (!before) return after?.value;
@@ -21885,7 +22189,7 @@ function propertyValueAt(animation, property, tweenPercentage) {
21885
22189
  const progress = (tweenPercentage - before.percentage) / (after.percentage - before.percentage);
21886
22190
  return ends.from + (ends.to - ends.from) * easedProgress(progress, animation, after.ease);
21887
22191
  }
21888
- function valuesAt(animation, group, tweenPercentage) {
22192
+ function valuesAt(animation, group, tweenPercentage, startValues = {}) {
21889
22193
  const propertyNames = /* @__PURE__ */ new Set();
21890
22194
  for (const keyframe of animation.keyframes?.keyframes ?? []) {
21891
22195
  for (const property of Object.keys(keyframe.properties)) {
@@ -21894,11 +22198,34 @@ function valuesAt(animation, group, tweenPercentage) {
21894
22198
  }
21895
22199
  const values = {};
21896
22200
  for (const property of propertyNames) {
21897
- const value = propertyValueAt(animation, property, tweenPercentage);
22201
+ const value = propertyValueAt(animation, property, tweenPercentage, startValues[property]);
21898
22202
  if (value !== void 0) values[property] = value;
21899
22203
  }
21900
22204
  return values;
21901
22205
  }
22206
+ var endValue = (animation, property) => animation.keyframes?.keyframes.findLast((keyframe) => property in keyframe.properties)?.properties[property] ?? animation.properties[property];
22207
+ function valuesBefore(animation, animations) {
22208
+ const start = resolveTweenStart(animation);
22209
+ if (start == null) return {};
22210
+ const endOf2 = (other) => (resolveTweenStart(other) ?? Infinity) + (other.method === "set" ? 0 : resolveTweenDuration(other));
22211
+ const index = animations.indexOf(animation);
22212
+ const finished = animations.filter((other, i) => {
22213
+ if (other.method === "from" || i === index) return false;
22214
+ return endOf2(other) < start || endOf2(other) === start && i < index;
22215
+ }).sort((a, b) => endOf2(a) - endOf2(b));
22216
+ const values = {};
22217
+ for (const other of finished) {
22218
+ const keyframes = other.keyframes?.keyframes ?? [{ properties: other.properties }];
22219
+ for (const property of new Set(
22220
+ keyframes.flatMap((keyframe) => Object.keys(keyframe.properties))
22221
+ )) {
22222
+ const value = endValue(other, property);
22223
+ if (typeof value === "number") values[property] = value;
22224
+ else delete values[property];
22225
+ }
22226
+ }
22227
+ return values;
22228
+ }
21902
22229
  function groupLabel(group, properties) {
21903
22230
  if (group === "visual" && ("opacity" in properties || "autoAlpha" in properties)) {
21904
22231
  return "Opacity";
@@ -21975,7 +22302,12 @@ function resolveLaneHeaderState(lane, currentTime, clipPercentage) {
21975
22302
  currentTime,
21976
22303
  clipPercentage
21977
22304
  );
21978
- const values = animation ? valuesAt(animation, lane.group, tweenPercentage) : {};
22305
+ const values = animation ? valuesAt(
22306
+ animation,
22307
+ lane.group,
22308
+ tweenPercentage,
22309
+ valuesBefore(animation, lane.elementAnimations)
22310
+ ) : {};
21979
22311
  return {
21980
22312
  navigation,
21981
22313
  values,
@@ -23078,7 +23410,7 @@ function isIntentBlocked(intent, capabilities, onResizeElement, onMoveElement) {
23078
23410
  if (capabilities.readOnly) return true;
23079
23411
  return intent === "move" ? Boolean(onMoveElement) : Boolean(onResizeElement);
23080
23412
  }
23081
- function resolvePointerDownAction(e, grabbed, capabilities, onResizeElement, onMoveElement) {
23413
+ function resolvePointerDownAction(e, capabilities, onResizeElement, onMoveElement) {
23082
23414
  if (e.button !== 0) return { kind: "ignore" };
23083
23415
  if (usePlayerStore.getState().activeTool === "razor") return { kind: "ignore" };
23084
23416
  if (isAdditiveClick(e)) return { kind: "ignore" };
@@ -23093,8 +23425,6 @@ function resolvePointerDownAction(e, grabbed, capabilities, onResizeElement, onM
23093
23425
  return { kind: "block", intent, rect };
23094
23426
  }
23095
23427
  if (!onMoveElement || !capabilities.canMove) return { kind: "ignore" };
23096
- if (exceedsHandEditLimit(grabbed, e.altKey, null))
23097
- return { kind: "block", intent: "edit-many", rect };
23098
23428
  return { kind: "move", rect };
23099
23429
  }
23100
23430
  function createClipGestureHandlers(el, elementKey3, previewElement, capabilities, deps) {
@@ -23117,17 +23447,6 @@ function createClipGestureHandlers(el, elementKey3, previewElement, capabilities
23117
23447
  const onResizeStart = (edge, e) => {
23118
23448
  if (!canStartResize(edge, e, capabilities, onResizeElement)) return;
23119
23449
  e.stopPropagation();
23120
- if (exceedsHandEditLimit(el, e.altKey, edge)) {
23121
- blockedClipRef.current = {
23122
- pointerId: e.pointerId,
23123
- element: el,
23124
- intent: "edit-many",
23125
- originClientX: e.clientX,
23126
- originClientY: e.clientY,
23127
- started: false
23128
- };
23129
- return;
23130
- }
23131
23450
  blockedClipRef.current = null;
23132
23451
  setShowPopover(false);
23133
23452
  setRangeSelection(null);
@@ -23145,7 +23464,7 @@ function createClipGestureHandlers(el, elementKey3, previewElement, capabilities
23145
23464
  });
23146
23465
  };
23147
23466
  const onPointerDown = (e) => {
23148
- const action = resolvePointerDownAction(e, el, capabilities, onResizeElement, onMoveElement);
23467
+ const action = resolvePointerDownAction(e, capabilities, onResizeElement, onMoveElement);
23149
23468
  if (action.kind === "ignore") return;
23150
23469
  if (action.kind === "block") {
23151
23470
  blockedClipRef.current = {
@@ -23352,6 +23671,14 @@ function TimelineTransitionOverlays({
23352
23671
 
23353
23672
  // src/player/components/TimelineLanes.tsx
23354
23673
  import { jsx as jsx57, jsxs as jsxs37 } from "react/jsx-runtime";
23674
+ function passengerStyleAt(offsetPx) {
23675
+ return {
23676
+ transform: `translateX(${offsetPx}px)`,
23677
+ opacity: 0.85,
23678
+ zIndex: 20,
23679
+ pointerEvents: "none"
23680
+ };
23681
+ }
23355
23682
  function TimelineLanes({
23356
23683
  pps,
23357
23684
  contentOrigin,
@@ -23658,7 +23985,7 @@ function TimelineLanes({
23658
23985
  isSelected2 || hoveredClip === clipKey || pinnedClipIdentities.has(clipKey)
23659
23986
  );
23660
23987
  const isPassenger = multiDragPreview != null && isMultiDragPassenger(clipKey, multiDragPreview);
23661
- const passengerOffsetPx = isPassenger ? multiDragPassengerOffsetPx(clipKey, pps, multiDragPreview) : 0;
23988
+ const passengerStyle = isPassenger ? passengerStyleAt(multiDragPassengerOffsetPx(clipKey, pps, multiDragPreview)) : void 0;
23662
23989
  const clipGestures = createClipGestureHandlers(
23663
23990
  el,
23664
23991
  elementKey3,
@@ -23690,6 +24017,7 @@ function TimelineLanes({
23690
24017
  },
23691
24018
  el: previewElement,
23692
24019
  pps,
24020
+ passengerStyle,
23693
24021
  clipY: CLIP_Y,
23694
24022
  clipHeight: clipBarHeight,
23695
24023
  isSelected: isSelected2,
@@ -23733,6 +24061,7 @@ function TimelineLanes({
23733
24061
  beatsActive: beatStripOnTrack,
23734
24062
  accentColor: clipStyle.accent,
23735
24063
  isSelected: isSelected2,
24064
+ passengerStyle,
23736
24065
  currentTime,
23737
24066
  selectedKeyframes,
23738
24067
  rovingTargetId: keyboard.rovingTargetId,
@@ -23754,6 +24083,7 @@ function TimelineLanes({
23754
24083
  clipDuration: previewElement.duration,
23755
24084
  clipLeftPx: previewElement.start * pps,
23756
24085
  clipWidthPx: Math.max(previewElement.duration * pps, 4),
24086
+ passengerStyle,
23757
24087
  accentColor: clipStyle.accent,
23758
24088
  isSelected: isSelected2,
23759
24089
  currentPercentage: previewElement.duration > 0 ? (currentTime - previewElement.start) / previewElement.duration * 100 : 0,
@@ -23769,31 +24099,11 @@ function TimelineLanes({
23769
24099
  },
23770
24100
  `${clipKey}-property-lanes`
23771
24101
  );
23772
- if (!isPassenger) {
23773
- return /* @__PURE__ */ jsxs37(Fragment18, { children: [
23774
- clip,
23775
- compactDiamonds,
23776
- propertyLanes
23777
- ] }, clipKey);
23778
- }
23779
- return /* @__PURE__ */ jsxs37(
23780
- "div",
23781
- {
23782
- className: "absolute inset-0",
23783
- style: {
23784
- transform: `translateX(${passengerOffsetPx}px)`,
23785
- opacity: 0.85,
23786
- zIndex: 20,
23787
- pointerEvents: "none"
23788
- },
23789
- children: [
23790
- clip,
23791
- compactDiamonds,
23792
- propertyLanes
23793
- ]
23794
- },
23795
- clipKey
23796
- );
24102
+ return /* @__PURE__ */ jsxs37(Fragment18, { children: [
24103
+ clip,
24104
+ compactDiamonds,
24105
+ propertyLanes
24106
+ ] }, clipKey);
23797
24107
  }),
23798
24108
  /* @__PURE__ */ jsx57(
23799
24109
  TimelineTransitionOverlays,
@@ -26149,9 +26459,9 @@ function mergeAudioInSource(source, input) {
26149
26459
  }
26150
26460
  var tagOf2 = (el) => el.tag.trim().toLowerCase();
26151
26461
  var keyOf11 = (el) => el.key ?? el.id;
26152
- var near2 = (a, b) => Math.abs(a - b) <= SYNC_TOLERANCE_S;
26462
+ var near3 = (a, b) => Math.abs(a - b) <= SYNC_TOLERANCE_S;
26153
26463
  function hasIdenticalTiming(a, b) {
26154
- return near2(a.start, b.start) && near2(a.duration, b.duration) && near2(a.playbackStart ?? 0, b.playbackStart ?? 0) && near2(a.playbackRate ?? 1, b.playbackRate ?? 1);
26464
+ return near3(a.start, b.start) && near3(a.duration, b.duration) && near3(a.playbackStart ?? 0, b.playbackStart ?? 0) && near3(a.playbackRate ?? 1, b.playbackRate ?? 1);
26155
26465
  }
26156
26466
  var sameAssetInScope = (a, b) => {
26157
26467
  const asset = mediaAssetIdentity(a);
@@ -26192,7 +26502,7 @@ function isPairInSync(a, b) {
26192
26502
  playbackRate: el.playbackRate ?? 1
26193
26503
  });
26194
26504
  const [x, y] = [timing(a), timing(b)];
26195
- return near2(x.playbackRate, y.playbackRate) && near2(sourceZeroTime(x), sourceZeroTime(y));
26505
+ return near3(x.playbackRate, y.playbackRate) && near3(sourceZeroTime(x), sourceZeroTime(y));
26196
26506
  }
26197
26507
  function pickDetachedAudioTrack(elements, window2) {
26198
26508
  const trackOf = (el) => el.authoredTrack ?? el.track;
@@ -27268,8 +27578,8 @@ async function decodeVideoThumbnail(request, signal, budgets = TIMELINE_VIEWPORT
27268
27578
  for (const time of timestamps) {
27269
27579
  const key2 = await keys.getKeyPacket(time, { metadataOnly: true });
27270
27580
  if (signal.aborted) return;
27271
- const near3 = key2 && key2.timestamp >= sourceStart && time - key2.timestamp <= maxKeyframeLead;
27272
- yield near3 ? key2.timestamp : time;
27581
+ const near4 = key2 && key2.timestamp >= sourceStart && time - key2.timestamp <= maxKeyframeLead;
27582
+ yield near4 ? key2.timestamp : time;
27273
27583
  }
27274
27584
  }
27275
27585
  const aspect = displayWidth / displayHeight;
@@ -29383,8 +29693,75 @@ var STUDIO_MANUAL_EDITS_WRAPPED_PROP = "__hfStudioManualEditsWrapped";
29383
29693
  var STUDIO_MANUAL_EDITS_PLAYBACK_FRAME_PROP = "__hfStudioManualEditsPlaybackFrame";
29384
29694
  var STUDIO_ROTATION_TRANSFORM_ORIGIN = "center center";
29385
29695
 
29386
- // src/hooks/gsapRuntimeKeyframes.ts
29696
+ // src/hooks/gsapRuntimeMotionPath.ts
29387
29697
  import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
29698
+ function isXY(p) {
29699
+ return !!p && typeof p.x === "number" && typeof p.y === "number";
29700
+ }
29701
+ function coordsFromMotionPath(mp) {
29702
+ if (!mp || typeof mp !== "object") return null;
29703
+ const obj = mp;
29704
+ const pathVal = Array.isArray(mp) ? mp : obj.path;
29705
+ if (!Array.isArray(pathVal)) return null;
29706
+ const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
29707
+ if (coords.length < 2) return null;
29708
+ const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
29709
+ const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
29710
+ return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
29711
+ }
29712
+ function readMotionPathTween(vars, runEase) {
29713
+ const mp = coordsFromMotionPath(vars.motionPath);
29714
+ const shape = mp && buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
29715
+ if (!shape) return null;
29716
+ const n = shape.waypoints.length;
29717
+ const keyframes = shape.waypoints.map((wp, i) => ({
29718
+ percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
29719
+ properties: { x: wp.x, y: wp.y }
29720
+ }));
29721
+ return { keyframes, arcPath: shape.arcPath, runEase: runEase ?? MOTION_PATH_RUN_EASE };
29722
+ }
29723
+
29724
+ // src/hooks/gsapRuntimeTweenIndex.ts
29725
+ function matchesElement(tween, el) {
29726
+ if (!tween.targets) return false;
29727
+ for (const t2 of tween.targets()) {
29728
+ if (t2 === el || el.id && t2.id === el.id) return true;
29729
+ }
29730
+ return false;
29731
+ }
29732
+ var tweenIndexes = null;
29733
+ function withTweenIndex(run) {
29734
+ const outer = tweenIndexes;
29735
+ tweenIndexes ??= /* @__PURE__ */ new WeakMap();
29736
+ try {
29737
+ return run();
29738
+ } finally {
29739
+ tweenIndexes = outer;
29740
+ }
29741
+ }
29742
+ function indexTweens(children) {
29743
+ const index = { byElement: /* @__PURE__ */ new Map(), byId: /* @__PURE__ */ new Map() };
29744
+ const add = (map, key2, tween) => {
29745
+ const list = map.get(key2);
29746
+ if (!list) map.set(key2, [tween]);
29747
+ else if (list.at(-1) !== tween) list.push(tween);
29748
+ };
29749
+ for (const tween of children) {
29750
+ for (const target of tween.targets?.() ?? []) {
29751
+ add(index.byElement, target, tween);
29752
+ const id = target.id;
29753
+ if (id) add(index.byId, id, tween);
29754
+ }
29755
+ }
29756
+ return index;
29757
+ }
29758
+ function tweensTargeting(timeline, el) {
29759
+ if (!timeline || typeof timeline !== "object" || !tweenIndexes)
29760
+ return (timeline?.getChildren?.(true) ?? []).filter((tween) => matchesElement(tween, el));
29761
+ let index = tweenIndexes.get(timeline);
29762
+ if (!index) tweenIndexes.set(timeline, index = indexTweens(timeline.getChildren?.(true) ?? []));
29763
+ return (el.id ? index.byId.get(el.id) : index.byElement.get(el)) ?? [];
29764
+ }
29388
29765
 
29389
29766
  // src/hooks/gsapParsedTween.ts
29390
29767
  var PARSED_NAME = { scale: "scaleX", rotate: "rotation" };
@@ -29547,14 +29924,13 @@ var BUILT_IN_EASES = [
29547
29924
  function withExactStepTimes(anim, tween) {
29548
29925
  const data = anim.keyframes;
29549
29926
  const parts = tween?.timeline?.getChildren?.() ?? [];
29550
- const total = tween?.timeline?.duration?.() ?? 0;
29551
- if (data?.format !== "object-array" || parts.length !== data.keyframes.length || !(total > 0))
29552
- return anim;
29553
- const ends = parts.map((part) => ((part.startTime?.() ?? 0) + (part.duration?.() ?? 0)) / total);
29554
- if (ends.some((end) => !Number.isFinite(end))) return anim;
29927
+ if (data?.format !== "object-array" || parts.length !== data.keyframes.length) return anim;
29928
+ const ends = parts.map((part) => (part.startTime?.() ?? 0) + (part.duration?.() ?? 0));
29929
+ const total = Math.max(...ends);
29930
+ if (!(total > 0) || ends.some((end) => !Number.isFinite(end))) return anim;
29555
29931
  const keyframes = data.keyframes.map((kf, i) => ({
29556
29932
  ...kf,
29557
- percentage: Math.round(ends[i] * 1e5) / 1e3
29933
+ percentage: Math.round(ends[i] / total * 1e5) / 1e3
29558
29934
  }));
29559
29935
  return { ...anim, keyframes: { ...data, keyframes } };
29560
29936
  }
@@ -29716,23 +30092,9 @@ function timelinesOf(iframe) {
29716
30092
  return null;
29717
30093
  }
29718
30094
  }
29719
- function isXY(p) {
29720
- return !!p && typeof p.x === "number" && typeof p.y === "number";
29721
- }
29722
30095
  function isZeroDurationSet(duration) {
29723
30096
  return !(duration > 0);
29724
30097
  }
29725
- function coordsFromMotionPath(mp) {
29726
- if (!mp || typeof mp !== "object") return null;
29727
- const obj = mp;
29728
- const pathVal = Array.isArray(mp) ? mp : obj.path;
29729
- if (!Array.isArray(pathVal)) return null;
29730
- const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
29731
- if (coords.length < 2) return null;
29732
- const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
29733
- const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
29734
- return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
29735
- }
29736
30098
  function flatTweenKeyframes(vars) {
29737
30099
  const properties = {};
29738
30100
  for (const [k, v] of Object.entries(vars)) {
@@ -29746,33 +30108,19 @@ function flatTweenKeyframes(vars) {
29746
30108
  { percentage: 100, properties }
29747
30109
  ];
29748
30110
  }
30111
+ var stringOr = (value) => typeof value === "string" ? value : void 0;
29749
30112
  function readTween(vars) {
30113
+ const runEase = stringOr(vars.ease);
29750
30114
  if (vars.keyframes && typeof vars.keyframes === "object") {
29751
30115
  const parsed = parsePercentageKeyframes(vars.keyframes);
29752
- if (parsed) return parsed;
29753
- }
29754
- const mp = coordsFromMotionPath(vars.motionPath);
29755
- if (mp) {
29756
- const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
29757
- if (shape) {
29758
- const n = shape.waypoints.length;
29759
- const keyframes = shape.waypoints.map((wp, i) => ({
29760
- percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
29761
- properties: { x: wp.x, y: wp.y }
29762
- }));
29763
- return { keyframes, arcPath: shape.arcPath };
29764
- }
30116
+ const keyframesEase = stringOr(vars.keyframes.ease);
30117
+ if (parsed) return { ...parsed, runEase: keyframesEase ?? runEase };
29765
30118
  }
30119
+ const path = readMotionPathTween(vars, runEase);
30120
+ if (path) return path;
29766
30121
  const flat = flatTweenKeyframes(vars);
29767
30122
  return flat ? { keyframes: flat } : null;
29768
30123
  }
29769
- function matchesElement(tween, el) {
29770
- if (!tween.targets) return false;
29771
- for (const t2 of tween.targets()) {
29772
- if (t2 === el || el.id && t2.id === el.id) return true;
29773
- }
29774
- return false;
29775
- }
29776
30124
  function tweenTiming(tween) {
29777
30125
  const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
29778
30126
  const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
@@ -29904,8 +30252,8 @@ var GSAP_TRANSFORM_KEYS = new Set(
29904
30252
  function gsapWritesChannels(el, channels) {
29905
30253
  const win = el.ownerDocument.defaultView;
29906
30254
  return Object.values(win?.__timelines ?? {}).some(
29907
- (tl) => (tl?.getChildren?.(true) ?? []).some(
29908
- (tween) => !!tween.vars && matchesElement(tween, el) && (channels.some((ch) => ch in tween.vars) || keyframeVarsCarryChannel(tween.vars, channels))
30255
+ (tl) => tweensTargeting(tl, el).some(
30256
+ (tween) => !!tween.vars && (channels.some((ch) => ch in tween.vars) || keyframeVarsCarryChannel(tween.vars, channels))
29909
30257
  )
29910
30258
  );
29911
30259
  }
@@ -29950,8 +30298,8 @@ function elementHasNonHoldTween(el, channels) {
29950
30298
  function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
29951
30299
  if (!timelines) return false;
29952
30300
  for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
29953
- for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
29954
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
30301
+ for (const tween of tweensTargeting(timelines[tlId], targetEl)) {
30302
+ if (!tween.vars) continue;
29955
30303
  const dur = typeof tween.duration === "function" ? tween.duration() : 0;
29956
30304
  if (isZeroDurationSet(dur)) continue;
29957
30305
  if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
@@ -29961,16 +30309,17 @@ function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
29961
30309
  }
29962
30310
  return false;
29963
30311
  }
29964
- function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
30312
+ function toClipRelative(read2, tweenStart, tweenDuration, clip) {
30313
+ const { keyframes, runEase } = read2;
29965
30314
  if (!clip || clip.duration <= 0) return keyframes;
29966
30315
  return keyframes.map((kf) => {
29967
- const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
30316
+ const abs = toAbsoluteTime(tweenStart, tweenDuration, timeAtProgress(runEase, kf.percentage));
29968
30317
  return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
29969
30318
  });
29970
30319
  }
29971
30320
  function buildEntry(read2, start, duration, clip) {
29972
30321
  return {
29973
- keyframes: toClipRelative(read2.keyframes, start, duration, clip),
30322
+ keyframes: toClipRelative(read2, start, duration, clip),
29974
30323
  tweenStart: start,
29975
30324
  tweenDuration: duration,
29976
30325
  ...read2.easeEach ? { easeEach: read2.easeEach } : {},
@@ -31971,7 +32320,7 @@ function NLEProvider({
31971
32320
  }, [projectId, setCompIdToSrc]);
31972
32321
  useEffect37(() => {
31973
32322
  if (compIdToSrc.size === 0) return;
31974
- const patchElements = (elements) => {
32323
+ const patchElements2 = (elements) => {
31975
32324
  const map = compIdToSrcRef.current;
31976
32325
  if (map.size === 0) return null;
31977
32326
  let patched2 = false;
@@ -31986,7 +32335,7 @@ function NLEProvider({
31986
32335
  });
31987
32336
  return patched2 ? updated : null;
31988
32337
  };
31989
- const patched = patchElements(usePlayerStore.getState().elements);
32338
+ const patched = patchElements2(usePlayerStore.getState().elements);
31990
32339
  if (patched) usePlayerStore.getState().setElements(patched);
31991
32340
  let patching = false;
31992
32341
  return usePlayerStore.subscribe((state, prev) => {
@@ -31994,7 +32343,7 @@ function NLEProvider({
31994
32343
  if (state.elements === prev.elements || state.elements.length === 0) return;
31995
32344
  if (state.elements.every((el) => el.compositionSrc)) return;
31996
32345
  patching = true;
31997
- const result = patchElements(state.elements);
32346
+ const result = patchElements2(state.elements);
31998
32347
  if (result) state.setElements(result);
31999
32348
  patching = false;
32000
32349
  });
@@ -36477,7 +36826,7 @@ function OffCanvasIndicators({
36477
36826
  const selectOffCanvas = async () => {
36478
36827
  const el = elements.current.get(r.key);
36479
36828
  if (!el) return;
36480
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-OL7GLSS4.js");
36829
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-IEVLO54Z.js");
36481
36830
  const acp = activeCompositionPathRef.current ?? "index.html";
36482
36831
  const sel = await resolveDomEditSelection2(el, {
36483
36832
  activeCompositionPath: acp,
@@ -43729,14 +44078,7 @@ function useMirrorLaneMoveCommit() {
43729
44078
  // NO readZIndex / onStackingPatches: see the hook doc — the lane→z
43730
44079
  // stacking sync must not re-trigger and fight the just-set z values.
43731
44080
  },
43732
- coalesceKey,
43733
- // Unbounded fold window: this record lands only AFTER the z persist's
43734
- // server round-trip resolved, so the gap between the gesture's two
43735
- // records exceeds editHistory's 300ms default under real latency and
43736
- // the fold would silently split into two undo entries. The shared key
43737
- // is unique per gesture (zReorderCoalesceKey's gesture seq), so the
43738
- // unbounded window can never merge two distinct user actions.
43739
- Number.POSITIVE_INFINITY
44081
+ coalesceKey
43740
44082
  );
43741
44083
  },
43742
44084
  [onMoveElements]
@@ -44869,7 +45211,23 @@ function TopologyLens({ iframeRef, activeCompositionPath }) {
44869
45211
  );
44870
45212
  }
44871
45213
 
45214
+ // src/utils/sameData.ts
45215
+ function sameData(a, b) {
45216
+ if (Object.is(a, b)) return true;
45217
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
45218
+ if ("nodeType" in a || "nodeType" in b || Array.isArray(a) !== Array.isArray(b)) return false;
45219
+ const keys = Object.keys(a);
45220
+ if (keys.length !== Object.keys(b).length) return false;
45221
+ return keys.every((key2) => sameData(a[key2], b[key2]));
45222
+ }
45223
+
44872
45224
  // src/hooks/gsapKeyframeCacheHelpers.ts
45225
+ function keepEqualEntries(before, draft) {
45226
+ for (const [key2, value] of draft) {
45227
+ const shown2 = before.get(key2);
45228
+ if (shown2 !== void 0 && shown2 !== value && sameData(shown2, value)) draft.set(key2, shown2);
45229
+ }
45230
+ }
44873
45231
  function sameEntries(before, after) {
44874
45232
  if (before.size !== after.size) return false;
44875
45233
  for (const [key2, value] of before) {
@@ -44884,6 +45242,8 @@ function publishKeyframeCache(edit) {
44884
45242
  gsapAnimations: new Map(gsapAnimations)
44885
45243
  };
44886
45244
  edit(draft);
45245
+ keepEqualEntries(keyframeCache, draft.keyframeCache);
45246
+ keepEqualEntries(gsapAnimations, draft.gsapAnimations);
44887
45247
  if (sameEntries(keyframeCache, draft.keyframeCache) && sameEntries(gsapAnimations, draft.gsapAnimations))
44888
45248
  return;
44889
45249
  usePlayerStore.setState({
@@ -44953,8 +45313,15 @@ function cachedElementIdsForFile(sourceFile, keyframeCache, gsapAnimations) {
44953
45313
  const sfPrefix = `${sourceFile}#`;
44954
45314
  const ids = /* @__PURE__ */ new Set();
44955
45315
  for (const key2 of [...keyframeCache.keys(), ...gsapAnimations.keys()]) {
44956
- if (!key2.startsWith(sfPrefix)) continue;
44957
- ids.add(key2.slice(sfPrefix.length));
45316
+ if (key2.startsWith(sfPrefix)) ids.add(key2.slice(sfPrefix.length));
45317
+ }
45318
+ if (sourceFile !== "index.html") return ids;
45319
+ for (const [key2, entry2] of keyframeCache) {
45320
+ const hash = key2.indexOf("#");
45321
+ const id = key2.slice(hash + 1);
45322
+ if (hash > 0 && !key2.startsWith(sfPrefix) && keyframeCache.get(sfPrefix + id) === entry2) {
45323
+ ids.delete(id);
45324
+ }
44958
45325
  }
44959
45326
  return ids;
44960
45327
  }
@@ -45362,10 +45729,9 @@ function computeCurrentPercentage(selection, animation) {
45362
45729
  return computeElementPercentage(usePlayerStore.getState().currentTime, selection, animation);
45363
45730
  }
45364
45731
  function parkPlayheadOnKeyframe(anim, pct) {
45365
- const ts = resolveTweenStart(anim);
45366
- const td = resolveTweenDuration(anim);
45367
- if (ts == null || !td || td <= 0) return;
45368
- usePlayerStore.getState().requestSeek(roundTo3(ts + pct / 100 * td));
45732
+ const time = percentageToAbsoluteForAnimation(pct, anim);
45733
+ if (time === null || resolveTweenDuration(anim) <= 0) return;
45734
+ usePlayerStore.getState().requestSeek(roundTo3(time));
45369
45735
  }
45370
45736
  async function replaceKeyframedPositionHold(selection, existingSet, properties, commitMutation) {
45371
45737
  const target = newTweenTarget(selection);
@@ -45740,7 +46106,7 @@ function findGsapPositionAnimation(animations, selector) {
45740
46106
  }
45741
46107
  function playheadProximity(a, currentTime) {
45742
46108
  const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
45743
- const dur = a.duration ?? 0;
46109
+ const dur = resolveTweenDuration(a);
45744
46110
  if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
45745
46111
  return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
45746
46112
  }
@@ -45748,10 +46114,12 @@ function statesValueAt(anim, time) {
45748
46114
  const duration = resolveTweenDuration(anim);
45749
46115
  if (!(duration > 0)) return false;
45750
46116
  const pct = (time - (resolveTweenStart(anim) ?? 0)) / duration * 100;
45751
- const near3 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
45752
- if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near3(kf.percentage));
45753
- if (anim.method === "from") return near3(0);
45754
- return near3(100) || anim.method === "fromTo" && near3(0);
46117
+ const near4 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
46118
+ const runEase = runEaseOf(anim);
46119
+ if (anim.keyframes)
46120
+ return anim.keyframes.keyframes.some((kf) => near4(timeAtProgress(runEase, kf.percentage)));
46121
+ if (anim.method === "from") return near4(0);
46122
+ return near4(100) || anim.method === "fromTo" && near4(0);
45755
46123
  }
45756
46124
  function pickClosestToPlayhead(anims) {
45757
46125
  if (anims.length <= 1) return anims[0] ?? null;
@@ -46054,8 +46422,6 @@ function tweensForThisElement(selection, animations) {
46054
46422
  }
46055
46423
 
46056
46424
  // src/hooks/gsapValueAtPlayhead.ts
46057
- var PERCENTAGE_SEGMENT_EASE = "power1.inOut";
46058
- var ARRAY_STEP_EASE = "none";
46059
46425
  var roundPct2 = (pct) => Math.round(pct * 1e3) / 1e3;
46060
46426
  var isLinear = (ease) => !ease || ease === "none" || ease === "linear";
46061
46427
  function refuse(reason) {
@@ -46070,7 +46436,7 @@ function normalize(edit) {
46070
46436
  const blocked = stepListBlock(anim);
46071
46437
  if (blocked) return { reason: blocked };
46072
46438
  return {
46073
- runEase: data.ease ?? anim.ease,
46439
+ runEase: runEaseOf(anim),
46074
46440
  keyframes: data.keyframes.map((kf) => ({
46075
46441
  ...kf,
46076
46442
  properties: { ...kf.properties },
@@ -47451,7 +47817,7 @@ function readAllAnimatedProperties(iframe, selector, anim, group) {
47451
47817
  // src/hooks/gsapDragPositionCommit.ts
47452
47818
  function buildTemporalArcKeyframes(anim, percentage, properties) {
47453
47819
  return [
47454
- ...(anim.keyframes?.keyframes ?? []).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
47820
+ ...linearArcKeyframes(anim).filter((keyframe) => Math.abs(keyframe.percentage - percentage) > KEYFRAME_PCT_MATCH).map((keyframe) => ({
47455
47821
  percentage: keyframe.percentage,
47456
47822
  properties: { ...keyframe.properties },
47457
47823
  ...keyframe.ease ? { ease: keyframe.ease } : {}
@@ -47459,6 +47825,35 @@ function buildTemporalArcKeyframes(anim, percentage, properties) {
47459
47825
  { percentage, properties }
47460
47826
  ].sort((a, b) => a.percentage - b.percentage);
47461
47827
  }
47828
+ function linearArcKeyframes(anim) {
47829
+ const runEase = runEaseOf(anim);
47830
+ return (anim.keyframes?.keyframes ?? []).map((keyframe) => ({
47831
+ ...keyframe,
47832
+ percentage: roundTo3(timeAtProgress(runEase, keyframe.percentage))
47833
+ }));
47834
+ }
47835
+ function buildExtendedKeyframes(anim, currentTime, position, sourceDuration = resolveTweenDuration(anim), writeEase = runEaseOf(anim)) {
47836
+ const oldStart = resolveTweenStart(anim) ?? 0;
47837
+ const oldDuration = sourceDuration;
47838
+ const newStart = Math.min(oldStart, currentTime);
47839
+ const newEnd = Math.max(oldStart + oldDuration, currentTime);
47840
+ const newDuration = roundTo3(newEnd - newStart);
47841
+ const oldRunEase = runEaseOf(anim);
47842
+ const toPct = (absoluteTime) => {
47843
+ if (newDuration <= 0) return 0;
47844
+ const progress = progressAtTime(writeEase, (absoluteTime - newStart) / newDuration * 100);
47845
+ return Math.max(0, Math.min(100, Math.round(progress * 10) / 10));
47846
+ };
47847
+ const stops = anim.keyframes?.keyframes ?? [];
47848
+ const rescaled = stops.map((stop2) => ({
47849
+ percentage: toPct(oldStart + timeAtProgress(oldRunEase, stop2.percentage) / 100 * oldDuration),
47850
+ properties: stop2.properties,
47851
+ ...stop2.ease ? { ease: stop2.ease } : {}
47852
+ }));
47853
+ const added = { percentage: toPct(currentTime), properties: position };
47854
+ const keyframes = [...rescaled, added].sort((a, b) => a.percentage - b.percentage);
47855
+ return { position: roundTo3(newStart), duration: newDuration, keyframes };
47856
+ }
47462
47857
  function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, iframe) {
47463
47858
  if (anim.arcPath?.enabled) return { status: "persisted" };
47464
47859
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
@@ -47487,12 +47882,38 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
47487
47882
  el.removeAttribute("data-hf-drag-initial-offset-y");
47488
47883
  };
47489
47884
  if (anim.arcPath?.enabled) {
47490
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
47885
+ const { activeKeyframePct, currentTime, setActiveKeyframePct } = usePlayerStore.getState();
47886
+ const tweenStart = resolveTweenStart(anim);
47887
+ const tweenDuration = resolveTweenDuration(anim);
47888
+ if (activeKeyframePct === null && tweenStart !== null && !isTimeWithinTween(currentTime, tweenStart, tweenDuration)) {
47889
+ const extended = buildExtendedKeyframes(
47890
+ anim,
47891
+ currentTime,
47892
+ { x: newX, y: newY },
47893
+ tweenDuration,
47894
+ "none"
47895
+ );
47896
+ await callbacks.commitMutation(
47897
+ selection,
47898
+ {
47899
+ type: "replace-with-keyframes",
47900
+ animationId: anim.id,
47901
+ targetSelector: anim.targetSelector,
47902
+ ...extended,
47903
+ ease: "none"
47904
+ },
47905
+ {
47906
+ label: "Move layer (new keyframe)",
47907
+ keyframeAction: "add",
47908
+ softReload: true,
47909
+ beforeReload: restoreOffset
47910
+ }
47911
+ );
47912
+ return { status: "persisted" };
47913
+ }
47491
47914
  const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
47492
47915
  const keyframes = anim.keyframes?.keyframes ?? [];
47493
- const pointIndex = keyframes.findIndex(
47494
- (kf) => Math.abs(kf.percentage - pct) <= KEYFRAME_PCT_MATCH
47495
- );
47916
+ const pointIndex = keyframes.findIndex((kf) => playsNear(anim, kf.percentage, pct));
47496
47917
  if (pointIndex >= 0) {
47497
47918
  await callbacks.commitMutation(
47498
47919
  selection,
@@ -47509,11 +47930,10 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
47509
47930
  parkPlayheadOnKeyframe(anim, pct);
47510
47931
  return { status: "persisted" };
47511
47932
  }
47512
- const tweenStart = resolveTweenStart(anim);
47513
- const tweenDuration = resolveEditableTweenDuration(anim, selection);
47514
47933
  if (tweenStart === null || tweenDuration <= 0 || keyframes.length < 2)
47515
47934
  return { status: "persisted" };
47516
- const temporalKeyframes = buildTemporalArcKeyframes(anim, pct, { x: newX, y: newY });
47935
+ const timePct = roundTo3(timeAtProgress(runEaseOf(anim), pct));
47936
+ const temporalKeyframes = buildTemporalArcKeyframes(anim, timePct, { x: newX, y: newY });
47517
47937
  await callbacks.commitMutation(
47518
47938
  selection,
47519
47939
  {
@@ -48010,19 +48430,7 @@ function useGsapAnimationsForElement(projectId, sourceFile, target, version, ifr
48010
48430
  if (isStaticPositionHold(anim)) continue;
48011
48431
  const kf = anim.keyframes ?? synthesizeFlatTweenKeyframes(anim);
48012
48432
  if (!kf) continue;
48013
- const tweenPos = anim.resolvedStart ?? (typeof anim.position === "number" ? anim.position : 0);
48014
- const tweenDur = anim.duration ?? elDuration;
48015
- for (const k of kf.keyframes) {
48016
- const absTime = toAbsoluteTime(tweenPos, tweenDur, k.percentage);
48017
- const clipPct = toClipPercentage(absTime, elStart, elDuration, k.percentage);
48018
- allKeyframes.push({
48019
- ...k,
48020
- percentage: clipPct,
48021
- tweenPercentage: k.percentage,
48022
- propertyGroup: anim.propertyGroup,
48023
- animationId: anim.id
48024
- });
48025
- }
48433
+ allKeyframes.push(...toClipKeyframes(kf.keyframes, anim, elStart, elDuration));
48026
48434
  format = kf.format;
48027
48435
  if (kf.ease) ease = kf.ease;
48028
48436
  if (kf.easeEach) easeEach = kf.easeEach;
@@ -48074,11 +48482,11 @@ function usePopulateKeyframeCacheForFile(projectId, sourceFile, version, iframeR
48074
48482
  const files = Array.from(
48075
48483
  /* @__PURE__ */ new Set([sourceFile, ...compositionSrcKey ? compositionSrcKey.split("|") : []])
48076
48484
  );
48077
- const sameData = loadedRef.current.dataKey === dataKey;
48078
- const stale = sameData ? files.filter((sf) => !loadedRef.current.files.has(sf)) : files;
48079
- const covered = sameData ? /* @__PURE__ */ new Set([...loadedRef.current.files, ...files]) : new Set(files);
48485
+ const sameData2 = loadedRef.current.dataKey === dataKey;
48486
+ const stale = sameData2 ? files.filter((sf) => !loadedRef.current.files.has(sf)) : files;
48487
+ const covered = sameData2 ? /* @__PURE__ */ new Set([...loadedRef.current.files, ...files]) : new Set(files);
48080
48488
  loadedRef.current = { dataKey, files: covered };
48081
- if (!sameData) pruneKeyframeCacheToFiles(files);
48489
+ if (!sameData2) pruneKeyframeCacheToFiles(files);
48082
48490
  if (stale.length === 0) return;
48083
48491
  runtimeScanDoneRef.current = "";
48084
48492
  const doc = iframeRef?.current?.contentDocument;
@@ -48155,7 +48563,7 @@ function resolveFlatTweenBoundaryRetime(opts) {
48155
48563
  };
48156
48564
  }
48157
48565
  function resolveKeyframeRetime(opts) {
48158
- const { keyframes, draggedTweenPct, tweenStart, tweenDuration, dropAbsTime } = opts;
48566
+ const { keyframes, draggedTweenPct, tweenStart, tweenDuration, dropAbsTime, runEase } = opts;
48159
48567
  if (tweenDuration <= 0) return { kind: "noop" };
48160
48568
  const tweenEnd = tweenStart + tweenDuration;
48161
48569
  const flatBoundary = resolveFlatTweenBoundaryRetime({
@@ -48167,7 +48575,8 @@ function resolveKeyframeRetime(opts) {
48167
48575
  });
48168
48576
  if (flatBoundary) return flatBoundary;
48169
48577
  if (dropAbsTime >= tweenStart - EPSILON_TIME && dropAbsTime <= tweenEnd + EPSILON_TIME) {
48170
- const toTweenPct = round33(clamp5((dropAbsTime - tweenStart) / tweenDuration * 100, 0, 100));
48578
+ const dropTimePct = clamp5((dropAbsTime - tweenStart) / tweenDuration * 100, 0, 100);
48579
+ const toTweenPct = round33(progressAtTime(runEase, dropTimePct));
48171
48580
  if (Math.abs(toTweenPct - draggedTweenPct) < NOOP_EPSILON_PCT2) return { kind: "noop" };
48172
48581
  return { kind: "move", toTweenPct };
48173
48582
  }
@@ -48185,8 +48594,9 @@ function resolveKeyframeRetime(opts) {
48185
48594
  }
48186
48595
  });
48187
48596
  const pctRemap = keyframes.map((kf, i) => {
48188
- const absTime = i === draggedIdx ? dropAbsTime : tweenStart + kf.percentage / 100 * tweenDuration;
48189
- return { from: kf.percentage, to: round33((absTime - newStart) / newDuration * 100) };
48597
+ const absTime = i === draggedIdx ? dropAbsTime : tweenStart + timeAtProgress(runEase, kf.percentage) / 100 * tweenDuration;
48598
+ const newTimePct = (absTime - newStart) / newDuration * 100;
48599
+ return { from: kf.percentage, to: round33(progressAtTime(runEase, newTimePct)) };
48190
48600
  });
48191
48601
  return {
48192
48602
  kind: "resize",
@@ -48365,14 +48775,15 @@ function useTimelineEditCallbacks({
48365
48775
  elements,
48366
48776
  domClipChildren
48367
48777
  );
48368
- const tweenDuration = resolveTweenDuration(anim, elDuration);
48778
+ const tweenDuration = resolveTweenDuration(anim);
48369
48779
  const dropAbsTime = elStart + toClipPct / 100 * elDuration;
48370
48780
  const decision = resolveKeyframeRetime({
48371
48781
  keyframes: anim.keyframes?.keyframes ?? [],
48372
48782
  draggedTweenPct: target.tweenPct,
48373
48783
  tweenStart,
48374
48784
  tweenDuration,
48375
- dropAbsTime
48785
+ dropAbsTime,
48786
+ runEase: runEaseOf(anim)
48376
48787
  });
48377
48788
  if (decision.kind === "move" && decision.toTweenPct != null) {
48378
48789
  return handleGsapMoveKeyframe(target.animId, target.tweenPct, decision.toTweenPct, sel);
@@ -73362,6 +73773,7 @@ function usePersistentEditHistory({ projectId }) {
73362
73773
  const projectIdRef = useRef125(projectId);
73363
73774
  projectIdRef.current = projectId;
73364
73775
  const [own] = useState111(createOwnHistory);
73776
+ const [pageKeyScope] = useState111(generateId);
73365
73777
  const refresh2 = useCallback100(async () => {
73366
73778
  if (!projectId) return;
73367
73779
  const seen = own.changes();
@@ -73379,6 +73791,10 @@ function usePersistentEditHistory({ projectId }) {
73379
73791
  own.clear();
73380
73792
  void refresh2().finally(() => setLoaded(true));
73381
73793
  }, [refresh2, own]);
73794
+ useEffect111(() => {
73795
+ setStudioPendingEditClaimClock(own.claimCount);
73796
+ return () => setStudioPendingEditClaimClock(null);
73797
+ }, [own]);
73382
73798
  const recordEdit = useCallback100(
73383
73799
  async ({ label, coalesceKey, coalesceMs, files, created = [] }) => {
73384
73800
  if (!projectId) return;
@@ -73388,7 +73804,10 @@ function usePersistentEditHistory({ projectId }) {
73388
73804
  label,
73389
73805
  paths,
73390
73806
  overwrote: await overwroteVersions(files),
73391
- ...coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }
73807
+ ...coalesceKey && {
73808
+ coalesceKey: `${pageKeyScope}:${coalesceKey}`,
73809
+ idleMs: coalesceMs ?? DEFAULT_COALESCE_MS
73810
+ }
73392
73811
  });
73393
73812
  const claimed = claimHeld(reply, label);
73394
73813
  if (claimed) {
@@ -73398,7 +73817,7 @@ function usePersistentEditHistory({ projectId }) {
73398
73817
  heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
73399
73818
  await refresh2();
73400
73819
  },
73401
- [projectId, refresh2, own]
73820
+ [projectId, refresh2, own, pageKeyScope]
73402
73821
  );
73403
73822
  const step = useCallback100(
73404
73823
  async (direction, callbacks) => {
@@ -74247,7 +74666,7 @@ function useFileManager({
74247
74666
  import { useCallback as useCallback106, useRef as useRef130, useState as useState114 } from "react";
74248
74667
 
74249
74668
  // src/utils/gsapUndoRestore.ts
74250
- import { isCompositionTemplate as isCompositionTemplate2 } from "@hyperframes/parsers/hf-ids";
74669
+ import { ensureHfIds as ensureHfIds2, isCompositionTemplate as isCompositionTemplate2 } from "@hyperframes/parsers/hf-ids";
74251
74670
  function elementIdentityKey(el) {
74252
74671
  const hfId = el.getAttribute("data-hf-id");
74253
74672
  if (hfId) return `hf:${hfId}`;
@@ -74383,8 +74802,10 @@ function scopeTargets(scope, keys, restoredByKey, previousByKey) {
74383
74802
  return targets;
74384
74803
  }
74385
74804
  function fileTargets(doc, path, isActive, file) {
74386
- const prevDoc = parseRestoreSource(file.previous);
74387
- const nextDoc = parseRestoreSource(file.restored);
74805
+ const previewStamped = doc.querySelector("[data-hf-id]") !== null;
74806
+ const stampedLikeThePreview = (html2) => previewStamped && !html2.includes("data-hf-id") ? ensureHfIds2(html2) : html2;
74807
+ const prevDoc = parseRestoreSource(stampedLikeThePreview(file.previous));
74808
+ const nextDoc = parseRestoreSource(stampedLikeThePreview(file.restored));
74388
74809
  const scripted = restoreScripted(prevDoc, nextDoc, isActive);
74389
74810
  if (scripted === null) return null;
74390
74811
  const keys = diffRestoreDocs(prevDoc, nextDoc);
@@ -74536,11 +74957,11 @@ function syncStoredElementAttribute(target, attr, value) {
74536
74957
  const fields = STORED_FIELD[attr]?.(value);
74537
74958
  if (!fields) return;
74538
74959
  const key2 = getTimelineElementIdentity(target);
74539
- const same = (element) => Object.keys(fields).every((k) => element[k] === fields[k]);
74960
+ const same2 = (element) => Object.keys(fields).every((k) => element[k] === fields[k]);
74540
74961
  usePlayerStore.setState((state) => {
74541
74962
  let changed = false;
74542
74963
  const elements = state.elements.map((element) => {
74543
- if (getTimelineElementIdentity(element) !== key2 || same(element)) return element;
74964
+ if (getTimelineElementIdentity(element) !== key2 || same2(element)) return element;
74544
74965
  changed = true;
74545
74966
  return { ...element, ...fields };
74546
74967
  });
@@ -74665,6 +75086,10 @@ function usePreviewPersistence({
74665
75086
  [applyCurrentStudioManualEditsToPreview, previewIframeRef]
74666
75087
  );
74667
75088
  applyStudioManualEditsToPreviewRef.current = applyStudioManualEditsToPreview;
75089
+ const heldPreviewDoc = useCallback106(() => {
75090
+ const doc = previewIframeRef.current?.contentDocument;
75091
+ return doc && isStudioManualEditGestureLiveIn(doc) ? doc : null;
75092
+ }, [previewIframeRef]);
74668
75093
  const syncHistoryPreviewAfterApply = useCallback106(
74669
75094
  async (restore2) => {
74670
75095
  const projectId = usePlayerStore.getState().timelineProjectId;
@@ -74676,6 +75101,15 @@ function usePreviewPersistence({
74676
75101
  (path) => projectId ? readProjectFileContent(projectId, path) : Promise.reject(new Error("No project is open to read nested files from."))
74677
75102
  )
74678
75103
  );
75104
+ const held = heldPreviewDoc();
75105
+ if (held) {
75106
+ const paths = restore2.paths ?? Object.keys(restore2.files ?? {});
75107
+ afterStudioManualEditGestures(held, () => {
75108
+ markScenesStale(previewIframeRef.current, paths);
75109
+ reloadPreview();
75110
+ });
75111
+ return;
75112
+ }
74679
75113
  const strategy = applyUndoRestoreToPreview(
74680
75114
  previewIframeRef.current,
74681
75115
  activeCompPathRef.current,
@@ -74690,21 +75124,23 @@ function usePreviewPersistence({
74690
75124
  }
74691
75125
  syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
74692
75126
  },
74693
- [previewIframeRef, activeCompPathRef, reloadPreview]
75127
+ [previewIframeRef, activeCompPathRef, reloadPreview, heldPreviewDoc]
74694
75128
  );
74695
75129
  const showHistoryRestoreNow = useCallback106(
74696
75130
  (files) => {
74697
- if (!domEditSaveQueueRef.current?.isIdle() || hasStudioPendingEdits()) return null;
75131
+ if (!domEditSaveQueueRef.current?.isIdle() || hasStudioPendingEdits() || heldPreviewDoc())
75132
+ return null;
74698
75133
  const iframe = previewIframeRef.current;
74699
75134
  const now2 = () => usePlayerStore.getState().currentTime;
74700
75135
  const putBack = showRestoreInPlace(iframe, activeCompPathRef.current, files, now2());
74701
75136
  if (!putBack) return null;
74702
75137
  return () => {
74703
- if (putBack(now2())) syncStoredAutomationFromPreview(iframe?.contentDocument ?? null);
75138
+ if (!heldPreviewDoc() && putBack(now2()))
75139
+ syncStoredAutomationFromPreview(iframe?.contentDocument ?? null);
74704
75140
  else void syncHistoryPreviewAfterApply({ paths: Object.keys(files) });
74705
75141
  };
74706
75142
  },
74707
- [previewIframeRef, activeCompPathRef, syncHistoryPreviewAfterApply]
75143
+ [previewIframeRef, activeCompPathRef, syncHistoryPreviewAfterApply, heldPreviewDoc]
74708
75144
  );
74709
75145
  useMountEffect(() => {
74710
75146
  void clearLegacyStudioMotionFile(_readOptionalProjectFile, _writeProjectFile);
@@ -74750,7 +75186,7 @@ function usePreviewDocumentVersion() {
74750
75186
  }
74751
75187
 
74752
75188
  // src/hooks/useTimelineEditing.ts
74753
- import { useCallback as useCallback123, useRef as useRef139 } from "react";
75189
+ import { useRef as useRef139 } from "react";
74754
75190
 
74755
75191
  // src/hooks/useRazorSplit.ts
74756
75192
  import { useCallback as useCallback109, useRef as useRef133 } from "react";
@@ -76050,7 +76486,6 @@ function applyRippleShifts(survivors, changes) {
76050
76486
  return newStart != null ? { ...te, start: newStart } : te;
76051
76487
  });
76052
76488
  }
76053
- var deleteGestureSeq = 0;
76054
76489
  function unlinkInSource(source, survivors) {
76055
76490
  const targets = survivors.map(buildPatchTarget).filter((target) => target !== null);
76056
76491
  return targets.length > 0 ? setLinkInSource(source, targets, null) : source;
@@ -76086,7 +76521,7 @@ function useTimelineDeleteOps({
76086
76521
  (candidate) => (candidate.sourceFile || activeCompPath || "index.html") === targetPath
76087
76522
  );
76088
76523
  try {
76089
- const coalesceKey = `main-track-ripple-delete:${deleteGestureSeq++}`;
76524
+ const step = timingGestureStep("main-track-ripple-delete");
76090
76525
  const deleteHistoryLabel = "Delete timeline clip";
76091
76526
  let rollbackDuration = () => {
76092
76527
  };
@@ -76095,7 +76530,7 @@ function useTimelineDeleteOps({
76095
76530
  projectId: pid,
76096
76531
  path: targetPath,
76097
76532
  label: deleteHistoryLabel,
76098
- coalesceKey,
76533
+ ...step,
76099
76534
  writeFile: writeProjectFile,
76100
76535
  recordEdit,
76101
76536
  rewrite: async (originalContent) => {
@@ -76152,8 +76587,7 @@ function useTimelineDeleteOps({
76152
76587
  if (rippleChanges) {
76153
76588
  try {
76154
76589
  await handleTimelineGroupMove(rippleChanges, {
76155
- coalesceKey,
76156
- coalesceMs: Number.POSITIVE_INFINITY,
76590
+ coalesceKey: step.coalesceKey,
76157
76591
  // Coalescing keeps the LAST entry's label; without this the undo
76158
76592
  // toast reads "Undid Move timeline clips" after a delete, naming
76159
76593
  // the ripple's mechanics instead of what the user actually did.
@@ -76281,7 +76715,7 @@ function planSyncEdit(edit) {
76281
76715
  property: "start",
76282
76716
  value: value2
76283
76717
  });
76284
- return setCompositionDurationToContent(moved, furthestClipEndFromSource(moved));
76718
+ return syncCompositionDurationToContent(moved);
76285
76719
  }
76286
76720
  };
76287
76721
  }
@@ -76925,12 +77359,6 @@ function allChangesSharePath(changes, activeCompPath) {
76925
77359
  if (!firstPath) return null;
76926
77360
  return changes.every((change) => targetPathFor(change.element, activeCompPath) === firstPath) ? firstPath : null;
76927
77361
  }
76928
- function moveCoalesceKey(changes) {
76929
- return `timeline-group-move:${changes.map((change) => change.element.hfId ?? change.element.id).join(",")}`;
76930
- }
76931
- function resizeCoalesceKey(changes) {
76932
- return `timeline-group-resize:${changes.map((change) => change.element.hfId ?? change.element.id).join(",")}`;
76933
- }
76934
77362
  function toSdkTimingChanges(changes, timingUpdate) {
76935
77363
  return changes.map(
76936
77364
  (change) => change.element.hfId ? { hfId: change.element.hfId, timingUpdate: timingUpdate(change) } : null
@@ -77053,8 +77481,7 @@ function useTimelineGroupEditing({
77053
77481
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
77054
77482
  const needsExtension = extendRootDurationIfNeeded(maxEnd);
77055
77483
  syncPreviewContentDuration(previewIframeRef.current);
77056
- const coalesceKey = options?.coalesceKey ?? moveCoalesceKey(changes);
77057
- const coalesceMs = options?.coalesceMs;
77484
+ const { coalesceKey, coalesceMs } = timingGestureStep("timeline-group-move", options);
77058
77485
  const label = options?.label ?? "Move timeline clips";
77059
77486
  return enqueueGroupOperation(label, async (projectId) => {
77060
77487
  await options?.beforeTiming;
@@ -77077,7 +77504,7 @@ function useTimelineGroupEditing({
77077
77504
  label,
77078
77505
  changes.map((change) => ({
77079
77506
  element: change.element,
77080
- buildPatches: (original, target) => buildTimelineMoveTimingPatch(
77507
+ buildPatches: (original, target) => applyTimelineMoveAttributes(
77081
77508
  original,
77082
77509
  target,
77083
77510
  toAuthoredStart(change.element, change.start),
@@ -77105,12 +77532,7 @@ function useTimelineGroupEditing({
77105
77532
  changes,
77106
77533
  sdkGsap: sdk?.sdkGsap,
77107
77534
  resolveChangePath: (element) => targetPathFor(element, activeCompPath),
77108
- mutateChange: (change, changePath) => {
77109
- const delta = change.start - change.element.start;
77110
- const domId = change.element.domId;
77111
- if (delta === 0 || !domId) return null;
77112
- return shiftGsapPositions(projectId, changePath, domId, delta);
77113
- }
77535
+ mutationFor: (change) => shiftGsapMutation(change.element.domId, change.start - change.element.start)
77114
77536
  });
77115
77537
  } finally {
77116
77538
  invalidateGsapCache?.();
@@ -77159,8 +77581,7 @@ function useTimelineGroupEditing({
77159
77581
  const rollbackDuration = captureDurationRollback(previewIframeRef.current);
77160
77582
  const needsExtension = extendRootDurationIfNeeded(maxEnd);
77161
77583
  syncPreviewContentDuration(previewIframeRef.current);
77162
- const coalesceKey = options?.coalesceKey ?? resizeCoalesceKey(changes);
77163
- const coalesceMs = options?.coalesceMs;
77584
+ const { coalesceKey, coalesceMs } = timingGestureStep("timeline-group-resize", options);
77164
77585
  return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
77165
77586
  await options?.beforeTiming;
77166
77587
  const sdk = await trySdkBatchPersist({
@@ -77181,7 +77602,7 @@ function useTimelineGroupEditing({
77181
77602
  "Resize timeline clips",
77182
77603
  changes.map((change) => ({
77183
77604
  element: change.element,
77184
- buildPatches: (original, target) => buildTimelineResizeTimingPatch(original, target, change.element, {
77605
+ buildPatches: (original, target) => applyTimelineResizeAttributes(original, target, change.element, {
77185
77606
  start: change.start,
77186
77607
  duration: change.duration,
77187
77608
  playbackStart: change.playbackStart
@@ -77205,20 +77626,13 @@ function useTimelineGroupEditing({
77205
77626
  changes,
77206
77627
  sdkGsap: sdk?.sdkGsap,
77207
77628
  resolveChangePath: (element) => targetPathFor(element, activeCompPath),
77208
- mutateChange: (change, changePath) => {
77209
- const domId = change.element.domId;
77210
- const timingChanged = change.start !== change.element.start || change.duration !== change.element.duration;
77211
- if (!timingChanged || !domId) return null;
77212
- return scaleGsapPositions(
77213
- projectId,
77214
- changePath,
77215
- domId,
77216
- toCompositionTime(change.element, change.element.start),
77217
- change.element.duration,
77218
- toCompositionTime(change.element, change.start),
77219
- change.duration
77220
- );
77221
- }
77629
+ mutationFor: (change) => scaleGsapMutation(
77630
+ change.element.domId,
77631
+ toCompositionTime(change.element, change.element.start),
77632
+ change.element.duration,
77633
+ toCompositionTime(change.element, change.start),
77634
+ change.duration
77635
+ )
77222
77636
  });
77223
77637
  } finally {
77224
77638
  invalidateGsapCache?.();
@@ -77245,54 +77659,28 @@ function useTimelineGroupEditing({
77245
77659
  return { handleTimelineGroupMove, handleTimelineGroupResize };
77246
77660
  }
77247
77661
 
77248
- // src/hooks/useBlockedTimelineEditToast.ts
77249
- import { useCallback as useCallback122, useRef as useRef138 } from "react";
77250
- var BLOCKED_TOAST_INTERVAL_MS = 1500;
77251
- function useBlockedTimelineEditToast(showToast) {
77252
- const lastAtRef = useRef138(0);
77253
- return useCallback122(
77254
- (_element, intent) => {
77255
- const now2 = Date.now();
77256
- if (now2 - lastAtRef.current < BLOCKED_TOAST_INTERVAL_MS) return;
77257
- lastAtRef.current = now2;
77258
- showToast(
77259
- intent === "edit-many" ? `Select ${MAX_HAND_EDIT_CLIPS} or fewer clips to move or resize by hand, or describe the change.` : "This clip can't be moved or resized from the timeline yet.",
77260
- "info"
77261
- );
77262
- },
77263
- [showToast]
77264
- );
77265
- }
77266
-
77267
- // src/hooks/useTimelineEditing.ts
77268
- function useTimelineEditing({
77269
- projectId,
77662
+ // src/hooks/useTimelineClipTimingEditing.ts
77663
+ import { useCallback as useCallback122 } from "react";
77664
+ function useTimelineClipTimingEditing({
77270
77665
  activeCompPath,
77271
77666
  timelineElements,
77272
77667
  showToast,
77273
77668
  writeProjectFile,
77274
- observeProjectFileVersion,
77275
77669
  recordEdit,
77276
77670
  reloadPreview,
77277
77671
  previewIframeRef,
77278
77672
  pendingTimelineEditPathRef,
77279
- uploadProjectFiles,
77280
77673
  isRecordingRef,
77281
77674
  sdkSession,
77282
77675
  publishSdkSession,
77283
77676
  forceReloadSdkSession,
77284
77677
  invalidateGsapCache,
77285
77678
  handleDomZIndexReorderCommitRef,
77286
- canEdit
77679
+ editQueueRef,
77680
+ projectIdRef
77287
77681
  }) {
77288
- const projectIdRef = useRef139(projectId);
77289
- projectIdRef.current = projectId;
77290
- const editQueueRef = useRef139(Promise.resolve());
77291
- const track2 = useTrackPendingTimelineEdit();
77292
- const checkEditable = useTimelineEditGate(canEdit, showToast);
77293
- const guard = useTimelineEditGuard(canEdit, showToast);
77294
- const enqueueEdit = useCallback123(
77295
- (element, label, buildPatches, coalesceKey) => {
77682
+ const enqueueEdit = useCallback122(
77683
+ (element, label, buildPatches, step) => {
77296
77684
  if (isRecordingRef?.current) {
77297
77685
  showToast("Cannot edit timeline while recording", "error");
77298
77686
  return Promise.resolve();
@@ -77309,7 +77697,7 @@ function useTimelineEditing({
77309
77697
  writeProjectFile,
77310
77698
  recordEdit,
77311
77699
  pendingTimelineEditPathRef,
77312
- coalesceKey
77700
+ ...step
77313
77701
  })
77314
77702
  ).then(() => {
77315
77703
  forceReloadSdkSession?.();
@@ -77326,26 +77714,12 @@ function useTimelineEditing({
77326
77714
  pendingTimelineEditPathRef,
77327
77715
  showToast,
77328
77716
  isRecordingRef,
77329
- forceReloadSdkSession
77717
+ forceReloadSdkSession,
77718
+ editQueueRef,
77719
+ projectIdRef
77330
77720
  ]
77331
77721
  );
77332
- const groupEditing = useTimelineGroupEditing({
77333
- activeCompPath,
77334
- editQueueRef,
77335
- forceReloadSdkSession,
77336
- invalidateGsapCache,
77337
- isRecordingRef,
77338
- pendingTimelineEditPathRef,
77339
- previewIframeRef,
77340
- projectIdRef,
77341
- recordEdit,
77342
- reloadPreview,
77343
- sdkSession,
77344
- publishSdkSession,
77345
- showToast,
77346
- writeProjectFile
77347
- });
77348
- const handleTimelineElementMove = useCallback123(
77722
+ const handleTimelineElementMove = useCallback122(
77349
77723
  // fallow-ignore-next-line complexity
77350
77724
  (element, updates) => {
77351
77725
  const commitMove = () => {
@@ -77375,16 +77749,16 @@ function useTimelineEditing({
77375
77749
  const needsExtension = extendRootDurationIfNeeded(updates.start + element.duration);
77376
77750
  syncPreviewContentDuration(previewIframeRef.current);
77377
77751
  const buildMovePatches = (original, target) => {
77378
- const track3 = trackChanged ? updates.track : void 0;
77752
+ const track2 = trackChanged ? updates.track : void 0;
77379
77753
  return buildTimelineMoveTimingPatch(
77380
77754
  original,
77381
77755
  target,
77382
77756
  authoredStart,
77383
77757
  element.duration,
77384
- track3
77758
+ track2
77385
77759
  );
77386
77760
  };
77387
- const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
77761
+ const step = timingGestureStep("timeline-move");
77388
77762
  const finishMoveGsapSync = (sdkGsap) => (
77389
77763
  // One GSAP sync per edit: the SDK commit's own (sdkGsap), else the server rewrite here.
77390
77764
  finishClipTimingFallback({
@@ -77394,14 +77768,14 @@ function useTimelineEditing({
77394
77768
  targetPath,
77395
77769
  domId: element.domId,
77396
77770
  label: "Move timeline clip",
77397
- coalesceKey,
77771
+ coalesceKey: step.coalesceKey,
77398
77772
  recordEdit,
77399
77773
  writeProjectFile,
77400
77774
  edit: { kind: "shift", delta: updates.start - element.start },
77401
77775
  sdkGsap
77402
77776
  }).finally(() => invalidateGsapCache?.())
77403
77777
  );
77404
- const moveFallback = () => enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(
77778
+ const moveFallback = () => enqueueEdit(element, "Move timeline clip", buildMovePatches, step).then(
77405
77779
  () => finishMoveGsapSync()
77406
77780
  );
77407
77781
  return reorderDone.then(() => {
@@ -77421,7 +77795,7 @@ function useTimelineEditing({
77421
77795
  readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
77422
77796
  publishSession: publishSdkSession
77423
77797
  },
77424
- { label: "Move timeline clip", coalesceKey, skipRefresh: true }
77798
+ { label: "Move timeline clip", ...step, skipRefresh: true }
77425
77799
  ).then((result) => {
77426
77800
  if (!cutoverCommittedOrThrow(result)) return moveFallback();
77427
77801
  return finishMoveGsapSync(sdkTimingGsapSync(result));
@@ -77448,10 +77822,11 @@ function useTimelineEditing({
77448
77822
  timelineElements,
77449
77823
  handleDomZIndexReorderCommitRef,
77450
77824
  showToast,
77451
- invalidateGsapCache
77825
+ invalidateGsapCache,
77826
+ projectIdRef
77452
77827
  ]
77453
77828
  );
77454
- const handleTimelineElementResize = useCallback123(
77829
+ const handleTimelineElementResize = useCallback122(
77455
77830
  // fallow-ignore-next-line complexity
77456
77831
  (element, updates) => {
77457
77832
  const authoredStart = toAuthoredStart(element, updates.start);
@@ -77472,7 +77847,7 @@ function useTimelineEditing({
77472
77847
  return buildTimelineResizeTimingPatch(original, target, element, updates);
77473
77848
  };
77474
77849
  const hasPbsAdjustment = updates.playbackStart != null || updates.start !== element.start && element.playbackStart != null;
77475
- const coalesceKey = `timeline-resize:${element.hfId ?? element.id}`;
77850
+ const step = timingGestureStep("timeline-resize");
77476
77851
  const finishResizeGsapSync = (sdkGsap) => finishClipTimingFallback({
77477
77852
  iframe: previewIframeRef.current,
77478
77853
  reloadPreview,
@@ -77480,7 +77855,7 @@ function useTimelineEditing({
77480
77855
  targetPath,
77481
77856
  domId: element.domId,
77482
77857
  label: "Resize timeline clip",
77483
- coalesceKey,
77858
+ coalesceKey: step.coalesceKey,
77484
77859
  recordEdit,
77485
77860
  writeProjectFile,
77486
77861
  edit: {
@@ -77490,7 +77865,7 @@ function useTimelineEditing({
77490
77865
  },
77491
77866
  sdkGsap
77492
77867
  }).finally(() => invalidateGsapCache?.());
77493
- const resizeFallback = () => enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(
77868
+ const resizeFallback = () => enqueueEdit(element, "Resize timeline clip", buildResizePatches, step).then(
77494
77869
  () => finishResizeGsapSync()
77495
77870
  );
77496
77871
  const persistDone = sdkSession && element.hfId && !element.link && !hasPbsAdjustment && !needsExtension ? sdkTimingPersist(
@@ -77508,7 +77883,7 @@ function useTimelineEditing({
77508
77883
  readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
77509
77884
  publishSession: publishSdkSession
77510
77885
  },
77511
- { label: "Resize timeline clip", coalesceKey, skipRefresh: true }
77886
+ { label: "Resize timeline clip", ...step, skipRefresh: true }
77512
77887
  ).then((result) => {
77513
77888
  if (!cutoverCommittedOrThrow(result)) return resizeFallback();
77514
77889
  return finishResizeGsapSync(sdkTimingGsapSync(result));
@@ -77529,9 +77904,90 @@ function useTimelineEditing({
77529
77904
  writeProjectFile,
77530
77905
  reloadPreview,
77531
77906
  showToast,
77532
- invalidateGsapCache
77907
+ invalidateGsapCache,
77908
+ projectIdRef
77533
77909
  ]
77534
77910
  );
77911
+ return { handleTimelineElementMove, handleTimelineElementResize };
77912
+ }
77913
+
77914
+ // src/hooks/useBlockedTimelineEditToast.ts
77915
+ import { useCallback as useCallback123, useRef as useRef138 } from "react";
77916
+ var BLOCKED_TOAST_INTERVAL_MS = 1500;
77917
+ function useBlockedTimelineEditToast(showToast) {
77918
+ const lastAtRef = useRef138(0);
77919
+ return useCallback123(
77920
+ (_element) => {
77921
+ const now2 = Date.now();
77922
+ if (now2 - lastAtRef.current < BLOCKED_TOAST_INTERVAL_MS) return;
77923
+ lastAtRef.current = now2;
77924
+ showToast("This clip can't be moved or resized from the timeline yet.", "info");
77925
+ },
77926
+ [showToast]
77927
+ );
77928
+ }
77929
+
77930
+ // src/hooks/useTimelineEditing.ts
77931
+ function useTimelineEditing({
77932
+ projectId,
77933
+ activeCompPath,
77934
+ timelineElements,
77935
+ showToast,
77936
+ writeProjectFile,
77937
+ observeProjectFileVersion,
77938
+ recordEdit,
77939
+ reloadPreview,
77940
+ previewIframeRef,
77941
+ pendingTimelineEditPathRef,
77942
+ uploadProjectFiles,
77943
+ isRecordingRef,
77944
+ sdkSession,
77945
+ publishSdkSession,
77946
+ forceReloadSdkSession,
77947
+ invalidateGsapCache,
77948
+ handleDomZIndexReorderCommitRef,
77949
+ canEdit
77950
+ }) {
77951
+ const projectIdRef = useRef139(projectId);
77952
+ projectIdRef.current = projectId;
77953
+ const editQueueRef = useRef139(Promise.resolve());
77954
+ const track2 = useTrackPendingTimelineEdit();
77955
+ const checkEditable = useTimelineEditGate(canEdit, showToast);
77956
+ const guard = useTimelineEditGuard(canEdit, showToast);
77957
+ const groupEditing = useTimelineGroupEditing({
77958
+ activeCompPath,
77959
+ editQueueRef,
77960
+ forceReloadSdkSession,
77961
+ invalidateGsapCache,
77962
+ isRecordingRef,
77963
+ pendingTimelineEditPathRef,
77964
+ previewIframeRef,
77965
+ projectIdRef,
77966
+ recordEdit,
77967
+ reloadPreview,
77968
+ sdkSession,
77969
+ publishSdkSession,
77970
+ showToast,
77971
+ writeProjectFile
77972
+ });
77973
+ const { handleTimelineElementMove, handleTimelineElementResize } = useTimelineClipTimingEditing({
77974
+ activeCompPath,
77975
+ timelineElements,
77976
+ showToast,
77977
+ writeProjectFile,
77978
+ recordEdit,
77979
+ reloadPreview,
77980
+ previewIframeRef,
77981
+ pendingTimelineEditPathRef,
77982
+ isRecordingRef,
77983
+ sdkSession,
77984
+ publishSdkSession,
77985
+ forceReloadSdkSession,
77986
+ invalidateGsapCache,
77987
+ handleDomZIndexReorderCommitRef,
77988
+ editQueueRef,
77989
+ projectIdRef
77990
+ });
77535
77991
  const handleToggleTrackHidden = useTimelineTrackVisibilityEditing({
77536
77992
  projectIdRef,
77537
77993
  activeCompPath,
@@ -77753,7 +78209,7 @@ function useTimelineEditing({
77753
78209
 
77754
78210
  // src/hooks/timelineMoveAdapter.ts
77755
78211
  import { useCallback as useCallback124 } from "react";
77756
- function persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps, coalesceMs) {
78212
+ function persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps) {
77757
78213
  return deps.handleTimelineGroupMove(
77758
78214
  edits.map(({ element, updates }) => ({
77759
78215
  element,
@@ -77765,14 +78221,14 @@ function persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps,
77765
78221
  // ("timing") omit it so they stay eligible for the SDK fast path.
77766
78222
  track: operation === "timing" ? void 0 : updates.track
77767
78223
  })),
77768
- { coalesceKey, coalesceMs }
78224
+ { coalesceKey }
77769
78225
  );
77770
78226
  }
77771
78227
  function useTimelineMoveEditsHandler(handleTimelineGroupMove) {
77772
78228
  return useCallback124(
77773
- async (edits, coalesceKey, operation = "timing", coalesceMs) => {
78229
+ async (edits, coalesceKey, operation = "timing") => {
77774
78230
  const deps = { handleTimelineGroupMove };
77775
- await persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps, coalesceMs);
78231
+ await persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps);
77776
78232
  },
77777
78233
  [handleTimelineGroupMove]
77778
78234
  );
@@ -77902,6 +78358,9 @@ function domEditSelectionsTargetSame(a, b) {
77902
78358
  if (!a || !b) return false;
77903
78359
  return getDomEditTargetKey(a) === getDomEditTargetKey(b);
77904
78360
  }
78361
+ function domEditSelectionsEqual(a, b) {
78362
+ return sameData(a, b);
78363
+ }
77905
78364
  function domEditSelectionInGroup(group, selection) {
77906
78365
  if (!selection) return false;
77907
78366
  return group.some((entry2) => domEditSelectionsTargetSame(entry2, selection));
@@ -78168,6 +78627,8 @@ function useDomSelection({
78168
78627
  const currentSelection = domEditSelectionRef.current;
78169
78628
  const previousGroup = domEditGroupSelectionsRef.current;
78170
78629
  const isRepeatedSingleSelection = !isAdditiveSelection && !options?.preserveGroup && previousGroup.length === 1 && domEditSelectionsTargetSame(currentSelection, selection) && domEditSelectionsTargetSame(previousGroup[0], selection);
78630
+ const isUnchangedRefresh = options?.preserveGroup && currentSelection !== null && (previousGroup.length === 0 || previousGroup.includes(currentSelection)) && domEditSelectionsEqual(currentSelection, selection);
78631
+ if (isUnchangedRefresh) return;
78171
78632
  if (isRepeatedSingleSelection) {
78172
78633
  if (options?.revealPanel !== false) {
78173
78634
  setRightCollapsed(false);
@@ -78229,6 +78690,7 @@ function useDomSelection({
78229
78690
  preferClipAncestor: options?.preferClipAncestor,
78230
78691
  skipSourceProbe: options?.skipSourceProbe,
78231
78692
  exactTarget: options?.exactTarget,
78693
+ previous: options?.previous,
78232
78694
  activeGroupElement: options && "activeGroupElement" in options ? options.activeGroupElement : activeGroupElementRef.current,
78233
78695
  projectId
78234
78696
  });
@@ -78351,7 +78813,7 @@ function useDomSelection({
78351
78813
  applyDomSelection(null, { revealPanel: false });
78352
78814
  return;
78353
78815
  }
78354
- const nextSelection = await buildDomSelectionFromTarget(element);
78816
+ const nextSelection = await buildDomSelectionFromTarget(element, { previous: selection });
78355
78817
  if (nextSelection) {
78356
78818
  applyDomSelection(nextSelection, {
78357
78819
  revealPanel: false,
@@ -78376,7 +78838,7 @@ function useDomSelection({
78376
78838
  for (const selection of selections) {
78377
78839
  const element = findElementForSelection(doc, selection, activeCompPath);
78378
78840
  if (!element) continue;
78379
- const nextSelection2 = await buildDomSelectionFromTarget(element);
78841
+ const nextSelection2 = await buildDomSelectionFromTarget(element, { previous: selection });
78380
78842
  if (nextSelection2) nextGroup.push(nextSelection2);
78381
78843
  }
78382
78844
  if (nextGroup.length === 0) return;
@@ -81368,13 +81830,13 @@ function useGsapKeyframeOps({
81368
81830
 
81369
81831
  // src/hooks/useGsapPropertyDebounce.ts
81370
81832
  import { useCallback as useCallback138, useEffect as useEffect118, useRef as useRef146 } from "react";
81371
- import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
81833
+ import { parseGsapScriptAcorn as parseGsapScriptAcorn2 } from "@hyperframes/core/gsap-parser-acorn";
81372
81834
  var DEBOUNCE_MS = 150;
81373
81835
  function mergeTweenProperties(sdkSession, animationId, edited, kind) {
81374
81836
  try {
81375
81837
  const script = extractGsapScriptText(sdkSession.serialize());
81376
81838
  if (!script) return { ...edited };
81377
- const anim = parseGsapScriptAcorn(script).animations.find((a) => a.id === animationId);
81839
+ const anim = parseGsapScriptAcorn2(script).animations.find((a) => a.id === animationId);
81378
81840
  if (!anim) return { ...edited };
81379
81841
  const existing = kind === "from" ? anim.fromProperties ?? {} : anim.properties;
81380
81842
  return { ...existing, ...edited };
@@ -82052,7 +82514,9 @@ function useDomEditPreviewSync({
82052
82514
  applyDomSelection(null, { revealPanel: false });
82053
82515
  return;
82054
82516
  }
82055
- const nextSelection = await buildDomSelectionFromTarget(nextElement);
82517
+ const nextSelection = await buildDomSelectionFromTarget(nextElement, {
82518
+ previous: currentSelection
82519
+ });
82056
82520
  if (nextSelection) {
82057
82521
  applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
82058
82522
  }
@@ -83223,19 +83687,17 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
83223
83687
  const newStart = Math.min(ct, ts);
83224
83688
  const newEnd = Math.max(ct, ts + td);
83225
83689
  const newDuration = Math.max(0.01, newEnd - newStart);
83690
+ const runEase = runEaseOf(anim);
83691
+ const toNewPct = (absTime) => Math.round(progressAtTime(runEase, (absTime - newStart) / newDuration * 100) * 10) / 10;
83226
83692
  const remapped = kfs.map((kf) => {
83227
- const absTime = ts + kf.percentage / 100 * td;
83228
- const newPct = Math.round((absTime - newStart) / newDuration * 1e3) / 10;
83693
+ const newPct = toNewPct(ts + timeAtProgress(runEase, kf.percentage) / 100 * td);
83229
83694
  const p = { ...kf.properties };
83230
83695
  for (const k of Object.keys(properties)) {
83231
83696
  if (!(k in p) && backfillDefaults[k] != null) p[k] = backfillDefaults[k];
83232
83697
  }
83233
83698
  return { percentage: newPct, properties: p };
83234
83699
  });
83235
- remapped.push({
83236
- percentage: Math.round((ct - newStart) / newDuration * 1e3) / 10,
83237
- properties
83238
- });
83700
+ remapped.push({ percentage: toNewPct(ct), properties });
83239
83701
  remapped.sort((a, b) => a.percentage - b.percentage);
83240
83702
  await commit2(
83241
83703
  selection,
@@ -83924,6 +84386,19 @@ function runCommitPreflights(selection, fileAnimations, iframe, group) {
83924
84386
  };
83925
84387
  }
83926
84388
  var resolvedSelections = /* @__PURE__ */ new WeakMap();
84389
+ var checkedSelections = /* @__PURE__ */ new WeakMap();
84390
+ function checkedPreflight(resolved2, inputs) {
84391
+ const known = checkedSelections.get(resolved2);
84392
+ const same2 = known?.animations === inputs.animations && known.iframe === inputs.iframe && known.group === inputs.group && known.version === inputs.version;
84393
+ if (same2) return known;
84394
+ const { animations, iframe, group } = inputs;
84395
+ const checked = {
84396
+ ...inputs,
84397
+ preflight: runCommitPreflights(resolved2, animations, iframe, group)
84398
+ };
84399
+ checkedSelections.set(resolved2, checked);
84400
+ return checked;
84401
+ }
83927
84402
  var resolvedOf = (selection) => resolvedSelections.get(selection) ?? selection;
83928
84403
  var MANUAL_FLAGS = [
83929
84404
  ["canApplyManualOffset", "offset"],
@@ -83986,17 +84461,21 @@ function useCommitPreflightCapabilities({
83986
84461
  void parseTick;
83987
84462
  if (!enabled || !projectId) return { selection, groupSelections };
83988
84463
  const group = groupSelections.length > 1;
84464
+ const iframe = previewIframeRef.current;
83989
84465
  const narrow = (target) => {
83990
84466
  const file = gsapSourceFileForSelection(target);
83991
84467
  const animations = parsesRef.current.get(parseCacheKey(projectId, file))?.animations;
83992
- const preflight = animations ? runCommitPreflights(resolvedOf(target), animations, previewIframeRef.current, group) : null;
83993
- return narrowCapabilities(target, preflight);
84468
+ if (!animations) return narrowCapabilities(target, null);
84469
+ const inputs = { animations, iframe, group, version };
84470
+ const checked = checkedPreflight(resolvedOf(target), inputs);
84471
+ checked.narrowed ??= narrowCapabilities(target, checked.preflight);
84472
+ return checked.narrowed;
83994
84473
  };
83995
- return {
84474
+ return withTweenIndex(() => ({
83996
84475
  selection: selection && narrow(selection),
83997
84476
  groupSelections: groupSelections.map(narrow)
83998
- };
83999
- }, [enabled, projectId, selection, groupSelections, parseTick, previewIframeRef]);
84477
+ }));
84478
+ }, [enabled, projectId, selection, groupSelections, parseTick, previewIframeRef, version]);
84000
84479
  }
84001
84480
 
84002
84481
  // src/hooks/domEditDeleteMembers.ts
@@ -85956,11 +86435,11 @@ function useEditHistoryActions({
85956
86435
  const apply = useCallback153(
85957
86436
  async (direction) => {
85958
86437
  const noun = direction === "undo" ? "Undo" : "Redo";
85959
- const predicted = editHistory.predict?.(direction) ?? null;
86438
+ const pendingEditShown = direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
86439
+ const predicted = pendingEditShown ? null : editHistory.predict?.(direction) ?? null;
85960
86440
  const predictedShown = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
85961
- const pendingEditShown = !predictedShown && direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
85962
86441
  const putBack = predictedShown ?? pendingEditShown?.showAgain;
85963
- const claimedAfter = direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : void 0;
86442
+ const claimedAfter = pendingEditShown?.claimsAtBegin ?? (direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : void 0);
85964
86443
  let result = { ok: false, reason: "failed" };
85965
86444
  let serverSteppedShown = false;
85966
86445
  let revertIsTheUndo = false;
@@ -85974,7 +86453,8 @@ function useEditHistoryActions({
85974
86453
  claimedAfter
85975
86454
  });
85976
86455
  const stepped = Boolean(result.ok && result.label);
85977
- serverSteppedShown = predictedShown ? stepped && result.undoes === predicted?.id : stepped && Boolean(pendingEditShown);
86456
+ const editClaimed = pendingEditShown !== null && (editHistory.claims?.() ?? 0) > pendingEditShown.claimsAtBegin;
86457
+ serverSteppedShown = predictedShown ? stepped && result.undoes === predicted?.id : stepped && editClaimed;
85978
86458
  } finally {
85979
86459
  if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
85980
86460
  }
@@ -88727,16 +89207,16 @@ function averageSpeed(samples, from, to) {
88727
89207
  return total / (seg.length - 1);
88728
89208
  }
88729
89209
  function speedAtEdge(samples, t2, window2, side) {
88730
- const near3 = samples.filter(
89210
+ const near4 = samples.filter(
88731
89211
  (s) => side === "start" ? s.time >= t2 && s.time <= t2 + window2 : s.time >= t2 - window2 && s.time <= t2
88732
89212
  );
88733
- if (near3.length < 2) return 0;
89213
+ if (near4.length < 2) return 0;
88734
89214
  let total = 0;
88735
- for (let i = 1; i < near3.length; i++) {
88736
- const dt = near3[i].time - near3[i - 1].time;
88737
- if (dt > 0) total += Math.abs(near3[i].value - near3[i - 1].value) / dt;
89215
+ for (let i = 1; i < near4.length; i++) {
89216
+ const dt = near4[i].time - near4[i - 1].time;
89217
+ if (dt > 0) total += Math.abs(near4[i].value - near4[i - 1].value) / dt;
88738
89218
  }
88739
- return total / (near3.length - 1);
89219
+ return total / (near4.length - 1);
88740
89220
  }
88741
89221
  function fitEasesFromVelocity(keyframes, rawSamples, totalDuration) {
88742
89222
  if (keyframes.length < 2 || rawSamples.length < 3) return keyframes;
@@ -88972,7 +89452,7 @@ function useGestureCommit({
88972
89452
  );
88973
89453
  } else {
88974
89454
  const tweenStart = existingPositionTween.resolvedStart ?? 0;
88975
- const tweenDur = existingPositionTween.duration ?? duration;
89455
+ const tweenDur = resolveTweenDuration(existingPositionTween);
88976
89456
  const tweenEnd = tweenStart + tweenDur;
88977
89457
  const recEnd = recStart + duration;
88978
89458
  const overlaps3 = recStart < tweenEnd + 0.05 && recEnd > tweenStart - 0.05;
@@ -91367,9 +91847,9 @@ function useLastFrame(request, snapshot3) {
91367
91847
  }
91368
91848
  function useNearViewport() {
91369
91849
  const [element, setElement] = useState141(null);
91370
- const [near3, setNear] = useState141(false);
91850
+ const [near4, setNear] = useState141(false);
91371
91851
  useEffect144(() => {
91372
- if (!element || near3) return;
91852
+ if (!element || near4) return;
91373
91853
  if (typeof IntersectionObserver === "undefined") return setNear(true);
91374
91854
  const observer = new IntersectionObserver(
91375
91855
  (entries) => entries.some((entry2) => entry2.isIntersecting) && setNear(true),
@@ -91377,8 +91857,8 @@ function useNearViewport() {
91377
91857
  );
91378
91858
  observer.observe(element);
91379
91859
  return () => observer.disconnect();
91380
- }, [element, near3]);
91381
- return [setElement, near3];
91860
+ }, [element, near4]);
91861
+ return [setElement, near4];
91382
91862
  }
91383
91863
  function CompCard({
91384
91864
  projectId,
@@ -96737,31 +97217,10 @@ function animatedProps(anim) {
96737
97217
  }
96738
97218
  return ["x", "y"];
96739
97219
  }
96740
- function isPlayheadWithinTween(anim, currentTime, selection) {
97220
+ function isPlayheadWithinTween(anim, currentTime) {
96741
97221
  const start = resolveTweenStart(anim);
96742
97222
  if (start === null) return true;
96743
- const duration = selection ? resolveEditableTweenDuration(anim, selection) : resolveTweenDuration(anim);
96744
- return isTimeWithinTween(currentTime, start, duration);
96745
- }
96746
- function buildExtendedKeyframes(anim, currentTime, position, sourceDuration = resolveTweenDuration(anim)) {
96747
- const oldStart = resolveTweenStart(anim) ?? 0;
96748
- const oldDuration = sourceDuration;
96749
- const newStart = Math.min(oldStart, currentTime);
96750
- const newEnd = Math.max(oldStart + oldDuration, currentTime);
96751
- const newDuration = roundTo3(newEnd - newStart);
96752
- const toPct = (absoluteTime) => newDuration > 0 ? Math.max(
96753
- 0,
96754
- Math.min(100, Math.round((absoluteTime - newStart) / newDuration * 1e3) / 10)
96755
- ) : 0;
96756
- const stops = anim.keyframes?.keyframes ?? [];
96757
- const rescaled = stops.map((stop2) => ({
96758
- percentage: toPct(oldStart + stop2.percentage / 100 * oldDuration),
96759
- properties: stop2.properties,
96760
- ...stop2.ease ? { ease: stop2.ease } : {}
96761
- }));
96762
- const added = { percentage: toPct(currentTime), properties: position };
96763
- const keyframes = [...rescaled, added].sort((a, b) => a.percentage - b.percentage);
96764
- return { position: roundTo3(newStart), duration: newDuration, keyframes };
97223
+ return isTimeWithinTween(currentTime, start, resolveTweenDuration(anim));
96765
97224
  }
96766
97225
  async function replaceSetWithSingleKeyframe(session, sel, setAnim, t2, iframe, selector) {
96767
97226
  const position = readElementPosition(iframe, sel, setAnim);
@@ -96852,10 +97311,16 @@ async function extendKeyframedTweenToPlayhead(session, sel, anim, currentTime, d
96852
97311
  }
96853
97312
  );
96854
97313
  }
96855
- async function applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe, commitOverrides) {
96856
- const duration = resolveEditableTweenDuration(kfAnim, sel);
97314
+ async function applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe, commitOverrides, liveTween) {
97315
+ const keepAuthoredDuration = kfAnim.durationUnresolved === true;
97316
+ if (keepAuthoredDuration) {
97317
+ kfAnim = withLiveTiming(kfAnim, liveTween ?? findParsedTween(iframe, sel.element, kfAnim));
97318
+ }
97319
+ if (keepAuthoredDuration && kfAnim.duration == null) return;
97320
+ const duration = resolveTweenDuration(kfAnim);
96857
97321
  const start = resolveTweenStart(kfAnim);
96858
97322
  if (start !== null && !isTimeWithinTween(t2, start, duration)) {
97323
+ if (keepAuthoredDuration) return;
96859
97324
  await extendKeyframedTweenToPlayhead(
96860
97325
  session,
96861
97326
  sel,
@@ -96867,10 +97332,8 @@ async function applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe, commitO
96867
97332
  );
96868
97333
  return;
96869
97334
  }
96870
- const pct = start === null ? computeElementPercentage(t2, sel) : absoluteToPercentage(t2, start, duration);
96871
- const existing = kfAnim.keyframes?.keyframes.find(
96872
- (k) => Math.abs(k.percentage - pct) <= KEYFRAME_PCT_MATCH
96873
- );
97335
+ const pct = start === null ? computeElementPercentage(t2, sel) : absoluteToPercentageForAnimation(t2, kfAnim) ?? 0;
97336
+ const existing = kfAnim.keyframes?.keyframes.find((k) => playsNear(kfAnim, k.percentage, pct));
96874
97337
  if (existing) {
96875
97338
  session.handleGsapRemoveKeyframe(kfAnim.id, existing.percentage);
96876
97339
  return;
@@ -96922,21 +97385,26 @@ async function applyArcKeyframeAtPlayhead(session, sel, arcAnim, t2, iframe) {
96922
97385
  const targetSelector = existingTweenTargetSelector(arcAnim, sel);
96923
97386
  if (!targetSelector) return;
96924
97387
  const start = resolveTweenStart(arcAnim) ?? 0;
96925
- const duration = resolveEditableTweenDuration(arcAnim, sel);
97388
+ const duration = resolveTweenDuration(arcAnim);
96926
97389
  if (!isTimeWithinTween(t2, start, duration)) {
96927
- if (t2 > start) {
96928
- await session.commitMutation(
96929
- {
96930
- type: "update-meta",
96931
- animationId: arcAnim.id,
96932
- updates: { duration: roundTo3(t2 - start) }
96933
- },
96934
- { label: "Extend motion path", softReload: true }
96935
- );
96936
- }
97390
+ const held = readElementPosition(iframe, sel, arcAnim);
97391
+ if (t2 <= start || typeof held.x !== "number" || typeof held.y !== "number") return;
97392
+ const extended = buildExtendedKeyframes(arcAnim, t2, { x: held.x, y: held.y }, duration, "none");
97393
+ await session.commitMutation(
97394
+ {
97395
+ type: "replace-with-keyframes",
97396
+ animationId: arcAnim.id,
97397
+ targetSelector,
97398
+ position: extended.position,
97399
+ duration: extended.duration,
97400
+ keyframes: extended.keyframes,
97401
+ ease: "none"
97402
+ },
97403
+ { label: "Add keyframe", keyframeAction: "add", softReload: true }
97404
+ );
96937
97405
  return;
96938
97406
  }
96939
- const nodes = arcAnim.keyframes?.keyframes ?? [];
97407
+ const nodes = linearArcKeyframes(arcAnim);
96940
97408
  const playheadPercentage = absoluteToPercentage(t2, start, duration);
96941
97409
  const timedNodeIndex = nodes.findIndex(
96942
97410
  (node) => Math.abs(node.percentage - playheadPercentage) <= KEYFRAME_PCT_MATCH
@@ -96997,6 +97465,7 @@ function useEnableKeyframes(sessionRef) {
96997
97465
  } else if (setAnim) {
96998
97466
  await promoteSetToKeyframes(session, sel, setAnim, t2, iframe);
96999
97467
  } else if (flatAnim) {
97468
+ const flatLive = flatAnim.durationUnresolved ? findParsedTween(iframe, sel.element, flatAnim) : void 0;
97000
97469
  enableKeyframesTransactionCounter += 1;
97001
97470
  const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
97002
97471
  const convertCommitOverrides = {
@@ -97022,7 +97491,15 @@ function useEnableKeyframes(sessionRef) {
97022
97491
  // conversion splits into two undo entries under real latency.
97023
97492
  coalesceMs: Number.POSITIVE_INFINITY
97024
97493
  };
97025
- await applyKeyframeAtPlayhead(session, sel, converted, t2, iframe, applyCommitOverrides);
97494
+ await applyKeyframeAtPlayhead(
97495
+ session,
97496
+ sel,
97497
+ converted,
97498
+ t2,
97499
+ iframe,
97500
+ applyCommitOverrides,
97501
+ flatLive
97502
+ );
97026
97503
  }
97027
97504
  } else {
97028
97505
  const position = readElementPosition(iframe, sel, null);
@@ -97331,7 +97808,9 @@ var NO_KEYFRAME_TOGGLE = {
97331
97808
  function isMotionPathEndpoint(animation, percentage) {
97332
97809
  if (!animation?.keyframes) return false;
97333
97810
  const keyframes = animation.keyframes.keyframes;
97334
- return Math.abs((keyframes[0]?.percentage ?? -Infinity) - percentage) <= KEYFRAME_PCT_MATCH || Math.abs((keyframes.at(-1)?.percentage ?? Infinity) - percentage) <= KEYFRAME_PCT_MATCH;
97811
+ return [keyframes[0], keyframes.at(-1)].some(
97812
+ (keyframe) => keyframe && playsNear(animation, keyframe.percentage, percentage)
97813
+ );
97335
97814
  }
97336
97815
  function resolveKeyframeToggleState(session, currentTime) {
97337
97816
  if (!session?.domEditSelection) return NO_KEYFRAME_TOGGLE;
@@ -97341,13 +97820,13 @@ function resolveKeyframeToggleState(session, currentTime) {
97341
97820
  const animation = arcAnimation ?? session.selectedGsapAnimations.find((candidate) => candidate.keyframes && !candidate.arcPath);
97342
97821
  if (!animation?.keyframes) return NO_KEYFRAME_TOGGLE;
97343
97822
  const isMotionPath = Boolean(arcAnimation);
97344
- if (!isPlayheadWithinTween(animation, currentTime, session.domEditSelection)) {
97823
+ if (!isPlayheadWithinTween(animation, currentTime)) {
97345
97824
  return { state: "inactive", isMotionPath, pathEndpoint: false, willExtend: true };
97346
97825
  }
97347
97826
  const percentage = computeElementPercentage(currentTime, session.domEditSelection, animation);
97348
97827
  const pathEndpoint = isMotionPathEndpoint(arcAnimation, percentage);
97349
97828
  const active = animation.keyframes.keyframes.some(
97350
- (keyframe) => Math.abs(keyframe.percentage - percentage) <= KEYFRAME_PCT_MATCH
97829
+ (keyframe) => playsNear(animation, keyframe.percentage, percentage)
97351
97830
  );
97352
97831
  return {
97353
97832
  state: pathEndpoint ? "none" : active ? "active" : "inactive",