@hyperframes/studio 0.8.138 → 0.8.139

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 (77) hide show
  1. package/dist/assets/{hyperframes-player-B30apksz.js → hyperframes-player-CckOoCJ8.js} +1 -1
  2. package/dist/assets/{index-DIJuIBwI.js → index-BmIPrb8I.js} +1 -1
  3. package/dist/assets/{index-BdjA0CLD.js → index-Br1Whc0f.js} +192 -192
  4. package/dist/assets/{index-DXVvWfQG.js → index-DsEkE5_G.js} +1 -1
  5. package/dist/{chunk-4S3AYCOG.js → chunk-E422JDF5.js} +2 -2
  6. package/dist/{chunk-TDWGXBUT.js → chunk-TFLU2MGI.js} +1 -6
  7. package/dist/chunk-TFLU2MGI.js.map +1 -0
  8. package/dist/{chunk-7UQWVN52.js → chunk-UYWON5AX.js} +2 -2
  9. package/dist/{domEditingLayers-IEVLO54Z.js → domEditingLayers-KBTPQKGA.js} +3 -3
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.html +1 -1
  12. package/dist/index.js +846 -469
  13. package/dist/index.js.map +1 -1
  14. package/dist/player.js +2 -2
  15. package/package.json +8 -8
  16. package/src/App.tsx +9 -4
  17. package/src/components/editor/DomEditOverlay.tsx +9 -5
  18. package/src/components/editor/PropertyPanel.test.tsx +600 -748
  19. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +2 -0
  20. package/src/components/editor/domEditOverlayGeometry.ts +7 -7
  21. package/src/components/editor/domEditOverlayGestures.ts +18 -0
  22. package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +1 -0
  23. package/src/components/editor/domEditOverlayStartGesture.test.ts +3 -0
  24. package/src/components/editor/domEditOverlayStartGesture.ts +9 -1
  25. package/src/components/editor/dragUndoPaint.test.ts +2 -0
  26. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +432 -15
  27. package/src/components/editor/groupDropKeepsSelection.test.ts +1 -0
  28. package/src/components/editor/manualEditsDom.ts +7 -7
  29. package/src/components/editor/manualEditsTypes.ts +5 -0
  30. package/src/components/editor/manualOffsetDrag.test.ts +23 -0
  31. package/src/components/editor/manualOffsetDrag.ts +5 -0
  32. package/src/components/editor/previewGestureStarts.ts +284 -0
  33. package/src/components/editor/rotateInlineSave.test.ts +1 -0
  34. package/src/components/editor/shiftDragAxisLock.test.tsx +1 -0
  35. package/src/components/editor/useDomEditOverlayGestures.ts +67 -71
  36. package/src/components/nle/NLEContext.test.ts +26 -0
  37. package/src/components/nle/NLEContext.tsx +8 -3
  38. package/src/hooks/draggedGsapPosition.ts +8 -0
  39. package/src/hooks/editMoment.ts +13 -0
  40. package/src/hooks/gsapDragCommit.ts +2 -1
  41. package/src/hooks/gsapDragPositionCommit.ts +19 -7
  42. package/src/hooks/gsapPositionDetection.ts +5 -3
  43. package/src/hooks/gsapResizeIntercept.ts +11 -4
  44. package/src/hooks/gsapRuntimeBridge.ts +31 -11
  45. package/src/hooks/gsapValueAtPlayhead.test.ts +15 -0
  46. package/src/hooks/gsapValueAtPlayhead.ts +12 -5
  47. package/src/hooks/timelineEditingHelpers.test.ts +22 -0
  48. package/src/hooks/timelineEditingHelpers.ts +18 -2
  49. package/src/hooks/useDomSelection.test.ts +21 -2
  50. package/src/hooks/useEditHistoryActions.test.tsx +20 -0
  51. package/src/hooks/useEditHistoryActions.ts +2 -0
  52. package/src/hooks/useGsapAwareEditing.test.tsx +38 -0
  53. package/src/hooks/useGsapAwareEditing.ts +148 -136
  54. package/src/hooks/useGsapAwareGroupMove.ts +126 -113
  55. package/src/hooks/useGsapScriptCommits.ts +5 -4
  56. package/src/hooks/usePreviewPersistence.history.test.tsx +7 -2
  57. package/src/hooks/usePreviewPersistence.ts +1 -4
  58. package/src/hooks/useTimelineDeleteOps.test.tsx +10 -0
  59. package/src/hooks/useTimelineDeleteOps.ts +9 -10
  60. package/src/index.ts +1 -0
  61. package/src/player/hooks/useShadowPreviewReload.ts +6 -6
  62. package/src/player/hooks/useTimelinePlayer.shadowReload.test.ts +14 -0
  63. package/src/player/hooks/useTimelinePlayer.ts +8 -5
  64. package/src/player/lib/thumbnailScheduler.test.ts +20 -1
  65. package/src/player/lib/thumbnailScheduler.ts +1 -1
  66. package/src/player/lib/timelineElementHelpers.ts +0 -11
  67. package/src/player/previewReloading.test.ts +48 -0
  68. package/src/player/previewReloading.ts +48 -0
  69. package/src/utils/rootDuration.ts +5 -4
  70. package/src/utils/studioPendingEdits.test.ts +27 -0
  71. package/src/utils/studioPendingEdits.ts +9 -0
  72. package/src/utils/timelineAssetDrop.test.ts +13 -4
  73. package/src/utils/timelineAssetDrop.ts +6 -2
  74. package/dist/chunk-TDWGXBUT.js.map +0 -1
  75. /package/dist/{chunk-4S3AYCOG.js.map → chunk-E422JDF5.js.map} +0 -0
  76. /package/dist/{chunk-7UQWVN52.js.map → chunk-UYWON5AX.js.map} +0 -0
  77. /package/dist/{domEditingLayers-IEVLO54Z.js.map → domEditingLayers-KBTPQKGA.js.map} +0 -0
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  usePreviewVariablesStore,
40
40
  whenPreviewBooted,
41
41
  writeStudioUiPreferences
42
- } from "./chunk-4S3AYCOG.js";
42
+ } from "./chunk-E422JDF5.js";
43
43
  import {
44
44
  Button,
45
45
  ContextMenu,
@@ -104,7 +104,7 @@ import {
104
104
  resolveDomEditSelection,
105
105
  serializeDomEditTextFields,
106
106
  studioApiFetch
107
- } from "./chunk-7UQWVN52.js";
107
+ } from "./chunk-UYWON5AX.js";
108
108
  import {
109
109
  LAYER_REVEAL_PRIOR_POSITION_ATTR,
110
110
  LAYER_REVEAL_PRIOR_Z_ATTR,
@@ -122,7 +122,6 @@ import {
122
122
  findClipElementById,
123
123
  formatCssColor,
124
124
  furthestClipEndFromDocument,
125
- furthestClipEndFromSource,
126
125
  getAdapterDuration,
127
126
  getDefaultStaticSeekPlaybackClock,
128
127
  getSelectorIndex,
@@ -164,7 +163,7 @@ import {
164
163
  splitTimelineElementKey,
165
164
  toHexColor,
166
165
  wrapTimeline
167
- } from "./chunk-TDWGXBUT.js";
166
+ } from "./chunk-TFLU2MGI.js";
168
167
  import "./chunk-PZ5AY32C.js";
169
168
 
170
169
  // src/components/EditorShell.tsx
@@ -3162,8 +3161,10 @@ var COMPOSITION_ROOT_OPEN_TAG_RE = /<[^>]*data-composition-id="[^"]+"[^>]*>/i;
3162
3161
  var ROOT_COMPOSITION_OPEN_TAG_RE = /<[^>]*\bdata-composition-id(?=[\s=/>])[^>]*>/i;
3163
3162
  var DATA_DURATION_ATTR_RE = /(\bdata-duration\s*=\s*)(["'])[^"']*\2/i;
3164
3163
  function readRootCompositionDuration(source) {
3165
- const root = new DOMParser().parseFromString(source, "text/html").querySelector("[data-composition-id]");
3166
- const raw = root?.getAttribute("data-duration");
3164
+ return rootCompositionDurationOf(new DOMParser().parseFromString(source, "text/html"));
3165
+ }
3166
+ function rootCompositionDurationOf(doc) {
3167
+ const raw = doc.querySelector("[data-composition-id]")?.getAttribute("data-duration");
3167
3168
  if (raw == null) return null;
3168
3169
  return Number.parseFloat(raw);
3169
3170
  }
@@ -3260,9 +3261,9 @@ function buildTimelineAssetInsertHtml(input) {
3260
3261
  }
3261
3262
  return `<audio ${sharedAttrs} data-volume="1" style="z-index: ${input.zIndex}"></audio>`;
3262
3263
  }
3263
- function setCompositionDurationToContent(source, contentEnd) {
3264
+ function setCompositionDurationToContent(source, contentEnd, rootDuration) {
3264
3265
  if (!Number.isFinite(contentEnd) || contentEnd <= 0) return source;
3265
- const rootDur = readRootCompositionDuration(source);
3266
+ const rootDur = rootDuration === void 0 ? readRootCompositionDuration(source) : rootDuration;
3266
3267
  if (rootDur == null) return source;
3267
3268
  const next = roundToCenti(contentEnd);
3268
3269
  if (rootDur === next) return source;
@@ -9443,6 +9444,13 @@ function setStudioPendingEditClaimClock(read2) {
9443
9444
  historyClaims = read2 ?? (() => 0);
9444
9445
  }
9445
9446
  var NOT_SAVED = () => Promise.resolve(false);
9447
+ var cancelWaitingPress = null;
9448
+ function setStudioWaitingPressCancel(cancel) {
9449
+ cancelWaitingPress = cancel;
9450
+ }
9451
+ function cancelNewestStudioWaitingPress() {
9452
+ return cancelWaitingPress?.() ?? false;
9453
+ }
9446
9454
  var adopting = null;
9447
9455
  function adoptingStudioPendingEdit() {
9448
9456
  return adopting;
@@ -15730,10 +15738,10 @@ var ThumbnailScheduler = class {
15730
15738
  if (!scrolling) this.pump();
15731
15739
  }
15732
15740
  /** Holds composition renders while the preview loads: same server, and the preview is what the person awaits. */
15733
- setPreviewReloading(reloading) {
15734
- if (this.previewReloading === reloading) return;
15735
- this.previewReloading = reloading;
15736
- if (!reloading) {
15741
+ setPreviewReloading(reloading2) {
15742
+ if (this.previewReloading === reloading2) return;
15743
+ this.previewReloading = reloading2;
15744
+ if (!reloading2) {
15737
15745
  this.pump();
15738
15746
  return;
15739
15747
  }
@@ -15819,7 +15827,7 @@ var ThumbnailScheduler = class {
15819
15827
  slotFree = true;
15820
15828
  this.activeByBucket[bucket]--;
15821
15829
  this.activeByKind[entry2.request.kind]--;
15822
- this.pump();
15830
+ queueMicrotask(() => this.pump());
15823
15831
  };
15824
15832
  controller.signal.addEventListener("abort", freeSlot, { once: true });
15825
15833
  this.notify(entry2);
@@ -18362,7 +18370,17 @@ function applyTimelineMoveAttributes(original, target, start, duration, track2,
18362
18370
  return patched;
18363
18371
  }
18364
18372
  function syncCompositionDurationToContent(source) {
18365
- return setCompositionDurationToContent(source, furthestClipEndFromSource(source));
18373
+ return syncCompositionDurationWithEnd(source).source;
18374
+ }
18375
+ function syncCompositionDurationWithEnd(source) {
18376
+ const doc = new DOMParser().parseFromString(source, "text/html");
18377
+ const contentEnd = furthestClipEndFromDocument(doc);
18378
+ const synced = setCompositionDurationToContent(
18379
+ source,
18380
+ contentEnd,
18381
+ rootCompositionDurationOf(doc)
18382
+ );
18383
+ return { source: synced, contentEnd };
18366
18384
  }
18367
18385
  function buildTimelineMoveTimingPatch(original, target, start, duration, track2, audioGroup) {
18368
18386
  const patched = applyTimelineMoveAttributes(original, target, start, duration, track2, audioGroup);
@@ -29720,6 +29738,7 @@ import {
29720
29738
  } from "@hyperframes/core/editing/draft-markers";
29721
29739
  var STUDIO_ROTATION_PROP = "--hf-studio-rotation";
29722
29740
  var STUDIO_PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
29741
+ var STUDIO_EDIT_MOMENT_ATTR = "data-hf-studio-edit-at";
29723
29742
  var STUDIO_BOX_SIZE_ATTR = "data-hf-studio-box-size";
29724
29743
  var STUDIO_ROTATION_ATTR = "data-hf-studio-rotation";
29725
29744
  var STUDIO_ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
@@ -30432,10 +30451,12 @@ var GESTURE_DRAWS = {
30432
30451
  rotate: ["rotate", "transform", "transform-origin", "display", STUDIO_ROTATION_PROP],
30433
30452
  edit: []
30434
30453
  };
30435
- function beginStudioManualEditGesture(element, draws) {
30454
+ function beginStudioManualEditGesture(element, draws, at) {
30436
30455
  studioManualEditGestureId += 1;
30437
30456
  const token = `gesture-${studioManualEditGestureId}:${draws}`;
30438
30457
  element.setAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR, token);
30458
+ if (at) element.setAttribute(STUDIO_EDIT_MOMENT_ATTR, JSON.stringify(at));
30459
+ else element.removeAttribute(STUDIO_EDIT_MOMENT_ATTR);
30439
30460
  return token;
30440
30461
  }
30441
30462
  var GESTURE_ENDED = "hf-manual-edit-gesture-ended";
@@ -30443,6 +30464,7 @@ function endStudioManualEditGesture(element, token) {
30443
30464
  if (token && element.getAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR) !== token) return;
30444
30465
  if (!element.hasAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR)) return;
30445
30466
  element.removeAttribute(STUDIO_MANUAL_EDIT_GESTURE_ATTR);
30467
+ element.removeAttribute(STUDIO_EDIT_MOMENT_ATTR);
30446
30468
  const doc = element.ownerDocument;
30447
30469
  doc.dispatchEvent(new (doc.defaultView?.Event ?? Event)(GESTURE_ENDED));
30448
30470
  }
@@ -30848,6 +30870,43 @@ function makeStudioDebugLogger(name) {
30848
30870
  // src/utils/reloadDebug.ts
30849
30871
  var logReload = makeStudioDebugLogger("reload");
30850
30872
 
30873
+ // src/player/previewReloading.ts
30874
+ var reloading = false;
30875
+ var reloadRequested = false;
30876
+ var scriptWrites = 0;
30877
+ var givenUp = false;
30878
+ var changing = () => reloading || reloadRequested || scriptWrites > 0;
30879
+ var noteIdle = () => {
30880
+ if (!changing()) givenUp = false;
30881
+ };
30882
+ function giveUpOnPreviewChange() {
30883
+ givenUp = changing();
30884
+ }
30885
+ function setPreviewReloading(next) {
30886
+ reloading = next;
30887
+ thumbnailScheduler.setPreviewReloading(next);
30888
+ noteIdle();
30889
+ }
30890
+ function requestPreviewReload() {
30891
+ reloadRequested = true;
30892
+ }
30893
+ function previewReloadBegun() {
30894
+ reloadRequested = false;
30895
+ noteIdle();
30896
+ }
30897
+ async function whileScriptWrites(write) {
30898
+ scriptWrites += 1;
30899
+ try {
30900
+ return await write();
30901
+ } finally {
30902
+ scriptWrites -= 1;
30903
+ noteIdle();
30904
+ }
30905
+ }
30906
+ function isPreviewChanging() {
30907
+ return changing() && !givenUp;
30908
+ }
30909
+
30851
30910
  // src/player/sceneSwap.ts
30852
30911
  var SCENES_SWAPPED = "hf-scenes-swapped";
30853
30912
  var SCENE_SWAP_MS = 5e3;
@@ -30990,7 +31049,7 @@ function useShadowPreviewReload({
30990
31049
  isRefreshingRef.current = false;
30991
31050
  pendingSeekRef.current = null;
30992
31051
  setPreviewSlots(planShadowDiscard);
30993
- thumbnailScheduler.setPreviewReloading(false);
31052
+ setPreviewReloading(false);
30994
31053
  const message = `The preview did not reload (${cause}). The previous preview is still showing.`;
30995
31054
  logReload("shadow-failed", { cause });
30996
31055
  console.error(`[studio] ${message}`);
@@ -31061,7 +31120,7 @@ function useShadowPreviewReload({
31061
31120
  if (liveTime2 != null) handOverPlaybackRef.current(liveTime2, playing);
31062
31121
  announcePreviewPromoted({ retired, live: shadow });
31063
31122
  onPromotedRef.current?.();
31064
- thumbnailScheduler.setPreviewReloading(false);
31123
+ setPreviewReloading(false);
31065
31124
  },
31066
31125
  [
31067
31126
  stopPendingShadow,
@@ -31152,7 +31211,7 @@ function useShadowPreviewReload({
31152
31211
  savesAtRequestRef.current = gestureSavesOnScreen();
31153
31212
  armReadyTimer(gen);
31154
31213
  setPreviewSlots((prev) => planShadowReload(prev, gen, url));
31155
- thumbnailScheduler.setPreviewReloading(true);
31214
+ setPreviewReloading(true);
31156
31215
  },
31157
31216
  [stopPendingShadow, armReadyTimer, gestureSavesOnScreen]
31158
31217
  );
@@ -31165,7 +31224,7 @@ function useShadowPreviewReload({
31165
31224
  pendingSeekRef.current = null;
31166
31225
  usePlayerStore.getState().setTimelineReady(false);
31167
31226
  setPreviewSlots(planShadowDiscard);
31168
- thumbnailScheduler.setPreviewReloading(false);
31227
+ setPreviewReloading(false);
31169
31228
  }, [stopPendingShadow, isRefreshingRef, pendingSeekRef]);
31170
31229
  useMountEffect(() => {
31171
31230
  const onVisibilityChange = () => {
@@ -31180,7 +31239,7 @@ function useShadowPreviewReload({
31180
31239
  return () => {
31181
31240
  document.removeEventListener("visibilitychange", onVisibilityChange);
31182
31241
  stopPendingShadow();
31183
- thumbnailScheduler.setPreviewReloading(false);
31242
+ setPreviewReloading(false);
31184
31243
  };
31185
31244
  });
31186
31245
  return {
@@ -32043,11 +32102,13 @@ function useTimelinePlayer({
32043
32102
  const swapWouldReplaceGestureNode = !!iframe.contentDocument && isStudioManualEditGestureLiveIn(iframe.contentDocument);
32044
32103
  if (!swap || isRefreshingRef.current || swapWouldReplaceGestureNode)
32045
32104
  return reloadWholeFilm(url.toString());
32046
- swap(url.toString(), isCurrent, cancel.signal).catch((error) => {
32047
- if (!isCurrent()) return;
32048
- logReload("scene-swap-refused", { reason: String(error) });
32049
- reloadWholeFilm(url.toString());
32050
- });
32105
+ whileScriptWrites(() => swap(url.toString(), isCurrent, cancel.signal)).catch(
32106
+ (error) => {
32107
+ if (!isCurrent()) return;
32108
+ logReload("scene-swap-refused", { reason: String(error) });
32109
+ reloadWholeFilm(url.toString());
32110
+ }
32111
+ );
32051
32112
  }, [reloadWholeFilm, previewGeneration]);
32052
32113
  const pauseRef = useRef47(pause);
32053
32114
  pauseRef.current = pause;
@@ -32293,9 +32354,13 @@ function NLEProvider({
32293
32354
  const [previewCompositionSize, setPreviewCompositionSize] = useState35(null);
32294
32355
  const prevRefreshKeyRef = useRef49(refreshKey);
32295
32356
  useEffect37(() => {
32296
- if (refreshKey === prevRefreshKeyRef.current) return;
32297
- prevRefreshKeyRef.current = refreshKey;
32298
- refreshPlayer();
32357
+ try {
32358
+ if (refreshKey === prevRefreshKeyRef.current) return;
32359
+ prevRefreshKeyRef.current = refreshKey;
32360
+ refreshPlayer();
32361
+ } finally {
32362
+ previewReloadBegun();
32363
+ }
32299
32364
  }, [refreshKey, refreshPlayer]);
32300
32365
  const afterLiveIframeLoad = useCallback44(() => {
32301
32366
  ensureMotionPathPluginLoaded(iframeRef.current);
@@ -35975,6 +36040,11 @@ function readThroughPass(memo61, key2, compute) {
35975
36040
 
35976
36041
  // src/components/editor/domEditOverlayGeometry.ts
35977
36042
  var RESIZE_HANDLE_HIT_PX = 16;
36043
+ var shiftedOverlayRect = (rect, dx, dy) => ({
36044
+ ...rect,
36045
+ left: rect.left + dx,
36046
+ top: rect.top + dy
36047
+ });
35978
36048
  function isElementVisibleForOverlay(el) {
35979
36049
  return isElementVisibleThroughAncestors(el);
35980
36050
  }
@@ -36264,6 +36334,7 @@ var ROTATED_SNAP_BYPASS_DEGREES = 0.5;
36264
36334
  function movesGesture(gesture, e) {
36265
36335
  return e.pointerId === gesture.pointerId && (e.buttons & 1) === 1;
36266
36336
  }
36337
+ var PRESS_WAITING_ATTR = "data-dom-edit-press-waiting";
36267
36338
  function focusDomEditOverlayElement(element) {
36268
36339
  element?.focus({ preventScroll: true });
36269
36340
  }
@@ -36893,7 +36964,7 @@ function OffCanvasIndicators({
36893
36964
  const selectOffCanvas = async () => {
36894
36965
  const el = elements.current.get(r.key);
36895
36966
  if (!el) return;
36896
- const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-IEVLO54Z.js");
36967
+ const { resolveDomEditSelection: resolveDomEditSelection2 } = await import("./domEditingLayers-KBTPQKGA.js");
36897
36968
  const acp = activeCompositionPathRef.current ?? "index.html";
36898
36969
  const sel = await resolveDomEditSelection2(el, {
36899
36970
  activeCompositionPath: acp,
@@ -36947,6 +37018,68 @@ function ChildRectOutlines({
36947
37018
  )) });
36948
37019
  }
36949
37020
 
37021
+ // src/components/editor/domEditResizeLocal.ts
37022
+ var MIN_RESIZE_LOCAL_PX = 1;
37023
+ var DEGENERATE_START_DIST_PX = 3;
37024
+ function resolveCenterResizeScale(input) {
37025
+ const startDist = Math.hypot(
37026
+ input.pointerStart.x - input.centerStart.x,
37027
+ input.pointerStart.y - input.centerStart.y
37028
+ );
37029
+ if (!Number.isFinite(startDist) || startDist < DEGENERATE_START_DIST_PX) return 1;
37030
+ const nowDist = Math.hypot(
37031
+ input.pointer.x - input.centerStart.x,
37032
+ input.pointer.y - input.centerStart.y
37033
+ );
37034
+ return nowDist / startDist;
37035
+ }
37036
+ function resolveCenterResizeSize(input) {
37037
+ const baseWidth = Math.max(input.baseWidth, MIN_RESIZE_LOCAL_PX);
37038
+ const baseHeight = Math.max(input.baseHeight, MIN_RESIZE_LOCAL_PX);
37039
+ const rawScale = resolveCenterResizeScale({
37040
+ pointer: input.pointer,
37041
+ pointerStart: input.pointerStart,
37042
+ centerStart: input.centerStart
37043
+ });
37044
+ const minScale = MIN_RESIZE_LOCAL_PX / Math.min(baseWidth, baseHeight);
37045
+ const scale = Math.max(minScale, rawScale);
37046
+ return { width: baseWidth * scale, height: baseHeight * scale };
37047
+ }
37048
+ var CURSORS_8 = [
37049
+ "ns-resize",
37050
+ // 0° (up)
37051
+ "nesw-resize",
37052
+ // 45°
37053
+ "ew-resize",
37054
+ // 90° (right)
37055
+ "nwse-resize",
37056
+ // 135°
37057
+ "ns-resize",
37058
+ // 180° (down)
37059
+ "nesw-resize",
37060
+ // 225°
37061
+ "ew-resize",
37062
+ // 270° (left)
37063
+ "nwse-resize"
37064
+ // 315°
37065
+ ];
37066
+ var CORNER_BASE_ANGLE_DEG = {
37067
+ nw: 315,
37068
+ ne: 45,
37069
+ se: 135,
37070
+ sw: 225
37071
+ };
37072
+ function resolveRotatedResizeCursor(handle, rotationDeg) {
37073
+ const angle = CORNER_BASE_ANGLE_DEG[handle] + rotationDeg;
37074
+ const normalized = (angle % 360 + 360) % 360;
37075
+ const bucket = Math.round(normalized / 45) % 8;
37076
+ return CURSORS_8[bucket];
37077
+ }
37078
+ function computeNextResizeAnchor(prev, fixedStart, fixedNow) {
37079
+ const base2 = prev ?? { dx: 0, dy: 0 };
37080
+ return { dx: base2.dx + (fixedStart.x - fixedNow.x), dy: base2.dy + (fixedStart.y - fixedNow.y) };
37081
+ }
37082
+
36950
37083
  // src/components/editor/manualOffsetDrag.ts
36951
37084
  import { isHtmlElement as isHtmlElement6 } from "@hyperframes/core/runtime/dom-realm";
36952
37085
 
@@ -37542,9 +37675,14 @@ function readDragStamp(element) {
37542
37675
  origX: read2("data-hf-drag-initial-offset-x") || 0,
37543
37676
  origY: read2("data-hf-drag-initial-offset-y") || 0,
37544
37677
  baseX: read2("data-hf-drag-gsap-base-x"),
37545
- baseY: read2("data-hf-drag-gsap-base-y")
37678
+ baseY: read2("data-hf-drag-gsap-base-y"),
37679
+ at: readEditMoment(element)
37546
37680
  };
37547
37681
  }
37682
+ function readEditMoment(element) {
37683
+ const raw = element.getAttribute(STUDIO_EDIT_MOMENT_ATTR);
37684
+ return raw ? JSON.parse(raw) : void 0;
37685
+ }
37548
37686
  var freezeDragStamp = (element) => ({
37549
37687
  ...readDragStamp(element),
37550
37688
  frozen: true
@@ -37876,7 +38014,8 @@ function createManualOffsetDragMember(input) {
37876
38014
  const initialPathOffset = captureStudioPathOffset(input.element);
37877
38015
  const gestureToken = beginStudioManualEditGesture(
37878
38016
  input.element,
37879
- input.gesture === "resize" ? "resize" : "move"
38017
+ input.gesture === "resize" ? "resize" : "move",
38018
+ input.at
37880
38019
  );
37881
38020
  const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
37882
38021
  scaleX: input.rect.editScaleX,
@@ -37991,6 +38130,7 @@ function settleDraftTranslate(element, gsapOwnsPosition) {
37991
38130
  }
37992
38131
  function endManualOffsetDragMembers(members) {
37993
38132
  for (const member of members) {
38133
+ if (!isStudioManualEditGestureCurrent(member.element, member.gestureToken)) continue;
37994
38134
  endStudioManualEditGesture(member.element, member.gestureToken);
37995
38135
  member.element.removeAttribute("data-hf-drag-initial-offset-x");
37996
38136
  member.element.removeAttribute("data-hf-drag-initial-offset-y");
@@ -38170,119 +38310,6 @@ function readRotationBase(element, plain) {
38170
38310
  return gsapRotation + readStudioRotation(element).angle;
38171
38311
  }
38172
38312
 
38173
- // src/components/editor/domEditResizeLocal.ts
38174
- var MIN_RESIZE_LOCAL_PX = 1;
38175
- var DEGENERATE_START_DIST_PX = 3;
38176
- function resolveCenterResizeScale(input) {
38177
- const startDist = Math.hypot(
38178
- input.pointerStart.x - input.centerStart.x,
38179
- input.pointerStart.y - input.centerStart.y
38180
- );
38181
- if (!Number.isFinite(startDist) || startDist < DEGENERATE_START_DIST_PX) return 1;
38182
- const nowDist = Math.hypot(
38183
- input.pointer.x - input.centerStart.x,
38184
- input.pointer.y - input.centerStart.y
38185
- );
38186
- return nowDist / startDist;
38187
- }
38188
- function resolveCenterResizeSize(input) {
38189
- const baseWidth = Math.max(input.baseWidth, MIN_RESIZE_LOCAL_PX);
38190
- const baseHeight = Math.max(input.baseHeight, MIN_RESIZE_LOCAL_PX);
38191
- const rawScale = resolveCenterResizeScale({
38192
- pointer: input.pointer,
38193
- pointerStart: input.pointerStart,
38194
- centerStart: input.centerStart
38195
- });
38196
- const minScale = MIN_RESIZE_LOCAL_PX / Math.min(baseWidth, baseHeight);
38197
- const scale = Math.max(minScale, rawScale);
38198
- return { width: baseWidth * scale, height: baseHeight * scale };
38199
- }
38200
- var CURSORS_8 = [
38201
- "ns-resize",
38202
- // 0° (up)
38203
- "nesw-resize",
38204
- // 45°
38205
- "ew-resize",
38206
- // 90° (right)
38207
- "nwse-resize",
38208
- // 135°
38209
- "ns-resize",
38210
- // 180° (down)
38211
- "nesw-resize",
38212
- // 225°
38213
- "ew-resize",
38214
- // 270° (left)
38215
- "nwse-resize"
38216
- // 315°
38217
- ];
38218
- var CORNER_BASE_ANGLE_DEG = {
38219
- nw: 315,
38220
- ne: 45,
38221
- se: 135,
38222
- sw: 225
38223
- };
38224
- function resolveRotatedResizeCursor(handle, rotationDeg) {
38225
- const angle = CORNER_BASE_ANGLE_DEG[handle] + rotationDeg;
38226
- const normalized = (angle % 360 + 360) % 360;
38227
- const bucket = Math.round(normalized / 45) % 8;
38228
- return CURSORS_8[bucket];
38229
- }
38230
- function computeNextResizeAnchor(prev, fixedStart, fixedNow) {
38231
- const base2 = prev ?? { dx: 0, dy: 0 };
38232
- return { dx: base2.dx + (fixedStart.x - fixedNow.x), dy: base2.dy + (fixedStart.y - fixedNow.y) };
38233
- }
38234
-
38235
- // src/components/editor/resizeDraft.ts
38236
- function resolveResizeAnchor(g, corners, measureOrientedRect) {
38237
- const fixedStart = g.resizeFixedCenterStart;
38238
- if (corners && fixedStart) {
38239
- return computeNextResizeAnchor(g.lastResizeAnchor, fixedStart, overlayCornersCentroid(corners));
38240
- }
38241
- const fallbackRect = measureOrientedRect();
38242
- return resolveResizeCenterAnchorOffset({
38243
- originWidth: g.originWidth,
38244
- originHeight: g.originHeight,
38245
- overlayWidth: fallbackRect ? fallbackRect.width : g.originWidth,
38246
- overlayHeight: fallbackRect ? fallbackRect.height : g.originHeight
38247
- });
38248
- }
38249
- function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect) {
38250
- const corners = overlayEl && iframe ? elementCornerOverlayPoints(overlayEl, iframe, element) : null;
38251
- const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect);
38252
- g.lastResizeAnchor = anchor2;
38253
- applyManualOffsetDragDraft(member, anchor2.dx, anchor2.dy);
38254
- return measureOrientedRect() ?? {
38255
- left: g.originLeft + anchor2.dx,
38256
- top: g.originTop + anchor2.dy,
38257
- width: corners ? cornerEdgeLength(corners.nw, corners.ne) : g.originWidth,
38258
- height: corners ? cornerEdgeLength(corners.nw, corners.sw) : g.originHeight,
38259
- editScaleX: g.editScaleX,
38260
- editScaleY: g.editScaleY,
38261
- angle: g.actualRotation
38262
- };
38263
- }
38264
- function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect) {
38265
- if (g.pathOffsetMember) {
38266
- return resolveAnchoredResizeDraft(
38267
- g,
38268
- g.pathOffsetMember,
38269
- element,
38270
- overlayEl,
38271
- iframe,
38272
- measureOrientedRect
38273
- );
38274
- }
38275
- return measureOrientedRect() ?? {
38276
- left: g.originLeft,
38277
- top: g.originTop,
38278
- width: g.originWidth,
38279
- height: g.originHeight,
38280
- editScaleX: g.editScaleX,
38281
- editScaleY: g.editScaleY,
38282
- angle: g.actualRotation
38283
- };
38284
- }
38285
-
38286
38313
  // src/components/editor/snapEngine.ts
38287
38314
  var SNAP_THRESHOLD_PX = 6;
38288
38315
  var SNAP_ENGAGE_TRAVEL_PX = 4;
@@ -38760,7 +38787,7 @@ function notifyBlockedPress(e, opts, selection) {
38760
38787
  if (e.button !== 0 || selection.capabilities.commitCheckPending) return;
38761
38788
  opts.onBlockedMoveRef.current(selection);
38762
38789
  }
38763
- function startGroupDrag(e, opts) {
38790
+ function startGroupDrag(e, opts, at) {
38764
38791
  const items = opts.groupOverlayItemsRef.current;
38765
38792
  if (items.length <= 1) return false;
38766
38793
  const blockedSelection = items.find(
@@ -38781,7 +38808,8 @@ function startGroupDrag(e, opts) {
38781
38808
  selection: item.selection,
38782
38809
  element: item.element,
38783
38810
  rect: item.rect,
38784
- gesture: "drag"
38811
+ gesture: "drag",
38812
+ at
38785
38813
  });
38786
38814
  if (!result.ok) {
38787
38815
  restoreManualOffsetDragMembers(members);
@@ -38839,6 +38867,7 @@ function startGroupDrag(e, opts) {
38839
38867
  return true;
38840
38868
  }
38841
38869
  function startGesture(kind, e, opts, options) {
38870
+ const at = options?.at;
38842
38871
  const sel = options?.selection ?? opts.selectionRef.current;
38843
38872
  const rect = options?.rect ?? opts.overlayRectRef.current;
38844
38873
  const box = opts.boxRef.current;
@@ -38874,7 +38903,8 @@ function startGesture(kind, e, opts, options) {
38874
38903
  selection: sel,
38875
38904
  element: sel.element,
38876
38905
  rect,
38877
- gesture: "drag"
38906
+ gesture: "drag",
38907
+ at
38878
38908
  });
38879
38909
  if (!result.ok) {
38880
38910
  opts.onBlockedMoveRef.current(result.selection, result.reason);
@@ -38892,7 +38922,8 @@ function startGesture(kind, e, opts, options) {
38892
38922
  selection: sel,
38893
38923
  element: sel.element,
38894
38924
  rect,
38895
- gesture: "resize"
38925
+ gesture: "resize",
38926
+ at
38896
38927
  });
38897
38928
  if (result.ok) {
38898
38929
  pathOffsetMember = result.member;
@@ -38900,12 +38931,13 @@ function startGesture(kind, e, opts, options) {
38900
38931
  manualEditDragToken = result.member.gestureToken;
38901
38932
  if (result.member.plainTranslate) applyManualOffsetDragDraft(result.member, 0, 0);
38902
38933
  } else {
38903
- manualEditDragToken = beginStudioManualEditGesture(sel.element, "resize");
38934
+ manualEditDragToken = beginStudioManualEditGesture(sel.element, "resize", at);
38904
38935
  }
38905
38936
  } else {
38906
38937
  manualEditDragToken = beginStudioManualEditGesture(
38907
38938
  sel.element,
38908
- kind === "rotate" ? "rotate" : "resize"
38939
+ kind === "rotate" ? "rotate" : "resize",
38940
+ at
38909
38941
  );
38910
38942
  }
38911
38943
  }
@@ -38988,6 +39020,274 @@ function startGesture(kind, e, opts, options) {
38988
39020
  return true;
38989
39021
  }
38990
39022
 
39023
+ // src/hooks/editMoment.ts
39024
+ function playheadMoment() {
39025
+ const { currentTime, activeKeyframePct } = usePlayerStore.getState();
39026
+ return { time: currentTime, keyframePct: activeKeyframePct };
39027
+ }
39028
+ function editMoment(stamp) {
39029
+ return stamp?.at ?? playheadMoment();
39030
+ }
39031
+
39032
+ // src/components/editor/previewGestureStarts.ts
39033
+ var MAX_SETTLED_WAIT_FRAMES = 60;
39034
+ var PRESS_WAIT_MAX_MS = 1e4;
39035
+ function createPreviewGestureStarts(opts, moveActiveGesture, releaseActiveGesture) {
39036
+ const offerUndo = () => {
39037
+ let p = opts.waitingPressRef.current;
39038
+ while (p && !p.released) p = p.after;
39039
+ const press = p;
39040
+ setStudioWaitingPressCancel(press && (() => dropPress(press)));
39041
+ };
39042
+ const clearWaitingMark = () => {
39043
+ opts.boxRef.current?.removeAttribute(PRESS_WAITING_ATTR);
39044
+ opts.rafPausedRef.current = false;
39045
+ };
39046
+ const unlink = (press, next) => {
39047
+ for (let p = opts.waitingPressRef.current; p; p = p.after)
39048
+ if (p.after === press) p.after = next;
39049
+ };
39050
+ const endWaitingPress = (press = opts.waitingPressRef.current) => {
39051
+ for (let p = press; p && !p.ended; p = p.after) {
39052
+ p.ended = true;
39053
+ cancelAnimationFrame(p.frame);
39054
+ }
39055
+ if (press === opts.waitingPressRef.current) {
39056
+ opts.waitingPressRef.current = null;
39057
+ clearWaitingMark();
39058
+ } else if (press) unlink(press, null);
39059
+ offerUndo();
39060
+ };
39061
+ const showPressBefore = (press) => {
39062
+ const before = press.after;
39063
+ opts.waitingPressRef.current = before;
39064
+ const shown2 = before ?? press;
39065
+ const at = before?.moved;
39066
+ const [dx, dy] = at ? [at.clientX - shown2.startX, at.clientY - shown2.startY] : [0, 0];
39067
+ shown2.draw?.(dx, dy);
39068
+ if (!before) clearWaitingMark();
39069
+ };
39070
+ const dropPress = (press) => {
39071
+ if (press.ended) return false;
39072
+ press.ended = true;
39073
+ cancelAnimationFrame(press.frame);
39074
+ if (press === opts.waitingPressRef.current) showPressBefore(press);
39075
+ else unlink(press, press.after);
39076
+ offerUndo();
39077
+ return true;
39078
+ };
39079
+ const endHeldPress = () => {
39080
+ const held = opts.waitingPressRef.current;
39081
+ if (held && !held.released) dropPress(held);
39082
+ };
39083
+ const releaseWaitingPress = (press, e) => {
39084
+ press.released = e;
39085
+ offerUndo();
39086
+ };
39087
+ const drawPressedBox = (origin2) => origin2 && ((dx, dy) => opts.setOverlayRect(shiftedOverlayRect(origin2, dx, dy)));
39088
+ const drawResizedBox = (origin2, press) => {
39089
+ if (!origin2) return null;
39090
+ const bounds = opts.overlayRef.current?.getBoundingClientRect();
39091
+ const centre = {
39092
+ x: (bounds?.left ?? 0) + origin2.left + origin2.width / 2,
39093
+ y: (bounds?.top ?? 0) + origin2.top + origin2.height / 2
39094
+ };
39095
+ return (dx, dy) => {
39096
+ const pointer = { x: press.x + dx, y: press.y + dy };
39097
+ const scale = resolveCenterResizeScale({ pointer, pointerStart: press, centerStart: centre });
39098
+ const [width, height] = [origin2.width * scale, origin2.height * scale];
39099
+ opts.setOverlayRect({
39100
+ ...origin2,
39101
+ left: origin2.left + (origin2.width - width) / 2,
39102
+ top: origin2.top + (origin2.height - height) / 2,
39103
+ width,
39104
+ height
39105
+ });
39106
+ };
39107
+ };
39108
+ const startOnShownPreview = (e, pressed, start, draw = null) => {
39109
+ const at = playheadMoment();
39110
+ if (!isPreviewChanging() && !opts.waitingPressRef.current) return start(e, at, false);
39111
+ e.preventDefault();
39112
+ e.stopPropagation();
39113
+ e.currentTarget.setPointerCapture(e.pointerId);
39114
+ const held = { setPointerCapture() {
39115
+ } };
39116
+ const down = { ...e, currentTarget: held, preventDefault() {
39117
+ }, stopPropagation() {
39118
+ } };
39119
+ const press = {
39120
+ pointerId: e.pointerId,
39121
+ startX: e.clientX,
39122
+ startY: e.clientY,
39123
+ draw,
39124
+ frame: 0,
39125
+ moved: null,
39126
+ released: null,
39127
+ after: opts.waitingPressRef.current,
39128
+ ended: false
39129
+ };
39130
+ opts.waitingPressRef.current = press;
39131
+ opts.boxRef.current?.setAttribute(PRESS_WAITING_ATTR, "true");
39132
+ opts.rafPausedRef.current = true;
39133
+ let shownFrames = 0;
39134
+ let settledFrames = 0;
39135
+ const since = performance.now();
39136
+ const countFrame = () => {
39137
+ const elements = pressed();
39138
+ const live = opts.iframeRef.current?.contentDocument;
39139
+ const settled = !isPreviewChanging();
39140
+ const shown2 = settled && elements.every((el) => el.ownerDocument === live);
39141
+ shownFrames = shown2 ? shownFrames + 1 : 0;
39142
+ settledFrames = settled ? settledFrames + 1 : 0;
39143
+ return elements.length;
39144
+ };
39145
+ const waitStep = () => {
39146
+ if (press.after && !press.after.ended) return "wait";
39147
+ const found = countFrame();
39148
+ const overdue = performance.now() - since > PRESS_WAIT_MAX_MS;
39149
+ if (overdue) giveUpOnPreviewChange();
39150
+ if (found === 0 || settledFrames > MAX_SETTLED_WAIT_FRAMES) return "lost";
39151
+ return overdue || shownFrames >= 2 ? "run" : "wait";
39152
+ };
39153
+ const poll = () => {
39154
+ if (press.ended) return;
39155
+ const step = waitStep();
39156
+ if (step === "wait") {
39157
+ press.frame = requestAnimationFrame(poll);
39158
+ return;
39159
+ }
39160
+ endWaitingPress(press);
39161
+ if (step === "lost" || !start(down, at, true))
39162
+ return;
39163
+ if (press.moved) moveActiveGesture(press.moved);
39164
+ if (press.released) releaseActiveGesture(press.released);
39165
+ };
39166
+ press.frame = requestAnimationFrame(poll);
39167
+ return true;
39168
+ };
39169
+ const shownSelection = (selection) => {
39170
+ const doc = opts.iframeRef.current?.contentDocument;
39171
+ if (!doc) return null;
39172
+ const element = findElementForSelection(doc, selection, opts.activeCompositionPathRef.current);
39173
+ return element && { ...selection, element };
39174
+ };
39175
+ const shownRect = (element) => {
39176
+ const overlay = opts.overlayRef.current;
39177
+ const iframe = opts.iframeRef.current;
39178
+ return overlay && iframe && orientedGroupAwareOverlayRect(overlay, iframe, element);
39179
+ };
39180
+ const startGroupDrag2 = (e) => {
39181
+ const items = opts.groupOverlayItemsRef.current;
39182
+ return startOnShownPreview(
39183
+ e,
39184
+ () => items.flatMap((item) => {
39185
+ const selection = shownSelection(item.selection);
39186
+ return selection ? [selection.element] : [];
39187
+ }),
39188
+ (pressed, at, waited) => {
39189
+ if (!waited) return startGroupDrag(pressed, opts, at);
39190
+ const overlay = opts.overlayRef.current;
39191
+ const iframe = opts.iframeRef.current;
39192
+ if (!overlay || !iframe) return false;
39193
+ const scale = computeOverlayRootScale(overlay, iframe, iframe.contentDocument);
39194
+ const measured = items.flatMap((item) => {
39195
+ const selection = shownSelection(item.selection);
39196
+ const rect = selection && orientedGroupAwareOverlayRect(overlay, iframe, selection.element, scale);
39197
+ return selection && rect ? [{ ...item, selection, element: selection.element, rect }] : [];
39198
+ });
39199
+ if (measured.length !== items.length) return false;
39200
+ return startGroupDrag(
39201
+ pressed,
39202
+ { ...opts, groupOverlayItemsRef: { current: measured } },
39203
+ at
39204
+ );
39205
+ },
39206
+ (dx, dy) => opts.setGroupOverlayItems(
39207
+ items.map((item) => ({ ...item, rect: shiftedOverlayRect(item.rect, dx, dy) }))
39208
+ )
39209
+ );
39210
+ };
39211
+ const startGesture2 = (kind, e, options) => {
39212
+ const target = options?.selection ?? opts.selectionRef.current;
39213
+ let draw = null;
39214
+ if (kind === "drag") draw = drawPressedBox(opts.overlayRectRef.current);
39215
+ if (kind === "resize")
39216
+ draw = drawResizedBox(opts.overlayRectRef.current, { x: e.clientX, y: e.clientY });
39217
+ return startOnShownPreview(
39218
+ e,
39219
+ () => {
39220
+ const shown2 = target && shownSelection(target);
39221
+ return shown2 ? [shown2.element] : [];
39222
+ },
39223
+ (pressed, at, waited) => {
39224
+ if (!waited) return startGesture(kind, pressed, opts, { ...options, at });
39225
+ const selection = target && shownSelection(target);
39226
+ const rect = selection && shownRect(selection.element);
39227
+ return !!(selection && rect) && startGesture(kind, pressed, opts, {
39228
+ selection,
39229
+ rect,
39230
+ resizeHandle: options?.resizeHandle,
39231
+ at
39232
+ });
39233
+ },
39234
+ draw
39235
+ );
39236
+ };
39237
+ return { startGesture: startGesture2, startGroupDrag: startGroupDrag2, endWaitingPress, endHeldPress, releaseWaitingPress };
39238
+ }
39239
+
39240
+ // src/components/editor/resizeDraft.ts
39241
+ function resolveResizeAnchor(g, corners, measureOrientedRect) {
39242
+ const fixedStart = g.resizeFixedCenterStart;
39243
+ if (corners && fixedStart) {
39244
+ return computeNextResizeAnchor(g.lastResizeAnchor, fixedStart, overlayCornersCentroid(corners));
39245
+ }
39246
+ const fallbackRect = measureOrientedRect();
39247
+ return resolveResizeCenterAnchorOffset({
39248
+ originWidth: g.originWidth,
39249
+ originHeight: g.originHeight,
39250
+ overlayWidth: fallbackRect ? fallbackRect.width : g.originWidth,
39251
+ overlayHeight: fallbackRect ? fallbackRect.height : g.originHeight
39252
+ });
39253
+ }
39254
+ function resolveAnchoredResizeDraft(g, member, element, overlayEl, iframe, measureOrientedRect) {
39255
+ const corners = overlayEl && iframe ? elementCornerOverlayPoints(overlayEl, iframe, element) : null;
39256
+ const anchor2 = resolveResizeAnchor(g, corners, measureOrientedRect);
39257
+ g.lastResizeAnchor = anchor2;
39258
+ applyManualOffsetDragDraft(member, anchor2.dx, anchor2.dy);
39259
+ return measureOrientedRect() ?? {
39260
+ left: g.originLeft + anchor2.dx,
39261
+ top: g.originTop + anchor2.dy,
39262
+ width: corners ? cornerEdgeLength(corners.nw, corners.ne) : g.originWidth,
39263
+ height: corners ? cornerEdgeLength(corners.nw, corners.sw) : g.originHeight,
39264
+ editScaleX: g.editScaleX,
39265
+ editScaleY: g.editScaleY,
39266
+ angle: g.actualRotation
39267
+ };
39268
+ }
39269
+ function resolveResizeDraftRect(g, element, overlayEl, iframe, measureOrientedRect) {
39270
+ if (g.pathOffsetMember) {
39271
+ return resolveAnchoredResizeDraft(
39272
+ g,
39273
+ g.pathOffsetMember,
39274
+ element,
39275
+ overlayEl,
39276
+ iframe,
39277
+ measureOrientedRect
39278
+ );
39279
+ }
39280
+ return measureOrientedRect() ?? {
39281
+ left: g.originLeft,
39282
+ top: g.originTop,
39283
+ width: g.originWidth,
39284
+ height: g.originHeight,
39285
+ editScaleX: g.editScaleX,
39286
+ editScaleY: g.editScaleY,
39287
+ angle: g.actualRotation
39288
+ };
39289
+ }
39290
+
38991
39291
  // src/components/editor/groupDragMove.ts
38992
39292
  function createGroupDragMover(opts, setDraftGroupOverlayItems) {
38993
39293
  let lastGroupPositions = {};
@@ -39147,7 +39447,7 @@ function logGestureCommitFailure(message, error) {
39147
39447
  }
39148
39448
  function createDomEditOverlayGestureHandlers(opts) {
39149
39449
  const setDraftOverlayRect = (next) => {
39150
- opts.setOverlayRect(next);
39450
+ if (!opts.waitingPressRef.current) opts.setOverlayRect(next);
39151
39451
  };
39152
39452
  const restoreGestureOverlayRect = (g) => {
39153
39453
  setDraftOverlayRect({
@@ -39157,21 +39457,22 @@ function createDomEditOverlayGestureHandlers(opts) {
39157
39457
  height: g.originHeight,
39158
39458
  editScaleX: g.editScaleX,
39159
39459
  editScaleY: g.editScaleY,
39160
- // Every draft rect must carry the element's rotation: the rotation wrapper
39161
- // renders rotate(overlayRect.angle), so an omitted angle straightens the
39162
- // chrome for the duration of the draft (the "straightens while moving" bug).
39460
+ // Rotation keeps the chrome aligned with the element during its draft.
39163
39461
  angle: g.actualRotation
39164
39462
  });
39165
39463
  };
39166
39464
  const setDraftGroupOverlayItems = (next) => {
39167
- opts.setGroupOverlayItems(next);
39465
+ if (!opts.waitingPressRef.current) opts.setGroupOverlayItems(next);
39168
39466
  };
39169
39467
  const restoreGroupPathOffsets = (g) => {
39170
39468
  restoreManualOffsetDragMembers(g.members);
39171
39469
  setDraftGroupOverlayItems(g.originItems);
39172
39470
  };
39173
- const startGroupDrag2 = (e) => startGroupDrag(e, opts);
39174
- const startGesture2 = (kind, e, options) => startGesture(kind, e, opts, options);
39471
+ const { startGesture: startGesture2, startGroupDrag: startGroupDrag2, endWaitingPress, endHeldPress, releaseWaitingPress } = createPreviewGestureStarts(
39472
+ opts,
39473
+ (event) => moveActiveGesture(event),
39474
+ (event) => releaseActiveGesture(event)
39475
+ );
39175
39476
  const startBlockedMove = (e, selection) => {
39176
39477
  e.preventDefault();
39177
39478
  e.stopPropagation();
@@ -39181,10 +39482,20 @@ function createDomEditOverlayGestureHandlers(opts) {
39181
39482
  };
39182
39483
  const moveGroupDrag = createGroupDragMover(opts, setDraftGroupOverlayItems);
39183
39484
  const onPointerMove = (e) => {
39485
+ const waiting = opts.waitingPressRef.current;
39486
+ if (waiting && waiting.pointerId === e.pointerId) {
39487
+ if (waiting.released || !movesGesture(waiting, e)) return;
39488
+ waiting.moved = e;
39489
+ waiting.draw?.(e.clientX - waiting.startX, e.clientY - waiting.startY);
39490
+ return;
39491
+ }
39492
+ moveActiveGesture(e);
39493
+ };
39494
+ const moveActiveGesture = (e) => {
39184
39495
  const g = opts.gestureRef.current;
39185
39496
  const groupG = opts.groupGestureRef.current;
39186
39497
  const sel = g?.selection ?? opts.selectionRef.current;
39187
- const box = opts.boxRef.current;
39498
+ const box = opts.waitingPressRef.current ? null : opts.boxRef.current;
39188
39499
  const blockedMove = opts.blockedMoveRef.current;
39189
39500
  if (!blockedMove && !g && !groupG) {
39190
39501
  opts.onCanvasPointerMoveRef.current(e, { preferClipAncestor: false });
@@ -39288,7 +39599,6 @@ function createDomEditOverlayGestureHandlers(opts) {
39288
39599
  }
39289
39600
  if (g.pathOffsetMember) applyManualOffsetDragDraft(g.pathOffsetMember, dx, dy);
39290
39601
  } else {
39291
- if (!box) return;
39292
39602
  const nextSize = resolveCenterResizeSize({
39293
39603
  baseWidth: g.actualWidth,
39294
39604
  baseHeight: g.actualHeight,
@@ -39314,23 +39624,34 @@ function createDomEditOverlayGestureHandlers(opts) {
39314
39624
  draftRect,
39315
39625
  liveInlineStyle: sel.element.getAttribute("style")
39316
39626
  });
39317
- box.style.left = `${draftRect.left}px`;
39318
- box.style.top = `${draftRect.top}px`;
39319
- box.style.width = `${draftRect.width}px`;
39320
- box.style.height = `${draftRect.height}px`;
39627
+ if (box) {
39628
+ box.style.left = `${draftRect.left}px`;
39629
+ box.style.top = `${draftRect.top}px`;
39630
+ box.style.width = `${draftRect.width}px`;
39631
+ box.style.height = `${draftRect.height}px`;
39632
+ }
39321
39633
  setDraftOverlayRect(draftRect);
39322
39634
  }
39323
39635
  };
39324
39636
  const onPointerUp = (e) => {
39637
+ const waiting = opts.waitingPressRef.current;
39638
+ if (waiting && waiting.pointerId === e.pointerId) {
39639
+ releaseWaitingPress(waiting, e);
39640
+ opts.suppressNextBoxClickRef.current = true;
39641
+ return;
39642
+ }
39643
+ releaseActiveGesture(e);
39644
+ };
39645
+ const releaseActiveGesture = (e) => {
39325
39646
  opts.snapGuidesRef.current = null;
39326
39647
  const g = opts.gestureRef.current;
39327
39648
  const groupG = opts.groupGestureRef.current;
39328
39649
  const sel = g?.selection ?? opts.selectionRef.current;
39329
- const box = opts.boxRef.current;
39650
+ const box = opts.waitingPressRef.current ? null : opts.boxRef.current;
39330
39651
  opts.blockedMoveRef.current = null;
39652
+ opts.rafPausedRef.current = opts.waitingPressRef.current !== null;
39331
39653
  if (groupG) {
39332
39654
  opts.groupGestureRef.current = null;
39333
- opts.rafPausedRef.current = false;
39334
39655
  const rawDx = e.clientX - groupG.startX;
39335
39656
  const rawDy = e.clientY - groupG.startY;
39336
39657
  opts.suppressNextBoxClickRef.current = true;
@@ -39349,7 +39670,7 @@ function createDomEditOverlayGestureHandlers(opts) {
39349
39670
  setDraftGroupOverlayItems(
39350
39671
  groupG.originItems.map((item) => ({
39351
39672
  ...item,
39352
- rect: { ...item.rect, left: item.rect.left + dx, top: item.rect.top + dy }
39673
+ rect: shiftedOverlayRect(item.rect, dx, dy)
39353
39674
  }))
39354
39675
  );
39355
39676
  const updates = groupG.members.map((member) => ({
@@ -39384,13 +39705,8 @@ function createDomEditOverlayGestureHandlers(opts) {
39384
39705
  groupEdit.settle(groupSaved);
39385
39706
  return;
39386
39707
  }
39387
- if (!g || !sel) {
39388
- opts.gestureRef.current = null;
39389
- opts.rafPausedRef.current = false;
39390
- return;
39391
- }
39392
39708
  opts.gestureRef.current = null;
39393
- opts.rafPausedRef.current = false;
39709
+ if (!g || !sel) return;
39394
39710
  const movedDistance = Math.hypot(e.clientX - g.startX, e.clientY - g.startY);
39395
39711
  if (g.kind === "drag" && isTap(g, e)) {
39396
39712
  if (g.pathOffsetMember) restoreManualOffsetDragMembers([g.pathOffsetMember]);
@@ -39523,7 +39839,9 @@ function createDomEditOverlayGestureHandlers(opts) {
39523
39839
  logResizeSettle(sel.element, "post-release");
39524
39840
  }
39525
39841
  };
39526
- const clearPointerState = (selectionRef) => {
39842
+ const clearPointerState = (selectionRef, dropQueued = false) => {
39843
+ if (dropQueued) endWaitingPress();
39844
+ else endHeldPress();
39527
39845
  opts.snapGuidesRef.current = null;
39528
39846
  const groupG = opts.groupGestureRef.current;
39529
39847
  if (groupG) restoreGroupPathOffsets(groupG);
@@ -39554,7 +39872,12 @@ function createDomEditOverlayGestureHandlers(opts) {
39554
39872
  opts.blockedMoveRef.current = null;
39555
39873
  opts.groupGestureRef.current = null;
39556
39874
  opts.gestureRef.current = null;
39557
- opts.rafPausedRef.current = false;
39875
+ opts.rafPausedRef.current = opts.waitingPressRef.current !== null;
39876
+ };
39877
+ const onLostPointerCapture = (e) => {
39878
+ const waiting = opts.waitingPressRef.current;
39879
+ if (waiting?.pointerId === e.pointerId && waiting.released) return;
39880
+ clearPointerState(opts.selectionRef);
39558
39881
  };
39559
39882
  return {
39560
39883
  startGesture: startGesture2,
@@ -39562,6 +39885,7 @@ function createDomEditOverlayGestureHandlers(opts) {
39562
39885
  startBlockedMove,
39563
39886
  onPointerMove,
39564
39887
  onPointerUp,
39888
+ onLostPointerCapture,
39565
39889
  clearPointerState
39566
39890
  };
39567
39891
  }
@@ -41998,6 +42322,7 @@ var DomEditOverlay = memo28(function DomEditOverlay2({
41998
42322
  const gestureRef = useRef70(null);
41999
42323
  const groupGestureRef = useRef70(null);
42000
42324
  const blockedMoveRef = useRef70(null);
42325
+ const waitingPressRef = useRef70(null);
42001
42326
  const suppressNextBoxClickRef = useRef70(false);
42002
42327
  const snapGuidesRef = useRef70(null);
42003
42328
  const rafPausedRef = useRef70(false);
@@ -42086,6 +42411,7 @@ var DomEditOverlay = memo28(function DomEditOverlay2({
42086
42411
  activeCompositionPath
42087
42412
  });
42088
42413
  const gestures = createDomEditOverlayGestureHandlers({
42414
+ activeCompositionPathRef,
42089
42415
  overlayRef,
42090
42416
  iframeRef,
42091
42417
  boxRef,
@@ -42096,6 +42422,7 @@ var DomEditOverlay = memo28(function DomEditOverlay2({
42096
42422
  gestureRef,
42097
42423
  groupGestureRef,
42098
42424
  blockedMoveRef,
42425
+ waitingPressRef,
42099
42426
  rafPausedRef,
42100
42427
  suppressNextBoxClickRef,
42101
42428
  setOverlayRect,
@@ -42111,17 +42438,17 @@ var DomEditOverlay = memo28(function DomEditOverlay2({
42111
42438
  snapGuidesRef
42112
42439
  });
42113
42440
  useEffect55(() => {
42114
- if (readOnly) gestures.clearPointerState(selectionRef);
42441
+ if (readOnly) gestures.clearPointerState(selectionRef, true);
42115
42442
  }, [gestures, readOnly, selectionRef]);
42116
- const cancelGestureRef = useRef70(() => {
42443
+ const cancelGestureRef = useRef70((_dropQueued) => {
42117
42444
  });
42118
- cancelGestureRef.current = () => gestures.clearPointerState(selectionRef);
42445
+ cancelGestureRef.current = (dropQueued) => gestures.clearPointerState(selectionRef, dropQueued);
42119
42446
  useMountEffect(() => {
42120
42447
  const cancel = () => cancelGestureRef.current();
42121
42448
  window.addEventListener("blur", cancel);
42122
42449
  return () => {
42123
42450
  window.removeEventListener("blur", cancel);
42124
- cancel();
42451
+ cancelGestureRef.current(true);
42125
42452
  };
42126
42453
  });
42127
42454
  const { flushNudge } = useDomEditNudge({
@@ -42253,7 +42580,7 @@ var DomEditOverlay = memo28(function DomEditOverlay2({
42253
42580
  onPointerLeave: () => onCanvasPointerLeaveRef.current(),
42254
42581
  onPointerUp: marquee.onPointerUp,
42255
42582
  onPointerCancel: marquee.onPointerCancel,
42256
- onLostPointerCapture: () => cancelGestureRef.current(),
42583
+ onLostPointerCapture: gestures.onLostPointerCapture,
42257
42584
  onContextMenu: hostInput ? void 0 : handleContextMenu,
42258
42585
  children: [
42259
42586
  !hostInput && hoverSelection && hoverRect && compRect.width > 0 && /* @__PURE__ */ jsx102(
@@ -45807,8 +46134,8 @@ import { isTweenConfigKey } from "@hyperframes/parsers/gsap-constants";
45807
46134
  function newTweenTarget(selection) {
45808
46135
  return writeTargetSelector(selection);
45809
46136
  }
45810
- function computeCurrentPercentage(selection, animation) {
45811
- return computeElementPercentage(usePlayerStore.getState().currentTime, selection, animation);
46137
+ function computeCurrentPercentage(selection, animation, time = usePlayerStore.getState().currentTime) {
46138
+ return computeElementPercentage(time, selection, animation);
45812
46139
  }
45813
46140
  function parkPlayheadOnKeyframe(anim, pct) {
45814
46141
  const time = percentageToAbsoluteForAnimation(pct, anim);
@@ -46173,9 +46500,8 @@ function animHasPosition(anim) {
46173
46500
  }
46174
46501
  return "x" in anim.properties || "y" in anim.properties;
46175
46502
  }
46176
- function findGsapPositionAnimation(animations, selector) {
46503
+ function findGsapPositionAnimation(animations, selector, currentTime = usePlayerStore.getState().currentTime) {
46177
46504
  if (animations.length === 0) return null;
46178
- const currentTime = usePlayerStore.getState().currentTime;
46179
46505
  const scored = animations.filter(animHasPosition).map((a) => {
46180
46506
  let score = 0;
46181
46507
  if (a.keyframes) score += 5;
@@ -46203,9 +46529,8 @@ function statesValueAt(anim, time) {
46203
46529
  if (anim.method === "from") return near4(0);
46204
46530
  return near4(100) || anim.method === "fromTo" && near4(0);
46205
46531
  }
46206
- function pickClosestToPlayhead(anims) {
46532
+ function pickClosestToPlayhead(anims, ct = usePlayerStore.getState().currentTime) {
46207
46533
  if (anims.length <= 1) return anims[0] ?? null;
46208
- const ct = usePlayerStore.getState().currentTime;
46209
46534
  return anims.reduce((best, a) => {
46210
46535
  const dist = playheadDistance(a, ct);
46211
46536
  const bestDist = playheadDistance(best, ct);
@@ -46670,9 +46995,13 @@ function planValueAtPlayhead(edit) {
46670
46995
  added: keyframes.length > authored
46671
46996
  };
46672
46997
  }
46673
- function planValueEdit(selection, anim, values, iframe, { backfill, holdFromStart } = {}) {
46998
+ function planValueEdit(selection, anim, values, iframe, {
46999
+ backfill,
47000
+ holdFromStart,
47001
+ moment = playheadMoment()
47002
+ } = {}) {
46674
47003
  if (tweenReach(anim, selection.element) === "shared") return refuse("shared-tween");
46675
- const { activeKeyframePct, currentTime } = usePlayerStore.getState();
47004
+ const { keyframePct: activeKeyframePct, time: currentTime } = moment;
46676
47005
  const tween = findParsedTween(iframe, selection.element, anim);
46677
47006
  const timed = withExactStepTimes(anim, tween);
46678
47007
  return planValueAtPlayhead({
@@ -46687,10 +47016,11 @@ function planValueEdit(selection, anim, values, iframe, { backfill, holdFromStar
46687
47016
  }
46688
47017
  async function commitValueAtPlayhead(selection, anim, values, iframe, callbacks, options) {
46689
47018
  await materializeIfDynamic(anim, iframe, callbacks.commitMutation, selection);
46690
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
47019
+ const moment = editMoment(callbacks.stamp);
47020
+ const { setActiveKeyframePct } = usePlayerStore.getState();
46691
47021
  const tween = findParsedTween(iframe, selection.element, anim);
46692
- const parkAt = activeKeyframePct == null ? null : exactKeyframePct(anim, tween, activeKeyframePct);
46693
- const plan = planValueEdit(selection, anim, values, iframe, options);
47022
+ const parkAt = moment.keyframePct == null ? null : exactKeyframePct(anim, tween, moment.keyframePct);
47023
+ const plan = planValueEdit(selection, anim, values, iframe, { ...options, moment });
46694
47024
  if (!plan.ok) return { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
46695
47025
  await callbacks.commitMutation(selection, plan.mutation, {
46696
47026
  label: options.label,
@@ -47936,7 +48266,7 @@ function buildExtendedKeyframes(anim, currentTime, position, sourceDuration = re
47936
48266
  const keyframes = [...rescaled, added].sort((a, b) => a.percentage - b.percentage);
47937
48267
  return { position: roundTo3(newStart), duration: newDuration, keyframes };
47938
48268
  }
47939
- function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, iframe) {
48269
+ function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, iframe, moment) {
47940
48270
  if (anim.arcPath?.enabled) return { status: "persisted" };
47941
48271
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
47942
48272
  selection.element,
@@ -47944,7 +48274,8 @@ function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, ifr
47944
48274
  gsapPos
47945
48275
  );
47946
48276
  const plan = planValueEdit(selection, anim, { x: newX, y: newY }, iframe, {
47947
- backfill: { x: baseGsapX, y: baseGsapY }
48277
+ backfill: { x: baseGsapX, y: baseGsapY },
48278
+ moment
47948
48279
  });
47949
48280
  return plan.ok ? { status: "persisted" } : { status: "blocked", reason: "keyframes-uneditable", detail: plan.reason };
47950
48281
  }
@@ -47959,7 +48290,8 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
47959
48290
  );
47960
48291
  const restoreOffset = () => restoreDragOffset(el, stamp);
47961
48292
  if (anim.arcPath?.enabled) {
47962
- const { activeKeyframePct, currentTime, setActiveKeyframePct } = usePlayerStore.getState();
48293
+ const { keyframePct: activeKeyframePct, time: currentTime } = editMoment(stamp);
48294
+ const { setActiveKeyframePct } = usePlayerStore.getState();
47963
48295
  const tweenStart = resolveTweenStart(anim);
47964
48296
  const tweenDuration = resolveTweenDuration(anim);
47965
48297
  if (activeKeyframePct === null && tweenStart !== null && !isTimeWithinTween(currentTime, tweenStart, tweenDuration)) {
@@ -47988,7 +48320,7 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
47988
48320
  );
47989
48321
  return { status: "persisted" };
47990
48322
  }
47991
- const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
48323
+ const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim, currentTime);
47992
48324
  const keyframes = anim.keyframes?.keyframes ?? [];
47993
48325
  const pointIndex = keyframes.findIndex((kf) => playsNear(anim, kf.percentage, pct));
47994
48326
  if (pointIndex >= 0) {
@@ -48031,11 +48363,18 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
48031
48363
  );
48032
48364
  return { status: "persisted" };
48033
48365
  }
48034
- return commitValueAtPlayhead(selection, anim, { x: newX, y: newY }, iframe, callbacks, {
48035
- label: "Move layer",
48036
- backfill: { x: baseGsapX, y: baseGsapY },
48037
- beforeReload: restoreOffset
48038
- });
48366
+ return commitValueAtPlayhead(
48367
+ selection,
48368
+ anim,
48369
+ { x: newX, y: newY },
48370
+ iframe,
48371
+ { ...callbacks, stamp },
48372
+ {
48373
+ label: "Move layer",
48374
+ backfill: { x: baseGsapX, y: baseGsapY },
48375
+ beforeReload: restoreOffset
48376
+ }
48377
+ );
48039
48378
  }
48040
48379
 
48041
48380
  // src/hooks/gsapElementMatch.ts
@@ -48066,12 +48405,12 @@ function getAnimationsForElement(animations, target, element) {
48066
48405
  // src/hooks/gsapRuntimeBridge.ts
48067
48406
  var POSITION_CHANNEL_SET = new Set(POSITION_CHANNELS);
48068
48407
  var ROTATION_CHANNEL_SET = new Set(ROTATION_CHANNELS);
48069
- async function resolveGroupTween(group, animations, _selection, _commitMutation, fetchFallbackAnimations) {
48408
+ async function resolveGroupTween(group, animations, _selection, _commitMutation, fetchFallbackAnimations, time) {
48070
48409
  const inGroup = (list) => {
48071
48410
  const tagged = list.filter((a) => a.propertyGroup === group);
48072
48411
  const props = new Set(PROPERTY_GROUPS[group]);
48073
48412
  const mixed = list.filter((a) => !a.propertyGroup && animationWritesAnyProperty(a, props));
48074
- return pickClosestToPlayhead(tagged.length > 0 ? tagged : mixed);
48413
+ return pickClosestToPlayhead(tagged.length > 0 ? tagged : mixed, time);
48075
48414
  };
48076
48415
  const anim = inGroup(animations);
48077
48416
  if (anim) return { anim, animations };
@@ -48138,15 +48477,22 @@ function dragRoute(posAnim, iframe, selector, altKey) {
48138
48477
  if (!hasNonHold && !hasKeyframedPosTween) return "static";
48139
48478
  return altKey || !usePlayerStore.getState().autoKeyframeEnabled ? "whole-path" : "at-playhead";
48140
48479
  }
48141
- async function planDrag(selection, offset, allAnimations, iframe, options) {
48480
+ async function planDrag(selection, offset, allAnimations, iframe, options, moment) {
48142
48481
  const selector = selectorFromSelection(selection);
48143
48482
  if (!selector) return { status: "blocked", reason: "no-selector" };
48144
48483
  const own = tweensForThisElement(selection, allAnimations);
48145
48484
  const keeper = positionWriteKeeper(own, selector);
48146
48485
  const animations = keeper ? own.filter((a) => a === keeper || !isPositionWriteOf(selector)(a)) : own;
48147
- const resolved2 = await resolveGroupTween("position", animations, selection, async () => {
48148
- });
48149
- const posAnim = resolved2?.anim ?? findGsapPositionAnimation(animations, selector);
48486
+ const resolved2 = await resolveGroupTween(
48487
+ "position",
48488
+ animations,
48489
+ selection,
48490
+ async () => {
48491
+ },
48492
+ void 0,
48493
+ moment.time
48494
+ );
48495
+ const posAnim = resolved2?.anim ?? findGsapPositionAnimation(animations, selector, moment.time);
48150
48496
  const route = dragRoute(posAnim, iframe, selector, options.altKey);
48151
48497
  if (route === "static") return { status: "persisted" };
48152
48498
  if (!posAnim) {
@@ -48157,9 +48503,12 @@ async function planDrag(selection, offset, allAnimations, iframe, options) {
48157
48503
  return step ? { status: "blocked", reason: "keyframes-uneditable", detail: step } : { status: "persisted" };
48158
48504
  }
48159
48505
  const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
48160
- return gsapPositionFromDragOutcome(selection, posAnim, offset, gsapPos, iframe);
48506
+ return gsapPositionFromDragOutcome(selection, posAnim, offset, gsapPos, iframe, moment);
48161
48507
  }
48162
48508
  async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, gestureCommit, fetchAllAnimations, options) {
48509
+ const stamp = options?.stamp ?? readDragStamp(selection.element);
48510
+ const moment = editMoment(stamp);
48511
+ const time = moment.time;
48163
48512
  if (!options?.preflightPassed) {
48164
48513
  const preflight = await preflightGsapDragIntercept(
48165
48514
  selection,
@@ -48170,7 +48519,7 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
48170
48519
  );
48171
48520
  if (preflight.status !== "persisted") return preflight;
48172
48521
  if (options?.preflightOnly) {
48173
- return options.group ? planDrag(selection, offset, allAnimations, iframe, options) : preflight;
48522
+ return options.group ? planDrag(selection, offset, allAnimations, iframe, options, moment) : preflight;
48174
48523
  }
48175
48524
  }
48176
48525
  const animations = tweensForThisElement(selection, allAnimations);
@@ -48202,16 +48551,17 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
48202
48551
  workingAnimations,
48203
48552
  selection,
48204
48553
  commitMutation,
48205
- fetchFallbackAnimations
48554
+ fetchFallbackAnimations,
48555
+ time
48206
48556
  );
48207
48557
  let posAnim = resolved2?.anim ?? null;
48208
48558
  let resolvedAnimations = resolved2?.animations ?? workingAnimations;
48209
48559
  if (!posAnim) {
48210
- posAnim = findGsapPositionAnimation(workingAnimations, selector);
48560
+ posAnim = findGsapPositionAnimation(workingAnimations, selector, time);
48211
48561
  if (!posAnim && fetchFallbackAnimations) {
48212
48562
  const fresh = await fetchFallbackAnimations();
48213
48563
  resolvedAnimations = fresh;
48214
- posAnim = findGsapPositionAnimation(fresh, selector);
48564
+ posAnim = findGsapPositionAnimation(fresh, selector, time);
48215
48565
  }
48216
48566
  }
48217
48567
  const gsapPos = readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 };
@@ -48233,13 +48583,14 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
48233
48583
  const freshMatch = fresh.find((a) => a.id === posAnim.id) ?? pickClosestToPlayhead(
48234
48584
  fresh.filter(
48235
48585
  (a) => a.targetSelector === posAnim.targetSelector && a.propertyGroup === posAnim.propertyGroup && isXYPositionWrite(a) === isXYPositionWrite(posAnim)
48236
- )
48586
+ ),
48587
+ time
48237
48588
  );
48238
48589
  if (freshMatch && freshMatch.id !== posAnim.id) {
48239
48590
  posAnim = freshMatch;
48240
48591
  }
48241
48592
  }
48242
- const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations, stamp: options?.stamp };
48593
+ const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations, stamp };
48243
48594
  if (route === "whole-path") {
48244
48595
  await commitWholePathOffset(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
48245
48596
  } else {
@@ -48270,7 +48621,8 @@ function preflightGsapRotationIntercept(selection, animations, iframe, fetchedAn
48270
48621
  }
48271
48622
  return { status: "persisted" };
48272
48623
  }
48273
- async function tryGsapRotationIntercept(selection, angle, animations, iframe, commitMutation, fetchFallbackAnimations) {
48624
+ async function tryGsapRotationIntercept(selection, angle, animations, iframe, commitMutation, fetchFallbackAnimations, stamp) {
48625
+ const time = editMoment(stamp).time;
48274
48626
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
48275
48627
  const outcome = preflightGsapRotationIntercept(selection, animations, iframe, fetchedAnimations);
48276
48628
  if (outcome.status === "blocked") return outcome;
@@ -48282,13 +48634,15 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
48282
48634
  workingAnimations,
48283
48635
  selection,
48284
48636
  commitMutation,
48285
- postSplitFetch
48637
+ postSplitFetch,
48638
+ time
48286
48639
  );
48287
48640
  const resolvedAnimations = resolved2?.animations ?? workingAnimations;
48288
48641
  let anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, ROTATION_CHANNEL_SET) ? resolved2.anim : null;
48289
48642
  if (!anim) {
48290
48643
  anim = pickClosestToPlayhead(
48291
- workingAnimations.filter((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET))
48644
+ workingAnimations.filter((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET)),
48645
+ time
48292
48646
  );
48293
48647
  }
48294
48648
  const newRotation = roundTo3(angle);
@@ -48300,7 +48654,7 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
48300
48654
  });
48301
48655
  return { status: "persisted" };
48302
48656
  }
48303
- const pct = computeCurrentPercentage(selection, anim);
48657
+ const pct = computeCurrentPercentage(selection, anim, time);
48304
48658
  if (!usePlayerStore.getState().autoKeyframeEnabled) {
48305
48659
  await commitWholePropertyOffset(
48306
48660
  selection,
@@ -48318,7 +48672,7 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
48318
48672
  anim,
48319
48673
  { rotation: newRotation },
48320
48674
  iframe,
48321
- { commitMutation, fetchAnimations: fetchFallbackAnimations },
48675
+ { commitMutation, fetchAnimations: fetchFallbackAnimations, stamp },
48322
48676
  { label: "Rotate", backfill: { rotation: newRotation }, holdFromStart: true }
48323
48677
  );
48324
48678
  }
@@ -75224,10 +75578,7 @@ function usePreviewPersistence({
75224
75578
  reloadPreview,
75225
75579
  nestedFiles
75226
75580
  );
75227
- if (strategy === "full") {
75228
- usePlayerStore.getState().setSelectedElementId(null);
75229
- return;
75230
- }
75581
+ if (strategy === "full") return;
75231
75582
  syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
75232
75583
  },
75233
75584
  [previewIframeRef, activeCompPathRef, reloadPreview, heldPreviewDoc]
@@ -76663,14 +77014,11 @@ function useTimelineDeleteOps({
76663
77014
  }
76664
77015
  const removeData = await removeResponse.json();
76665
77016
  const removedContent = typeof removeData.content === "string" ? removeData.content : originalContent;
76666
- const deleteContentEnd = furthestClipEndFromSource(removedContent);
76667
- const patchedContent = unlinkInSource(
76668
- setCompositionDurationToContent(removedContent, deleteContentEnd),
76669
- alsoUnlink
76670
- );
77017
+ const synced = syncCompositionDurationWithEnd(removedContent);
77018
+ const patchedContent = unlinkInSource(synced.source, alsoUnlink);
76671
77019
  rollbackDuration = captureDurationRollback(previewIframeRef.current);
76672
- if (deleteContentEnd > 0 && targetPath === (activeCompPath || "index.html")) {
76673
- usePlayerStore.getState().setDuration(deleteContentEnd);
77020
+ if (synced.contentEnd > 0 && targetPath === (activeCompPath || "index.html")) {
77021
+ usePlayerStore.getState().setDuration(synced.contentEnd);
76674
77022
  }
76675
77023
  return { disk: removedContent, after: patchedContent };
76676
77024
  }
@@ -82430,7 +82778,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
82430
82778
  nestedFiles
82431
82779
  });
82432
82780
  }, [projectIdRef, previewIframeRef, reloadPreview, onCacheInvalidate, onFileContentChanged, forceReloadSdkSession, recordMutationEdit]);
82433
- const runCommit = useCallback140(async (pid, compositionPath, targetPath, selection, mutation, options) => {
82781
+ const runCommit = useCallback140((pid, compositionPath, targetPath, selection, mutation, options) => whileScriptWrites(async () => {
82434
82782
  const result = await runMutationRequest(
82435
82783
  [mutation],
82436
82784
  options,
@@ -82441,8 +82789,8 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
82441
82789
  trackKeyframeCommit([mutation], result, options);
82442
82790
  options.onResult?.(result);
82443
82791
  await finalizeSuccessfulMutation(pid, compositionPath, selection, mutation, targetPath, result, options);
82444
- }, [showToast, finalizeSuccessfulMutation]);
82445
- const runBatchCommit = useCallback140(async (pid, compositionPath, targetPath, calls, options) => {
82792
+ }), [showToast, finalizeSuccessfulMutation]);
82793
+ const runBatchCommit = useCallback140((pid, compositionPath, targetPath, calls, options) => whileScriptWrites(async () => {
82446
82794
  const first = calls[0];
82447
82795
  const last = calls.at(-1);
82448
82796
  if (!first || !last) return;
@@ -82462,7 +82810,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
82462
82810
  const { instantPatch: _instantPatch, ...batchOptions } = options;
82463
82811
  const allPatched = instantPatches.length === calls.length;
82464
82812
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, allPatched ? { ...batchOptions, instantPatches } : batchOptions);
82465
- }, [showToast, finalizeSuccessfulMutation]);
82813
+ }), [showToast, finalizeSuccessfulMutation]);
82466
82814
  const commitMutation = useMemo73(() => {
82467
82815
  const serializeFile = (file, task) => {
82468
82816
  if (writeProjectFile) {
@@ -83202,119 +83550,128 @@ function useGsapAwareGroupMove({
83202
83550
  const writes = observeGsapGesture(gsapCommitMutation);
83203
83551
  const writer = writes.commit;
83204
83552
  if (!writer) return { ok: true, changed: false };
83205
- const toastRefusal = options.refusalToast !== false;
83206
- const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
83207
- let renderOnCommit = false;
83208
- const previewFallbackLatch = { pending: false };
83209
- const withGroupOptions = (options2) => ({
83210
- ...options2,
83211
- coalesceKey,
83212
- coalesceMs: Number.POSITIVE_INFINITY,
83213
- deferPreviewSync: !renderOnCommit,
83214
- previewFallbackLatch
83215
- });
83216
- const queued = [];
83217
- const flushQueued = async () => {
83218
- if (queued.length === 0) return;
83219
- const calls = queued.splice(0, queued.length);
83220
- if (!writer.batch) {
83221
- for (const call of calls) {
83222
- await writer(call.selection, call.mutation, call.options);
83223
- }
83224
- return;
83225
- }
83226
- await writer.batch(calls, {
83227
- ...calls.at(-1)?.options ?? { label: "Move animated layer (group)" },
83228
- label: "Move animated layer (group)"
83553
+ const scripted = updates.some(
83554
+ ({ selection, plainTranslate }) => !(plainTranslate ?? editsPlainCss(selection.element, "move"))
83555
+ );
83556
+ const commitGroup = async () => {
83557
+ const stamps = new Map(
83558
+ updates.map((u) => [u.selection, freezeDragStamp(u.selection.element)])
83559
+ );
83560
+ const toastRefusal = options.refusalToast !== false;
83561
+ const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
83562
+ let renderOnCommit = false;
83563
+ const previewFallbackLatch = { pending: false };
83564
+ const withGroupOptions = (options2) => ({
83565
+ ...options2,
83566
+ coalesceKey,
83567
+ coalesceMs: Number.POSITIVE_INFINITY,
83568
+ deferPreviewSync: !renderOnCommit,
83569
+ previewFallbackLatch
83229
83570
  });
83230
- };
83231
- const coalescedCommit = (selection, mutation, options2) => {
83232
- queued.push({ selection, mutation, options: withGroupOptions(options2) });
83233
- return Promise.resolve();
83234
- };
83235
- const preflightAnimations = /* @__PURE__ */ new Map();
83236
- const offsetMembers = /* @__PURE__ */ new Map();
83237
- const preflightResults = await Promise.allSettled(
83238
- updates.map(async ({ selection, next, plainTranslate }) => {
83239
- if (plainTranslate ?? editsPlainCss(selection.element, "move")) {
83240
- refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {
83241
- });
83242
- return void offsetMembers.set(selection, true);
83571
+ const queued = [];
83572
+ const flushQueued = async () => {
83573
+ if (queued.length === 0) return;
83574
+ const calls = queued.splice(0, queued.length);
83575
+ if (!writer.batch) {
83576
+ for (const call of calls) {
83577
+ await writer(call.selection, call.mutation, call.options);
83578
+ }
83579
+ return;
83243
83580
  }
83244
- const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
83245
- preflightAnimations.set(selection, animations);
83246
- const outcome = await tryGsapDragIntercept(
83247
- selection,
83248
- next,
83249
- animations,
83250
- previewIframeRef.current,
83251
- coalescedCommit,
83252
- void 0,
83253
- { preflightOnly: true, group: true }
83254
- );
83255
- if (outcome.status === "element-offset") offsetMembers.set(selection, false);
83256
- assertGsapEditPersisted(outcome);
83257
- })
83258
- );
83259
- const preflightFailure = firstPreflightFailure(
83260
- preflightResults,
83261
- updates,
83262
- offsetMembers,
83263
- activeCompPath
83264
- );
83265
- if (preflightFailure) {
83266
- trackGsapInteractionFailure(
83267
- preflightFailure.error,
83268
- preflightFailure.selection,
83269
- "drag",
83270
- "Move animated layer (group)",
83271
- toastRefusal
83581
+ await writer.batch(calls, {
83582
+ ...calls.at(-1)?.options ?? { label: "Move animated layer (group)" },
83583
+ label: "Move animated layer (group)"
83584
+ });
83585
+ };
83586
+ const coalescedCommit = (selection, mutation, options2) => {
83587
+ queued.push({ selection, mutation, options: withGroupOptions(options2) });
83588
+ return Promise.resolve();
83589
+ };
83590
+ const preflightAnimations = /* @__PURE__ */ new Map();
83591
+ const offsetMembers = /* @__PURE__ */ new Map();
83592
+ const preflightResults = await Promise.allSettled(
83593
+ updates.map(async ({ selection, next, plainTranslate }) => {
83594
+ if (plainTranslate ?? editsPlainCss(selection.element, "move")) {
83595
+ refuseGsapTakeover(selection.element, toastRefusal ? showToast : () => {
83596
+ });
83597
+ return void offsetMembers.set(selection, true);
83598
+ }
83599
+ const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
83600
+ preflightAnimations.set(selection, animations);
83601
+ const outcome = await tryGsapDragIntercept(
83602
+ selection,
83603
+ next,
83604
+ animations,
83605
+ previewIframeRef.current,
83606
+ coalescedCommit,
83607
+ void 0,
83608
+ { preflightOnly: true, group: true }
83609
+ );
83610
+ if (outcome.status === "element-offset") offsetMembers.set(selection, false);
83611
+ assertGsapEditPersisted(outcome);
83612
+ })
83272
83613
  );
83273
- throw preflightFailure.error;
83274
- }
83275
- const lastScriptWrite = updates.findLastIndex(
83276
- ({ selection }) => !offsetMembers.has(selection)
83277
- );
83278
- for (const [index, { selection, next }] of updates.entries()) {
83279
- renderOnCommit = index === lastScriptWrite;
83280
- const plain = offsetMembers.get(selection);
83281
- if (plain !== void 0) {
83282
- const staged = writes.drawKeepingUndone(
83283
- () => stageElementPositionOffset(selection, next, plain, coalesceKey)
83614
+ const preflightFailure = firstPreflightFailure(
83615
+ preflightResults,
83616
+ updates,
83617
+ offsetMembers,
83618
+ activeCompPath
83619
+ );
83620
+ if (preflightFailure) {
83621
+ trackGsapInteractionFailure(
83622
+ preflightFailure.error,
83623
+ preflightFailure.selection,
83624
+ "drag",
83625
+ "Move animated layer (group)",
83626
+ toastRefusal
83284
83627
  );
83285
- const result = await staged.save();
83286
- writes.recordDomResult(result);
83287
- continue;
83628
+ throw preflightFailure.error;
83629
+ }
83630
+ const lastScriptWrite = updates.findLastIndex(
83631
+ ({ selection }) => !offsetMembers.has(selection)
83632
+ );
83633
+ for (const [index, { selection, next }] of updates.entries()) {
83634
+ renderOnCommit = index === lastScriptWrite;
83635
+ const plain = offsetMembers.get(selection);
83636
+ if (plain !== void 0) {
83637
+ const staged = writes.drawKeepingUndone(
83638
+ () => stageElementPositionOffset(selection, next, plain, coalesceKey)
83639
+ );
83640
+ const result = await staged.save();
83641
+ writes.recordDomResult(result);
83642
+ continue;
83643
+ }
83644
+ try {
83645
+ const outcome = await tryGsapDragIntercept(
83646
+ selection,
83647
+ next,
83648
+ preflightAnimations.get(selection) ?? [],
83649
+ previewIframeRef.current,
83650
+ coalescedCommit,
83651
+ // The intercept re-reads the file to resolve a stale or shared tween.
83652
+ // Anything already queued has to be on disk before that read, or it
83653
+ // resolves against a file missing writes it is about to build on.
83654
+ async () => {
83655
+ await flushQueued();
83656
+ return makeFetchFallback(selection, { fresh: true })();
83657
+ },
83658
+ { preflightPassed: true, stamp: stamps.get(selection) }
83659
+ );
83660
+ assertGsapEditPersisted(outcome);
83661
+ } catch (error) {
83662
+ trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
83663
+ throw error;
83664
+ }
83288
83665
  }
83289
83666
  try {
83290
- const outcome = await tryGsapDragIntercept(
83291
- selection,
83292
- next,
83293
- preflightAnimations.get(selection) ?? [],
83294
- previewIframeRef.current,
83295
- coalescedCommit,
83296
- // The intercept re-reads the file to resolve a stale or shared tween.
83297
- // Anything already queued has to be on disk before that read, or it
83298
- // resolves against a file missing writes it is about to build on.
83299
- async () => {
83300
- await flushQueued();
83301
- return makeFetchFallback(selection, { fresh: true })();
83302
- },
83303
- { preflightPassed: true }
83304
- );
83305
- assertGsapEditPersisted(outcome);
83667
+ await flushQueued();
83668
+ return writes.finish();
83306
83669
  } catch (error) {
83307
- trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
83670
+ trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
83308
83671
  throw error;
83309
83672
  }
83310
- }
83311
- try {
83312
- await flushQueued();
83313
- return writes.finish();
83314
- } catch (error) {
83315
- trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
83316
- throw error;
83317
- }
83673
+ };
83674
+ return scripted ? whileScriptWrites(commitGroup) : commitGroup();
83318
83675
  },
83319
83676
  [
83320
83677
  gsapCommitMutation,
@@ -83743,8 +84100,8 @@ function handOverDraftSize(selection, written, size, draw) {
83743
84100
  });
83744
84101
  return written;
83745
84102
  }
83746
- function firstSizeKey(selection, size, resizeGroup, animations) {
83747
- const { autoKeyframeEnabled, currentTime } = usePlayerStore.getState();
84103
+ function firstSizeKey(selection, size, resizeGroup, animations, currentTime) {
84104
+ const { autoKeyframeEnabled } = usePlayerStore.getState();
83748
84105
  const keyframed = animations.some(
83749
84106
  (a) => a.keyframes && animationWritesAnyProperty(a, MOVED_OR_SIZED)
83750
84107
  );
@@ -83759,6 +84116,7 @@ function tweenUsesScaleLonghands(anim) {
83759
84116
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
83760
84117
  }
83761
84118
  async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run(), stamp) {
84119
+ const moment = editMoment(stamp);
83762
84120
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
83763
84121
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
83764
84122
  if (outcome.status === "blocked") return outcome;
@@ -83772,7 +84130,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83772
84130
  workingAnimations,
83773
84131
  selection,
83774
84132
  commitMutation,
83775
- postSplitFetch
84133
+ postSplitFetch,
84134
+ moment.time
83776
84135
  );
83777
84136
  const anim = resolved2?.anim && animationWritesAnyProperty(resolved2.anim, resizeProperties) ? resolved2.anim : null;
83778
84137
  logResize("intercept-enter", {
@@ -83788,7 +84147,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83788
84147
  (a) => animationWritesAnyProperty(a, SIZE_PROPS)
83789
84148
  );
83790
84149
  if (!scriptWritesSize) {
83791
- const firstKey = firstSizeKey(selection, sized, resizeGroup, workingAnimations);
84150
+ const firstKey = firstSizeKey(selection, sized, resizeGroup, workingAnimations, moment.time);
83792
84151
  if (!firstKey) return { status: "element-size" };
83793
84152
  await commitMutation(selection, firstKey, { label: "Resize", softReload: true });
83794
84153
  return handOverDraftSize(selection, { status: "persisted" }, sized, draw);
@@ -83800,7 +84159,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83800
84159
  const animatedTween = pickClosestToPlayhead(
83801
84160
  workingAnimations.filter(
83802
84161
  (a) => !isInstantHold(a) && resolveTweenDuration(a) > 0 && animationWritesAnyProperty(a, resizeProperties)
83803
- )
84162
+ ),
84163
+ moment.time
83804
84164
  );
83805
84165
  if (animatedTween) {
83806
84166
  logResize("intercept-route", { route: "keyframed-size", tweenId: animatedTween.id });
@@ -83830,8 +84190,9 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83830
84190
  if (tweenDuration <= 0) {
83831
84191
  return { status: "blocked", reason: "source-uneditable", detail: "zero-duration-tween" };
83832
84192
  }
83833
- const { activeKeyframePct, setActiveKeyframePct } = usePlayerStore.getState();
83834
- const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim);
84193
+ const { setActiveKeyframePct } = usePlayerStore.getState();
84194
+ const activeKeyframePct = moment.keyframePct;
84195
+ const pct = activeKeyframePct ?? computeCurrentPercentage(selection, anim, moment.time);
83835
84196
  const selector = selectorFromSelection(selection);
83836
84197
  let resizeProps;
83837
84198
  let resizeBackfill;
@@ -83930,7 +84291,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83930
84291
  const positionTween = pickClosestToPlayhead(
83931
84292
  currentAnimations.filter(
83932
84293
  (a) => a.propertyGroup === "position" && !isInstantHold(a) && resolveTweenDuration(a) > 0
83933
- )
84294
+ ),
84295
+ moment.time
83934
84296
  );
83935
84297
  if (positionTween) {
83936
84298
  logResize("scale-finalize", { route: "position-keyframe", tweenId: positionTween.id });
@@ -84371,7 +84733,10 @@ function useGsapAwareEditing({
84371
84733
  const result = await stageElementPositionOffset(selection, next, true).save();
84372
84734
  return writes.finish(result?.changed === true);
84373
84735
  }
84374
- if (writes.commit) {
84736
+ const writer = writes.commit;
84737
+ if (!writer) return;
84738
+ return whileScriptWrites(async () => {
84739
+ const stamp = freezeDragStamp(selection.element);
84375
84740
  try {
84376
84741
  const ownedAnimations = getGsapAnimationsForSelection(selection);
84377
84742
  const targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
@@ -84380,9 +84745,9 @@ function useGsapAwareEditing({
84380
84745
  next,
84381
84746
  targetAnimations,
84382
84747
  previewIframeRef.current,
84383
- writes.commit,
84748
+ writer,
84384
84749
  makeFetchFallback(selection),
84385
- modifiers
84750
+ { ...modifiers, stamp }
84386
84751
  );
84387
84752
  await saveMove(outcome, async () => {
84388
84753
  const staged = writes.drawKeepingUndone(
@@ -84395,7 +84760,7 @@ function useGsapAwareEditing({
84395
84760
  trackGsapInteractionFailure(error, selection, "drag", "Move animated layer");
84396
84761
  throw error;
84397
84762
  }
84398
- }
84763
+ });
84399
84764
  },
84400
84765
  [
84401
84766
  gsapCommitMutation,
@@ -84417,134 +84782,136 @@ function useGsapAwareEditing({
84417
84782
  });
84418
84783
  const handleGsapAwareBoxSizeCommit = useCallback147(
84419
84784
  async (selection, next, offset, restore2 = () => void 0, route) => {
84420
- const stamp = freezeDragStamp(selection.element);
84421
84785
  const writes = observeGsapGesture(gsapCommitMutation);
84422
84786
  if (route?.plainTranslate ?? editsPlainCss(selection.element, "resize")) {
84423
84787
  const result = await handleDomBoxSizeCommit(selection, next, offset, restore2);
84424
84788
  return writes.finish(result?.changed === true);
84425
84789
  }
84426
- let targetAnimations;
84427
- try {
84428
- const ownedAnimations = getGsapAnimationsForSelection(selection);
84429
- targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
84430
- } catch (error) {
84431
- restore2();
84432
- trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
84433
- throw error;
84434
- }
84435
- const scaleRoute = targetAnimations.some((anim) => anim.propertyGroup === "scale");
84436
- const selector = selectorFromSelection(selection);
84437
- const hasLivePositionTween = selector ? hasNonHoldTweenForElement(
84438
- previewIframeRef.current,
84439
- selector,
84440
- void 0,
84441
- POSITION_CHANNELS
84442
- ) : false;
84443
- logResize("commit-route", {
84444
- next,
84445
- offset: offset ?? null,
84446
- scaleRoute,
84447
- animCount: targetAnimations.length,
84448
- animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`)
84449
- });
84450
- let anchorMove = null;
84451
- const stageCrop = prepareCropResize(selection.element);
84452
- let cropUndoKey = null;
84453
- const saveResizeAnchor = async (ownsDragOffset, commitMutation2, coalesceKey) => {
84454
- if (offset && !ownsDragOffset) {
84455
- const dragOutcome = await tryGsapDragIntercept(
84456
- selection,
84457
- offset,
84458
- targetAnimations,
84459
- previewIframeRef.current,
84460
- commitMutation2,
84461
- makeFetchFallback(selection),
84462
- { stamp }
84463
- );
84464
- await saveMove(dragOutcome, async () => {
84465
- const plainAfterSettle = editsPlainCss(selection.element, "move");
84466
- anchorMove = writes.drawKeepingUndone(
84467
- () => stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey)
84468
- );
84469
- });
84790
+ return whileScriptWrites(async () => {
84791
+ const stamp = freezeDragStamp(selection.element);
84792
+ let targetAnimations;
84793
+ try {
84794
+ const ownedAnimations = getGsapAnimationsForSelection(selection);
84795
+ targetAnimations = Array.isArray(ownedAnimations) ? ownedAnimations : await ownedAnimations;
84796
+ } catch (error) {
84797
+ restore2();
84798
+ trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
84799
+ throw error;
84470
84800
  }
84471
- };
84472
- await runGestureTransaction({
84473
- element: selection.element,
84474
- label: "Resize layer",
84475
- draw: writes.drawKeepingUndone,
84476
- settle: () => {
84477
- if (!offset || scaleRoute || !selector) return;
84478
- writes.drawKeepingUndone(() => {
84479
- const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
84480
- x: 0,
84481
- y: 0
84482
- };
84483
- const { newX, newY } = computeDraggedGsapPosition(
84484
- selection.element,
84801
+ const scaleRoute = targetAnimations.some((anim) => anim.propertyGroup === "scale");
84802
+ const selector = selectorFromSelection(selection);
84803
+ const hasLivePositionTween = selector ? hasNonHoldTweenForElement(
84804
+ previewIframeRef.current,
84805
+ selector,
84806
+ void 0,
84807
+ POSITION_CHANNELS
84808
+ ) : false;
84809
+ logResize("commit-route", {
84810
+ next,
84811
+ offset: offset ?? null,
84812
+ scaleRoute,
84813
+ animCount: targetAnimations.length,
84814
+ animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`)
84815
+ });
84816
+ let anchorMove = null;
84817
+ const stageCrop = prepareCropResize(selection.element);
84818
+ let cropUndoKey = null;
84819
+ const saveResizeAnchor = async (ownsDragOffset, commitMutation2, coalesceKey) => {
84820
+ if (offset && !ownsDragOffset) {
84821
+ const dragOutcome = await tryGsapDragIntercept(
84822
+ selection,
84485
84823
  offset,
84486
- gsapPos,
84487
- stamp
84824
+ targetAnimations,
84825
+ previewIframeRef.current,
84826
+ commitMutation2,
84827
+ makeFetchFallback(selection),
84828
+ { stamp }
84488
84829
  );
84489
- logResize("sync-settle", { gsapPos, offset, newX, newY });
84490
- setElementGsapPosition(selection.element, newX, newY);
84491
- });
84492
- },
84493
- persist: async (commit2, coalesceKey) => {
84494
- if (writes.commit) {
84495
- const commitMutation2 = commit2(writes.commit);
84496
- try {
84497
- const outcome = await tryGsapResizeIntercept(
84498
- selection,
84499
- next,
84500
- targetAnimations,
84501
- previewIframeRef.current,
84502
- commitMutation2,
84503
- makeFetchFallback(selection),
84830
+ await saveMove(dragOutcome, async () => {
84831
+ const plainAfterSettle = editsPlainCss(selection.element, "move");
84832
+ anchorMove = writes.drawKeepingUndone(
84833
+ () => stageElementPositionOffset(selection, offset, plainAfterSettle, coalesceKey)
84834
+ );
84835
+ });
84836
+ }
84837
+ };
84838
+ await runGestureTransaction({
84839
+ element: selection.element,
84840
+ label: "Resize layer",
84841
+ draw: writes.drawKeepingUndone,
84842
+ settle: () => {
84843
+ if (!offset || scaleRoute || !selector) return;
84844
+ writes.drawKeepingUndone(() => {
84845
+ const gsapPos = readGsapPositionFromIframe(previewIframeRef.current, selector) ?? {
84846
+ x: 0,
84847
+ y: 0
84848
+ };
84849
+ const { newX, newY } = computeDraggedGsapPosition(
84850
+ selection.element,
84504
84851
  offset,
84505
- writes.drawKeepingUndone,
84852
+ gsapPos,
84506
84853
  stamp
84507
84854
  );
84508
- assertGsapEditPersisted(outcome);
84509
- if (outcome.status === "element-size") {
84510
- const result = await writes.drawKeepingUndone(
84511
- () => handleDomBoxSizeCommit(selection, next, void 0, void 0, coalesceKey)
84855
+ logResize("sync-settle", { gsapPos, offset, newX, newY });
84856
+ setElementGsapPosition(selection.element, newX, newY);
84857
+ });
84858
+ },
84859
+ persist: async (commit2, coalesceKey) => {
84860
+ if (writes.commit) {
84861
+ const commitMutation2 = commit2(writes.commit);
84862
+ try {
84863
+ const outcome = await tryGsapResizeIntercept(
84864
+ selection,
84865
+ next,
84866
+ targetAnimations,
84867
+ previewIframeRef.current,
84868
+ commitMutation2,
84869
+ makeFetchFallback(selection),
84870
+ offset,
84871
+ writes.drawKeepingUndone,
84872
+ stamp
84512
84873
  );
84513
- writes.recordDomResult(result);
84514
- } else cropUndoKey = coalesceKey;
84515
- const ownsDragOffset = outcome.status === "persisted" && outcome.ownsDragOffset === true;
84516
- logResize("intercept-handled", {
84517
- scaleRoute,
84518
- ownsDragOffset,
84519
- willForwardOffset: !!(offset && !ownsDragOffset)
84520
- });
84521
- await saveResizeAnchor(ownsDragOffset, commitMutation2, coalesceKey);
84522
- logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
84523
- return;
84524
- } catch (error) {
84525
- trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
84526
- throw error;
84874
+ assertGsapEditPersisted(outcome);
84875
+ if (outcome.status === "element-size") {
84876
+ const result = await writes.drawKeepingUndone(
84877
+ () => handleDomBoxSizeCommit(selection, next, void 0, void 0, coalesceKey)
84878
+ );
84879
+ writes.recordDomResult(result);
84880
+ } else cropUndoKey = coalesceKey;
84881
+ const ownsDragOffset = outcome.status === "persisted" && outcome.ownsDragOffset === true;
84882
+ logResize("intercept-handled", {
84883
+ scaleRoute,
84884
+ ownsDragOffset,
84885
+ willForwardOffset: !!(offset && !ownsDragOffset)
84886
+ });
84887
+ await saveResizeAnchor(ownsDragOffset, commitMutation2, coalesceKey);
84888
+ logResizeSettle(selection.element, ownsDragOffset ? "gsap-scale" : "gsap-size");
84889
+ return;
84890
+ } catch (error) {
84891
+ trackGsapInteractionFailure(error, selection, "resize", "Resize animated layer");
84892
+ throw error;
84893
+ }
84527
84894
  }
84528
- }
84529
- throw new Error("Resize of a GSAP-owned box has no GSAP writer");
84530
- },
84531
- afterBufferedCommitsSaved: async () => {
84532
- const anchorResult = await anchorMove?.save();
84533
- writes.recordDomResult(anchorResult);
84534
- if (cropUndoKey) {
84535
- const cropResult = await writes.drawKeepingUndone(
84536
- () => saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey)
84537
- );
84538
- writes.recordDomResult(cropResult);
84539
- }
84540
- },
84541
- restore: () => {
84542
- anchorMove?.rollback();
84543
- restore2();
84544
- },
84545
- skipPixelAssert: hasLivePositionTween
84895
+ throw new Error("Resize of a GSAP-owned box has no GSAP writer");
84896
+ },
84897
+ afterBufferedCommitsSaved: async () => {
84898
+ const anchorResult = await anchorMove?.save();
84899
+ writes.recordDomResult(anchorResult);
84900
+ if (cropUndoKey) {
84901
+ const cropResult = await writes.drawKeepingUndone(
84902
+ () => saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey)
84903
+ );
84904
+ writes.recordDomResult(cropResult);
84905
+ }
84906
+ },
84907
+ restore: () => {
84908
+ anchorMove?.rollback();
84909
+ restore2();
84910
+ },
84911
+ skipPixelAssert: hasLivePositionTween
84912
+ });
84913
+ return writes.finish();
84546
84914
  });
84547
- return writes.finish();
84548
84915
  },
84549
84916
  [
84550
84917
  handleDomBoxSizeCommit,
@@ -84564,7 +84931,10 @@ function useGsapAwareEditing({
84564
84931
  const result = await handleDomRotationCommit(selection, next);
84565
84932
  return writes.finish(result?.changed === true);
84566
84933
  }
84567
- if (writes.commit) {
84934
+ const writer = writes.commit;
84935
+ if (!writer) return;
84936
+ return whileScriptWrites(async () => {
84937
+ const stamp = freezeDragStamp(selection.element);
84568
84938
  try {
84569
84939
  const targetAnimations = await getGsapAnimationsForSelection(selection);
84570
84940
  const outcome = await tryGsapRotationIntercept(
@@ -84572,8 +84942,9 @@ function useGsapAwareEditing({
84572
84942
  next.angle,
84573
84943
  targetAnimations,
84574
84944
  previewIframeRef.current,
84575
- writes.commit,
84576
- makeFetchFallback(selection)
84945
+ writer,
84946
+ makeFetchFallback(selection),
84947
+ stamp
84577
84948
  );
84578
84949
  assertGsapEditPersisted(outcome);
84579
84950
  return writes.finish();
@@ -84581,7 +84952,7 @@ function useGsapAwareEditing({
84581
84952
  trackGsapInteractionFailure(error, selection, "rotation", "Rotate animated layer");
84582
84953
  throw error;
84583
84954
  }
84584
- }
84955
+ });
84585
84956
  },
84586
84957
  [
84587
84958
  gsapCommitMutation,
@@ -86929,6 +87300,7 @@ function useEditHistoryActions({
86929
87300
  const apply = useCallback154(
86930
87301
  async (direction) => {
86931
87302
  const noun = direction === "undo" ? "Undo" : "Redo";
87303
+ if (direction === "undo" && cancelNewestStudioWaitingPress()) return;
86932
87304
  const pendingEditShown = direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
86933
87305
  const predicted = pendingEditShown ? null : editHistory.predict?.(direction) ?? null;
86934
87306
  const predictedShown = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
@@ -98661,7 +99033,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
98661
99033
  const [compIdToSrc, setCompIdToSrc] = useState160(/* @__PURE__ */ new Map());
98662
99034
  const previewIframe = useOwnPreviewIframe();
98663
99035
  const [compositionLoading, setCompositionLoading] = useState160(true);
98664
- const [refreshKey, setRefreshKey] = useState160(0);
99036
+ const [refreshKey, setRefreshKeyState] = useState160(0);
98665
99037
  const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
98666
99038
  const [blockPreview, setBlockPreview] = useState160(null);
98667
99039
  const previewIframeRef = useRef187(null);
@@ -98686,7 +99058,11 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
98686
99058
  const handleDomZIndexReorderCommitRef = useRef187(null);
98687
99059
  const pendingTimelineEditPathRef = useRef187(/* @__PURE__ */ new Set());
98688
99060
  const isGestureRecordingRef = useRef187(false);
98689
- const reloadPreview = useCallback194(() => setRefreshKey((k) => k + 1), []);
99061
+ const setRefreshKey = useCallback194((next) => {
99062
+ requestPreviewReload();
99063
+ setRefreshKeyState(next);
99064
+ }, []);
99065
+ const reloadPreview = useCallback194(() => setRefreshKey((k) => k + 1), [setRefreshKey]);
98690
99066
  const fileManagerResult = useFileManager({
98691
99067
  projectId,
98692
99068
  showToast,
@@ -98725,7 +99101,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
98725
99101
  recordEdit: editHistory.recordEdit,
98726
99102
  previewIframeRef,
98727
99103
  activeCompPathRef,
98728
- reloadPreview: () => setRefreshKey((k) => k + 1)
99104
+ reloadPreview
98729
99105
  });
98730
99106
  const previewPersistence = useStableHandlers(previewPersistenceResult, projectId);
98731
99107
  const externalFileChanges = useStudioExternalFileChanges({
@@ -99786,6 +100162,7 @@ export {
99786
100162
  buttonBase,
99787
100163
  buttonSizes,
99788
100164
  buttonVariants,
100165
+ cancelNewestStudioWaitingPress,
99789
100166
  cn,
99790
100167
  collectSnapContext,
99791
100168
  collectStudioLookScene,