@hyperframes/studio 0.8.98 → 0.8.99

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 (51) hide show
  1. package/dist/assets/{hyperframes-player-C6muFtfk.js → hyperframes-player-Rml34FRW.js} +1 -1
  2. package/dist/assets/{index-itc1XziP.js → index-CVDArg3m.js} +178 -178
  3. package/dist/assets/{index-B6vU39NM.js → index-C_fi-jTe.js} +1 -1
  4. package/dist/assets/{index-BxnoLgxG.js → index-D9YzDuBx.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +542 -373
  7. package/dist/index.js.map +1 -1
  8. package/package.json +9 -9
  9. package/src/components/editor/DomEditCropHandles.test.tsx +34 -66
  10. package/src/components/editor/DomEditCropHandles.tsx +30 -62
  11. package/src/components/editor/DomEditOverlay.test.ts +2 -2
  12. package/src/components/editor/cropResize.test.ts +129 -0
  13. package/src/components/editor/cropResize.ts +143 -0
  14. package/src/components/editor/domEditOverlayCrop.ts +65 -15
  15. package/src/components/editor/domEditOverlayCropLift.test.ts +44 -0
  16. package/src/components/editor/domEditOverlayGestures.ts +2 -5
  17. package/src/components/editor/elementPositionOffset.ts +5 -4
  18. package/src/components/editor/manualEdits.test.ts +22 -8
  19. package/src/components/editor/manualEditsDom.ts +27 -11
  20. package/src/components/editor/manualEditsParsing.ts +0 -4
  21. package/src/components/editor/manualEditsSnapshot.ts +3 -0
  22. package/src/components/editor/manualOffsetDrag.test.ts +14 -0
  23. package/src/hooks/domSelectionTestHarness.ts +8 -0
  24. package/src/hooks/draggedGsapPosition.ts +4 -2
  25. package/src/hooks/gsapDragCommit.ts +6 -6
  26. package/src/hooks/gsapEditOutcome.ts +6 -0
  27. package/src/hooks/gsapResizeIntercept.ts +5 -5
  28. package/src/hooks/gsapRuntimeBridge.test.ts +11 -0
  29. package/src/hooks/gsapRuntimeBridge.ts +2 -1
  30. package/src/hooks/gsapRuntimeKeyframes.test.ts +7 -0
  31. package/src/hooks/gsapRuntimeKeyframes.ts +34 -21
  32. package/src/hooks/timelineTimingSync.ts +40 -4
  33. package/src/hooks/useAnimatedPropertyCommit.test.tsx +40 -1
  34. package/src/hooks/useAnimatedPropertyCommit.ts +11 -13
  35. package/src/hooks/useCropOverlay.ts +8 -2
  36. package/src/hooks/useDomEditCommits.test.tsx +6 -1
  37. package/src/hooks/useDomEditCommits.ts +1 -0
  38. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -2
  39. package/src/hooks/useDomEditSession.ts +2 -0
  40. package/src/hooks/useDomGeometryCommit.ts +1 -0
  41. package/src/hooks/useDomGeometryCommits.test.tsx +112 -0
  42. package/src/hooks/useDomGeometryCommits.ts +5 -0
  43. package/src/hooks/useElementLifecycleOps.multiDelete.test.tsx +3 -1
  44. package/src/hooks/useGsapAwareEditing.test.tsx +101 -5
  45. package/src/hooks/useGsapAwareEditing.ts +37 -23
  46. package/src/hooks/useTimelineEditing.test.tsx +190 -135
  47. package/src/hooks/useTimelineEditing.ts +13 -13
  48. package/src/hooks/useTimelineGroupEditing.ts +13 -9
  49. package/src/player/components/useTimelineVirtualRows.test.tsx +30 -1
  50. package/src/utils/rounding.ts +5 -0
  51. package/src/utils/sdkEditTransaction.ts +10 -3
package/dist/index.js CHANGED
@@ -2173,6 +2173,9 @@ function getTimelinePlayheadTransform(time, pixelsPerSecond, contentOrigin, atRe
2173
2173
  function roundTo3(val) {
2174
2174
  return Math.round(val * 1e3) / 1e3;
2175
2175
  }
2176
+ function roundToLayoutPx(val) {
2177
+ return Math.round(val);
2178
+ }
2176
2179
  function roundToCenti(val) {
2177
2180
  return Math.round(val * 100) / 100;
2178
2181
  }
@@ -19808,6 +19811,8 @@ async function postGsapMutation(projectId, filePath, mutation, fallback) {
19808
19811
  }
19809
19812
 
19810
19813
  // src/hooks/timelineTimingSync.ts
19814
+ import { findTimelineScript } from "@hyperframes/core/gsap-parser-acorn";
19815
+ import { walkCompositionDescendants } from "@hyperframes/parsers/hf-ids";
19811
19816
  async function readFileContent(projectId, targetPath) {
19812
19817
  if (targetPath.includes("\0") || targetPath.includes("..")) {
19813
19818
  throw new Error(`Unsafe path: ${targetPath}`);
@@ -20007,8 +20012,19 @@ async function scaleGsapPositions(projectId, filePath, elementId2, oldStart, old
20007
20012
  "scale-positions failed"
20008
20013
  );
20009
20014
  }
20015
+ function sdkTimingGsapSync(result) {
20016
+ const timelineText = (html2) => {
20017
+ const scripts = [];
20018
+ walkCompositionDescendants(new DOMParser().parseFromString(html2, "text/html"), (el) => {
20019
+ if (el.tagName.toLowerCase() === "script") scripts.push(el);
20020
+ });
20021
+ return findTimelineScript(scripts)?.textContent ?? null;
20022
+ };
20023
+ if (timelineText(result.before) === timelineText(result.after)) return null;
20024
+ return { mutated: true, scriptText: extractGsapScriptText(result.after) };
20025
+ }
20010
20026
  function finishClipTimingFallback(input) {
20011
- const { projectId, targetPath, domId, edit } = input;
20027
+ const { projectId, targetPath, domId, edit, sdkGsap } = input;
20012
20028
  const timingChanged = edit.kind === "shift" ? edit.delta !== 0 : edit.from.start !== edit.to.start || edit.from.duration !== edit.to.duration;
20013
20029
  const runMutation = (pid, id) => edit.kind === "shift" ? shiftGsapPositions(pid, targetPath, id, edit.delta) : scaleGsapPositions(
20014
20030
  pid,
@@ -20023,7 +20039,7 @@ function finishClipTimingFallback(input) {
20023
20039
  return finishTimelineTimingFallback({
20024
20040
  iframe: input.iframe,
20025
20041
  reloadPreview: input.reloadPreview,
20026
- gsapMutation: timingChanged && domId && projectId ? () => foldGsapMutationIntoHistory({
20042
+ gsapMutation: sdkGsap ? () => Promise.resolve(sdkGsap) : timingChanged && domId && projectId ? () => foldGsapMutationIntoHistory({
20027
20043
  writeFile: input.writeProjectFile,
20028
20044
  paths: [targetPath],
20029
20045
  projectId,
@@ -20043,6 +20059,17 @@ async function finishGroupTimingGsapFallback(input) {
20043
20059
  (change) => input.resolveChangePath(change.element) !== activePath
20044
20060
  );
20045
20061
  const onGsapError = (err) => console.error(`[Timeline] ${input.errorLabel}`, err);
20062
+ const { sdkGsap } = input;
20063
+ if (sdkGsap) {
20064
+ await finishTimelineTimingFallback({
20065
+ iframe: input.iframe,
20066
+ reloadPreview: input.reloadPreview,
20067
+ gsapMutation: () => Promise.resolve(sdkGsap),
20068
+ onGsapError,
20069
+ rebindWhenUnmutated: !otherFileChanged
20070
+ });
20071
+ return;
20072
+ }
20046
20073
  await finishTimelineTimingFallback({
20047
20074
  iframe: input.iframe,
20048
20075
  reloadPreview: input.reloadPreview,
@@ -30159,12 +30186,52 @@ function cropRectFromInsets(rect, insets, scaleX, scaleY) {
30159
30186
  height: Math.max(0, rect.height - (insets.top + insets.bottom) * sy)
30160
30187
  };
30161
30188
  }
30162
- function readElementCropInsets(element) {
30189
+ var cropLifts = /* @__PURE__ */ new WeakMap();
30190
+ function liftSelector(element) {
30191
+ const quote = (value) => `"${value.replace(/["\\]/g, "\\$&")}"`;
30192
+ const hfId = element.getAttribute("data-hf-id");
30193
+ if (hfId) return `[data-hf-id=${quote(hfId)}]`;
30194
+ return element.id ? `[id=${quote(element.id)}]` : null;
30195
+ }
30196
+ function liftElementCrop(element) {
30197
+ const selector = liftSelector(element);
30198
+ if (!selector || cropLifts.has(element)) return;
30199
+ const doc = element.ownerDocument;
30200
+ const rule = doc.createElement("style");
30201
+ rule.textContent = `${selector}{clip-path:none!important}`;
30202
+ (doc.head ?? doc.documentElement).append(rule);
30203
+ cropLifts.set(element, { rule });
30204
+ }
30205
+ function dropElementCropLift(element) {
30206
+ cropLifts.get(element)?.rule.remove();
30207
+ cropLifts.delete(element);
30208
+ }
30209
+ function isElementCropLifted(element) {
30210
+ return cropLifts.has(element);
30211
+ }
30212
+ var computedClipPath = (element) => element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
30213
+ function readElementClipPath(element) {
30163
30214
  const inline = element.style.getPropertyValue("clip-path").trim();
30164
- const value = inline || element.ownerDocument.defaultView?.getComputedStyle(element).clipPath.trim() || "";
30215
+ const lift = cropLifts.get(element);
30216
+ if (inline || !lift) return inline || computedClipPath(element);
30217
+ if (lift.sheetClip === void 0) {
30218
+ const { parentNode, nextSibling } = lift.rule;
30219
+ lift.rule.remove();
30220
+ lift.sheetClip = computedClipPath(element);
30221
+ parentNode?.insertBefore(lift.rule, nextSibling);
30222
+ }
30223
+ return lift.sheetClip;
30224
+ }
30225
+ function parseCropClipPath(value) {
30165
30226
  if (!value || value === "none") return { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
30166
30227
  return parseInsetClipPathSides(value);
30167
30228
  }
30229
+ function readElementCropInsets(element) {
30230
+ return parseCropClipPath(readElementClipPath(element));
30231
+ }
30232
+ function hasCropInsets(insets) {
30233
+ return insets.top > 0 || insets.right > 0 || insets.bottom > 0 || insets.left > 0;
30234
+ }
30168
30235
  function clampInset(value, max) {
30169
30236
  if (!Number.isFinite(value)) return 0;
30170
30237
  return Math.min(Math.max(0, value), Math.max(0, max));
@@ -30206,7 +30273,7 @@ function resolveCropInsetFromMoveDrag(input) {
30206
30273
  return { left, right: totalX - left, top, bottom: totalY - top };
30207
30274
  }
30208
30275
  function hugRectForElement(rect, element) {
30209
- const insets = readElementCropInsets(element);
30276
+ const insets = isElementCropLifted(element) ? null : readElementCropInsets(element);
30210
30277
  if (!insets || insets.top <= 0 && insets.right <= 0 && insets.bottom <= 0 && insets.left <= 0)
30211
30278
  return rect;
30212
30279
  return cropRectFromInsets(rect, insets, rect.editScaleX, rect.editScaleY);
@@ -30944,9 +31011,6 @@ function pointerAngleDegrees(centerX, centerY, x, y) {
30944
31011
  function normalizeAngleDelta(delta) {
30945
31012
  return ((delta + 180) % 360 + 360) % 360 - 180;
30946
31013
  }
30947
- function roundAngle(angle) {
30948
- return Math.round(angle * 10) / 10;
30949
- }
30950
31014
  function resolveDomEditRotationGesture(input) {
30951
31015
  const startAngle = pointerAngleDegrees(input.centerX, input.centerY, input.startX, input.startY);
30952
31016
  const currentAngle = pointerAngleDegrees(
@@ -30958,7 +31022,7 @@ function resolveDomEditRotationGesture(input) {
30958
31022
  const delta = normalizeAngleDelta(currentAngle - startAngle);
30959
31023
  const angle = input.actualAngle + delta;
30960
31024
  return {
30961
- angle: input.snap ? Math.round(angle / ROTATION_SNAP_DEGREES) * ROTATION_SNAP_DEGREES : roundAngle(angle)
31025
+ angle: input.snap ? Math.round(angle / ROTATION_SNAP_DEGREES) * ROTATION_SNAP_DEGREES : roundTo3(angle)
30962
31026
  };
30963
31027
  }
30964
31028
  function hasDomEditRotationChanged(initialAngle, nextAngle) {
@@ -31317,9 +31381,6 @@ var STUDIO_ROTATION_TRANSFORM_ORIGIN = "center center";
31317
31381
  function finiteNumber2(value) {
31318
31382
  return typeof value === "number" && Number.isFinite(value) ? value : null;
31319
31383
  }
31320
- function roundRotationAngle(angle) {
31321
- return Math.round(angle * 10) / 10;
31322
- }
31323
31384
  function normalizeStudioFileChangePath(path) {
31324
31385
  return path.trim().replace(/\\/g, "/").replace(/^\.?\//, "");
31325
31386
  }
@@ -31523,8 +31584,8 @@ function prepareStudioPathOffsetBase(element, updateBase) {
31523
31584
  function writeStudioPathOffsetVars(element, offset, options = {}) {
31524
31585
  prepareStudioPathOffsetBase(element, options.updateBase ?? true);
31525
31586
  element.setAttribute(STUDIO_PATH_OFFSET_ATTR, "true");
31526
- element.style.setProperty(STUDIO_OFFSET_X_PROP, `${Math.round(offset.x)}px`);
31527
- element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${Math.round(offset.y)}px`);
31587
+ element.style.setProperty(STUDIO_OFFSET_X_PROP, `${roundTo3(offset.x)}px`);
31588
+ element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${roundTo3(offset.y)}px`);
31528
31589
  }
31529
31590
  function isIdentityAfterTranslateStrip(m) {
31530
31591
  return m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1;
@@ -31594,7 +31655,7 @@ function applyStudioPathOffsetDraft(element, offset) {
31594
31655
  if (applyStudioPathOffsetViaGsap(element, offset)) return;
31595
31656
  element.style.setProperty(
31596
31657
  "translate",
31597
- composeTranslateValue(element, `${Math.round(offset.x)}px`, `${Math.round(offset.y)}px`)
31658
+ composeTranslateValue(element, `${roundTo3(offset.x)}px`, `${roundTo3(offset.y)}px`)
31598
31659
  );
31599
31660
  stripGsapTranslateFromTransform(element);
31600
31661
  }
@@ -31604,7 +31665,7 @@ function readParentFlexBasisPixels(element, size) {
31604
31665
  const display2 = readStyleOrComputed(parent, "display").trim();
31605
31666
  if (display2 !== "flex" && display2 !== "inline-flex") return null;
31606
31667
  const direction = readStyleOrComputed(parent, "flex-direction").trim();
31607
- return Math.round(Math.max(1, direction.startsWith("column") ? size.height : size.width));
31668
+ return roundToLayoutPx(Math.max(1, direction.startsWith("column") ? size.height : size.width));
31608
31669
  }
31609
31670
  function restoreStaleStudioScaleResize(element) {
31610
31671
  if (!element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) return;
@@ -31667,14 +31728,14 @@ function writeStudioBoxSizeVars(element, size) {
31667
31728
  element.setAttribute(STUDIO_ORIGINAL_DISPLAY_ATTR, element.style.getPropertyValue("display"));
31668
31729
  }
31669
31730
  element.setAttribute(STUDIO_BOX_SIZE_ATTR, "true");
31670
- element.style.setProperty(STUDIO_WIDTH_PROP, `${Math.round(Math.max(1, size.width))}px`);
31671
- element.style.setProperty(STUDIO_HEIGHT_PROP, `${Math.round(Math.max(1, size.height))}px`);
31731
+ element.style.setProperty(STUDIO_WIDTH_PROP, `${roundToLayoutPx(Math.max(1, size.width))}px`);
31732
+ element.style.setProperty(STUDIO_HEIGHT_PROP, `${roundToLayoutPx(Math.max(1, size.height))}px`);
31672
31733
  }
31673
31734
  function applyStudioBoxSizeDimensions(element, size) {
31674
31735
  writeStudioBoxSizeVars(element, size);
31675
31736
  restoreStaleStudioScaleResize(element);
31676
- const width = Math.round(Math.max(1, size.width));
31677
- const height = Math.round(Math.max(1, size.height));
31737
+ const width = roundToLayoutPx(Math.max(1, size.width));
31738
+ const height = roundToLayoutPx(Math.max(1, size.height));
31678
31739
  element.style.setProperty("box-sizing", "border-box");
31679
31740
  element.style.setProperty("width", `${width}px`);
31680
31741
  element.style.setProperty("height", `${height}px`);
@@ -31697,7 +31758,17 @@ function applyStudioBoxSize(element, size) {
31697
31758
  promoteInlineForTransform(element);
31698
31759
  applyStudioBoxSizeDimensions(element, size);
31699
31760
  }
31761
+ var boxSizeDraftBases = /* @__PURE__ */ new WeakMap();
31762
+ function readStudioBoxSizeDraftBase(element) {
31763
+ return boxSizeDraftBases.get(element) ?? null;
31764
+ }
31765
+ function forgetStudioBoxSizeDraftBase(element) {
31766
+ boxSizeDraftBases.delete(element);
31767
+ }
31700
31768
  function applyStudioBoxSizeDraft(element, size) {
31769
+ if (!boxSizeDraftBases.has(element)) {
31770
+ boxSizeDraftBases.set(element, { width: element.offsetWidth, height: element.offsetHeight });
31771
+ }
31701
31772
  promoteInlineForTransform(element);
31702
31773
  applyStudioBoxSizeDimensions(element, size);
31703
31774
  }
@@ -31735,7 +31806,7 @@ function prepareStudioRotationBase(element, updateBase) {
31735
31806
  function writeStudioRotationVars(element, rotation, options = {}) {
31736
31807
  prepareStudioRotationBase(element, options.updateBase ?? true);
31737
31808
  element.setAttribute(STUDIO_ROTATION_ATTR, "true");
31738
- element.style.setProperty(STUDIO_ROTATION_PROP, `${roundRotationAngle(rotation.angle)}deg`);
31809
+ element.style.setProperty(STUDIO_ROTATION_PROP, `${roundTo3(rotation.angle)}deg`);
31739
31810
  element.style.setProperty("transform-origin", STUDIO_ROTATION_TRANSFORM_ORIGIN);
31740
31811
  }
31741
31812
  function applyStudioRotation(element, rotation) {
@@ -31753,7 +31824,7 @@ function applyStudioRotationDraft(element, rotation) {
31753
31824
  element.setAttribute(STUDIO_ROTATION_DRAFT_ATTR, "true");
31754
31825
  element.style.setProperty(
31755
31826
  "rotate",
31756
- composeStudioRotationValue(element, `${roundRotationAngle(rotation.angle)}deg`)
31827
+ composeStudioRotationValue(element, `${roundTo3(rotation.angle)}deg`)
31757
31828
  );
31758
31829
  }
31759
31830
 
@@ -31921,6 +31992,7 @@ function restoreStyleProperty(element, property, value) {
31921
31992
  else element.style.removeProperty(property);
31922
31993
  }
31923
31994
  function restoreStudioBoxSize(element, previous) {
31995
+ forgetStudioBoxSizeDraftBase(element);
31924
31996
  restoreStyleProperty(element, "width", previous.width);
31925
31997
  restoreStyleProperty(element, "height", previous.height);
31926
31998
  restoreStyleProperty(element, "min-width", previous.minWidth);
@@ -32050,6 +32122,7 @@ function clearStudioRotation(element) {
32050
32122
  element.removeAttribute(STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR);
32051
32123
  }
32052
32124
  function clearStudioBoxSize(element) {
32125
+ forgetStudioBoxSizeDraftBase(element);
32053
32126
  if (element.hasAttribute(STUDIO_BOX_SIZE_ATTR) || styleUsesStudioSize(element.style.getPropertyValue("width")) || styleUsesStudioSize(element.style.getPropertyValue("height")) || element.hasAttribute(STUDIO_ORIGINAL_SCALE_ATTR)) {
32054
32127
  restoreOriginalBoxSizeProperty(element, "width", STUDIO_ORIGINAL_WIDTH_ATTR);
32055
32128
  restoreOriginalBoxSizeProperty(element, "height", STUDIO_ORIGINAL_HEIGHT_ATTR);
@@ -32303,8 +32376,8 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
32303
32376
  const baseGsapX = Number.isFinite(parsedBaseX) ? parsedBaseX : fallbackBase.x;
32304
32377
  const baseGsapY = Number.isFinite(parsedBaseY) ? parsedBaseY : fallbackBase.y;
32305
32378
  return {
32306
- newX: Math.round(baseGsapX + adjX),
32307
- newY: Math.round(baseGsapY + adjY),
32379
+ newX: roundTo3(baseGsapX + adjX),
32380
+ newY: roundTo3(baseGsapY + adjY),
32308
32381
  baseGsapX,
32309
32382
  baseGsapY
32310
32383
  };
@@ -34301,6 +34374,340 @@ var SnapGuideOverlay = memo23(function SnapGuideOverlay2({
34301
34374
 
34302
34375
  // src/components/editor/DomEditCropHandles.tsx
34303
34376
  import { useEffect as useEffect44, useRef as useRef65, useState as useState40 } from "react";
34377
+
34378
+ // src/hooks/gsapRuntimeKeyframes.ts
34379
+ import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
34380
+ var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
34381
+ "ease",
34382
+ "duration",
34383
+ "delay",
34384
+ "stagger",
34385
+ "motionPath",
34386
+ "overwrite",
34387
+ "immediateRender",
34388
+ "onComplete",
34389
+ "onUpdate",
34390
+ "onStart",
34391
+ "keyframes"
34392
+ ]);
34393
+ function timelinesOf(iframe) {
34394
+ if (!iframe?.contentWindow) return null;
34395
+ try {
34396
+ return iframe.contentWindow.__timelines ?? null;
34397
+ } catch {
34398
+ return null;
34399
+ }
34400
+ }
34401
+ function isXY(p) {
34402
+ return !!p && typeof p.x === "number" && typeof p.y === "number";
34403
+ }
34404
+ function isZeroDurationSet(duration) {
34405
+ return !(duration > 0);
34406
+ }
34407
+ function coordsFromMotionPath(mp) {
34408
+ if (!mp || typeof mp !== "object") return null;
34409
+ const obj = mp;
34410
+ const pathVal = Array.isArray(mp) ? mp : obj.path;
34411
+ if (!Array.isArray(pathVal)) return null;
34412
+ const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
34413
+ if (coords.length < 2) return null;
34414
+ const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
34415
+ const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
34416
+ return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
34417
+ }
34418
+ function flatTweenKeyframes(vars) {
34419
+ const properties = {};
34420
+ for (const [k, v] of Object.entries(vars)) {
34421
+ if (FLAT_SKIP_KEYS.has(k)) continue;
34422
+ if (typeof v === "number") properties[k] = roundTo3(v);
34423
+ else if (typeof v === "string") properties[k] = v;
34424
+ }
34425
+ if (Object.keys(properties).length === 0) return null;
34426
+ return [
34427
+ { percentage: 0, properties },
34428
+ { percentage: 100, properties }
34429
+ ];
34430
+ }
34431
+ function readTween(vars) {
34432
+ if (vars.keyframes && typeof vars.keyframes === "object") {
34433
+ const parsed = parsePercentageKeyframes(vars.keyframes);
34434
+ if (parsed) return parsed;
34435
+ }
34436
+ const mp = coordsFromMotionPath(vars.motionPath);
34437
+ if (mp) {
34438
+ const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
34439
+ if (shape) {
34440
+ const n = shape.waypoints.length;
34441
+ const keyframes = shape.waypoints.map((wp, i) => ({
34442
+ percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
34443
+ properties: { x: wp.x, y: wp.y }
34444
+ }));
34445
+ return { keyframes, arcPath: shape.arcPath };
34446
+ }
34447
+ }
34448
+ const flat = flatTweenKeyframes(vars);
34449
+ return flat ? { keyframes: flat } : null;
34450
+ }
34451
+ function matchesElement(tween, el) {
34452
+ if (!tween.targets) return false;
34453
+ for (const t of tween.targets()) {
34454
+ if (t === el || el.id && t.id === el.id) return true;
34455
+ }
34456
+ return false;
34457
+ }
34458
+ function tweenTiming(tween) {
34459
+ const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
34460
+ const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
34461
+ return {
34462
+ start: Number.isFinite(rawStart) ? rawStart : 0,
34463
+ duration: Number.isFinite(rawDur) ? rawDur : 0
34464
+ };
34465
+ }
34466
+ function varsCarryChannel(vars, channels) {
34467
+ if (!vars) return false;
34468
+ for (const ch of channels) {
34469
+ if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
34470
+ }
34471
+ return false;
34472
+ }
34473
+ function keyframeVarsCarryChannel(vars, channels) {
34474
+ const kf = vars?.keyframes;
34475
+ if (!kf || typeof kf !== "object") return false;
34476
+ if (!Array.isArray(kf) && channels.some((ch) => Object.prototype.hasOwnProperty.call(kf, ch)))
34477
+ return true;
34478
+ const steps = Array.isArray(kf) ? kf : Object.values(kf);
34479
+ return steps.some(
34480
+ (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
34481
+ );
34482
+ }
34483
+ function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
34484
+ const timelines = timelinesOf(iframe);
34485
+ if (!timelines) return null;
34486
+ let targetEl = null;
34487
+ try {
34488
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34489
+ } catch {
34490
+ return null;
34491
+ }
34492
+ if (!targetEl) return null;
34493
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34494
+ const wantChannels = channels && channels.length > 0 ? channels : null;
34495
+ let first = null;
34496
+ let channelMatch = null;
34497
+ for (const tlId of tlIds) {
34498
+ const timeline = timelines[tlId];
34499
+ if (!timeline?.getChildren) continue;
34500
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34501
+ for (const tween of timeline.getChildren(true)) {
34502
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34503
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34504
+ const isSet = !(dur > 0);
34505
+ if (kind === "set" ? !isSet : isSet) continue;
34506
+ if (wantChannels) {
34507
+ const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
34508
+ if (carries) {
34509
+ if (channelMatch === null) channelMatch = { tween, timeline };
34510
+ } else if (first === null) {
34511
+ first = { tween, timeline };
34512
+ }
34513
+ continue;
34514
+ }
34515
+ if (first === null) first = { tween, timeline };
34516
+ if (kind === "keyframe" && now2 != null) {
34517
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34518
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
34519
+ }
34520
+ }
34521
+ }
34522
+ return channelMatch ?? first;
34523
+ }
34524
+ function readCarriesChannel(read, channels) {
34525
+ return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
34526
+ }
34527
+ function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
34528
+ const timelines = timelinesOf(iframe);
34529
+ if (!timelines) return null;
34530
+ let targetEl = null;
34531
+ try {
34532
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34533
+ } catch {
34534
+ return null;
34535
+ }
34536
+ if (!targetEl) return null;
34537
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34538
+ if (tlIds.length === 0) return null;
34539
+ let firstRead = null;
34540
+ for (const tlId of tlIds) {
34541
+ const timeline = timelines[tlId];
34542
+ if (!timeline?.getChildren) continue;
34543
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34544
+ for (const tween of timeline.getChildren(true)) {
34545
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34546
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34547
+ if (isZeroDurationSet(dur)) continue;
34548
+ const read = readTween(tween.vars);
34549
+ if (!read) continue;
34550
+ if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
34551
+ if (firstRead === null) firstRead = read;
34552
+ if (now2 != null) {
34553
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34554
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
34555
+ }
34556
+ }
34557
+ }
34558
+ return firstRead;
34559
+ }
34560
+ function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
34561
+ let targetEl = null;
34562
+ try {
34563
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34564
+ } catch {
34565
+ return false;
34566
+ }
34567
+ return !!targetEl && hasNonHoldTween(timelinesOf(iframe), targetEl, channels, compositionId);
34568
+ }
34569
+ function elementHasNonHoldTween(el, channels) {
34570
+ const win = el.ownerDocument.defaultView;
34571
+ return hasNonHoldTween(win?.__timelines ?? null, el, channels);
34572
+ }
34573
+ function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
34574
+ if (!timelines) return false;
34575
+ for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
34576
+ for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
34577
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34578
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34579
+ if (isZeroDurationSet(dur)) continue;
34580
+ if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
34581
+ const read = readTween(tween.vars);
34582
+ if (read && (!channels || readCarriesChannel(read, channels))) return true;
34583
+ }
34584
+ }
34585
+ return false;
34586
+ }
34587
+ function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
34588
+ if (!clip || clip.duration <= 0) return keyframes;
34589
+ return keyframes.map((kf) => {
34590
+ const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
34591
+ return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
34592
+ });
34593
+ }
34594
+ function buildEntry(read, start, duration, clip) {
34595
+ return {
34596
+ keyframes: toClipRelative(read.keyframes, start, duration, clip),
34597
+ tweenStart: start,
34598
+ tweenDuration: duration,
34599
+ ...read.easeEach ? { easeEach: read.easeEach } : {},
34600
+ ...read.arcPath ? { arcPath: read.arcPath } : {}
34601
+ };
34602
+ }
34603
+ function addScanEntry(result, tween, clipById) {
34604
+ if (!tween.targets || !tween.vars) return;
34605
+ const { start, duration } = tweenTiming(tween);
34606
+ if (isZeroDurationSet(duration)) return;
34607
+ const read = readTween(tween.vars);
34608
+ if (!read) return;
34609
+ for (const target of tween.targets()) {
34610
+ const id = target.id;
34611
+ if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
34612
+ }
34613
+ }
34614
+ function scanAllRuntimeKeyframes(iframe, clipById) {
34615
+ const result = /* @__PURE__ */ new Map();
34616
+ const timelines = timelinesOf(iframe);
34617
+ if (!timelines) return result;
34618
+ for (const timeline of Object.values(timelines)) {
34619
+ if (!timeline?.getChildren) continue;
34620
+ for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
34621
+ }
34622
+ return result;
34623
+ }
34624
+
34625
+ // src/components/editor/cropResize.ts
34626
+ var boxOf = (element) => ({
34627
+ width: element.offsetWidth,
34628
+ height: element.offsetHeight
34629
+ });
34630
+ function cropFollows(element) {
34631
+ const clip = gsapAnimatesProperty(element, "clipPath");
34632
+ return {
34633
+ width: !clip && !elementHasNonHoldTween(element, ["width"]),
34634
+ height: !clip && !elementHasNonHoldTween(element, ["height"])
34635
+ };
34636
+ }
34637
+ function scaleCrop(crop, from, to, follows) {
34638
+ const sx = follows.width && from.width > 0 ? to.width / from.width : 1;
34639
+ const sy = follows.height && from.height > 0 ? to.height / from.height : 1;
34640
+ return {
34641
+ top: crop.top * sy,
34642
+ right: crop.right * sx,
34643
+ bottom: crop.bottom * sy,
34644
+ left: crop.left * sx,
34645
+ radius: crop.radius
34646
+ };
34647
+ }
34648
+ function readCropFollowingResize(element) {
34649
+ const crop = readElementCropInsets(element);
34650
+ const base2 = readStudioBoxSizeDraftBase(element);
34651
+ return crop && base2 ? scaleCrop(crop, base2, boxOf(element), cropFollows(element)) : crop;
34652
+ }
34653
+ function prepareCropResize(element) {
34654
+ const from = readStudioBoxSizeDraftBase(element) ?? boxOf(element);
34655
+ const before = element.style.getPropertyValue("clip-path");
34656
+ const priority = element.style.getPropertyPriority("clip-path");
34657
+ const follows = cropFollows(element);
34658
+ const crop = readElementCropInsets(element);
34659
+ return () => {
34660
+ forgetStudioBoxSizeDraftBase(element);
34661
+ const to = boxOf(element);
34662
+ const untouched = element.style.getPropertyValue("clip-path") === before;
34663
+ if (!crop || !hasCropInsets(crop) || !untouched || !(to.width > 0 && to.height > 0))
34664
+ return null;
34665
+ const clip = buildInsetClipPathSides(scaleCrop(crop, from, to, follows), crop.radius);
34666
+ if (clip === buildInsetClipPathSides(crop, crop.radius)) return null;
34667
+ element.style.setProperty("clip-path", clip, priority);
34668
+ return {
34669
+ patch: {
34670
+ type: "inline-style",
34671
+ property: "clip-path",
34672
+ value: priority ? `${clip} !important` : clip
34673
+ },
34674
+ revert: () => {
34675
+ if (before) element.style.setProperty("clip-path", before, priority);
34676
+ else element.style.removeProperty("clip-path");
34677
+ }
34678
+ };
34679
+ };
34680
+ }
34681
+ async function saveCropResize(stage, selection, commit, coalesceKey) {
34682
+ const crop = stage();
34683
+ if (!crop) return;
34684
+ try {
34685
+ await commit(selection, [crop.patch], {
34686
+ label: "Resize layer",
34687
+ coalesceKey,
34688
+ coalesceMs: Number.POSITIVE_INFINITY
34689
+ });
34690
+ } catch (error) {
34691
+ crop.revert();
34692
+ throw error;
34693
+ }
34694
+ }
34695
+ var sizeWriteCounter = 0;
34696
+ async function writeSizeWithCrop(selection, properties, mutation, commit, write) {
34697
+ if (!mutation || !("width" in properties || "height" in properties)) return write();
34698
+ const stage = prepareCropResize(selection.element);
34699
+ const coalesceKey = `size-write:${++sizeWriteCounter}`;
34700
+ const undoStep = { coalesceKey, coalesceMs: Number.POSITIVE_INFINITY };
34701
+ const keyed = (s, m, options) => mutation(s, m, { ...options, ...undoStep });
34702
+ if (mutation.batch) {
34703
+ const batch = mutation.batch;
34704
+ keyed.batch = (calls, options) => batch(calls, { ...options, ...undoStep });
34705
+ }
34706
+ await write(keyed);
34707
+ await saveCropResize(stage, selection, commit, coalesceKey);
34708
+ }
34709
+
34710
+ // src/components/editor/DomEditCropHandles.tsx
34304
34711
  import { Fragment as Fragment26, jsx as jsx84, jsxs as jsxs57 } from "react/jsx-runtime";
34305
34712
  var EDGE_HIT_THICKNESS = 12;
34306
34713
  var EDGE_HIT_LENGTH = 32;
@@ -34341,6 +34748,7 @@ function repositionHandleSize(rect) {
34341
34748
  return Math.min(REPOSITION_HANDLE_PX, Math.max(rect.width, rect.height) - RESIZE_HANDLE_HIT_PX);
34342
34749
  }
34343
34750
  var EDGES = ["top", "right", "bottom", "left"];
34751
+ var NO_CROP = { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
34344
34752
  function DomEditCropHandles({
34345
34753
  selection,
34346
34754
  overlayRect,
@@ -34351,38 +34759,28 @@ function DomEditCropHandles({
34351
34759
  const [hotEdge, setHotEdge] = useState40(null);
34352
34760
  const cropStateFor = (element) => {
34353
34761
  const parsed = readElementCropInsets(element);
34354
- const { radius, ...insets } = parsed ?? { top: 0, right: 0, bottom: 0, left: 0, radius: 0 };
34355
- return { element, croppable: parsed !== null, insets, radius };
34762
+ const { top, right, bottom, left } = parsed ?? NO_CROP;
34763
+ return { element, croppable: parsed !== null, insets: { top, right, bottom, left } };
34356
34764
  };
34357
34765
  const [state, setState] = useState40(() => cropStateFor(selection.element));
34358
34766
  if (state.element !== selection.element) {
34359
34767
  setState(cropStateFor(selection.element));
34360
34768
  }
34361
- const hasCrop = state.insets.top > 0 || state.insets.right > 0 || state.insets.bottom > 0 || state.insets.left > 0;
34362
- const liftedRef = useRef65(false);
34363
- const preLiftInlineClipRef = useRef65("");
34364
- const committedClipRef = useRef65(null);
34769
+ const committed = readCropFollowingResize(selection.element) ?? NO_CROP;
34770
+ const insets = dragging ? state.insets : committed;
34771
+ const hasCrop = hasCropInsets(insets);
34365
34772
  useEffect44(() => {
34366
34773
  const el = selection.element;
34367
34774
  if (readElementCropInsets(el) === null) return;
34368
- preLiftInlineClipRef.current = el.style.getPropertyValue("clip-path");
34369
- committedClipRef.current = null;
34370
- el.style.setProperty("clip-path", "none");
34371
- liftedRef.current = true;
34372
- return () => {
34373
- liftedRef.current = false;
34374
- const committed = committedClipRef.current;
34375
- const restore = committed !== null ? committed || null : preLiftInlineClipRef.current || null;
34376
- if (restore) el.style.setProperty("clip-path", restore);
34377
- else el.style.removeProperty("clip-path");
34378
- };
34775
+ liftElementCrop(el);
34776
+ return () => dropElementCropLift(el);
34379
34777
  }, [selection.element]);
34380
34778
  const frame = readElementCropFrame(selection.element, overlayRect);
34381
34779
  const width = frame.width / frame.scaleX;
34382
34780
  const height = frame.height / frame.scaleY;
34383
34781
  const cropRect = cropRectFromInsets(
34384
34782
  { left: 0, top: 0, width: frame.width, height: frame.height },
34385
- state.insets,
34783
+ insets,
34386
34784
  frame.scaleX,
34387
34785
  frame.scaleY
34388
34786
  );
@@ -34392,17 +34790,20 @@ function DomEditCropHandles({
34392
34790
  event.preventDefault();
34393
34791
  event.stopPropagation();
34394
34792
  event.currentTarget.setPointerCapture(event.pointerId);
34793
+ const pressed = readCropFollowingResize(selection.element) ?? NO_CROP;
34395
34794
  gestureRef.current = {
34396
34795
  edge,
34397
34796
  pointerId: event.pointerId,
34398
34797
  startX: event.clientX,
34399
34798
  startY: event.clientY,
34400
- startInsets: state.insets,
34401
- insets: state.insets,
34799
+ startInsets: pressed,
34800
+ insets: pressed,
34801
+ radius: pressed.radius,
34402
34802
  angleDeg: frame.angleDeg,
34403
34803
  scaleX: frame.scaleX,
34404
34804
  scaleY: frame.scaleY
34405
34805
  };
34806
+ setState((prev) => ({ ...prev, insets: pressed }));
34406
34807
  setDragging(true);
34407
34808
  };
34408
34809
  const updateCropGesture = (event) => {
@@ -34438,19 +34839,10 @@ function DomEditCropHandles({
34438
34839
  const finishCropGesture = (event) => {
34439
34840
  const gesture = endCropGesture(event);
34440
34841
  if (!gesture) return;
34441
- const el = selection.element;
34442
- const reLift = () => {
34443
- if (liftedRef.current) el.style.setProperty("clip-path", "none");
34444
- };
34445
- const { insets } = gesture;
34446
- const committedValue = buildInsetClipPathSides(insets, state.radius);
34447
- if (committedValue === buildInsetClipPathSides(gesture.startInsets, state.radius)) return;
34448
- const cropped = insets.top > 0 || insets.right > 0 || insets.bottom > 0 || insets.left > 0;
34449
- const commit = onStyleCommit?.("clip-path", committedValue);
34450
- reLift();
34451
- void Promise.resolve(commit).then(() => {
34452
- committedClipRef.current = cropped ? committedValue : "";
34453
- }, reLift);
34842
+ const value = buildInsetClipPathSides(gesture.insets, gesture.radius);
34843
+ if (value === buildInsetClipPathSides(gesture.startInsets, gesture.radius)) return;
34844
+ const commit = onStyleCommit?.("clip-path", value);
34845
+ void Promise.resolve(commit).catch(() => void 0);
34454
34846
  };
34455
34847
  const cancelCropGesture = (event) => {
34456
34848
  const gesture = endCropGesture(event);
@@ -34803,19 +35195,19 @@ function DomEditSelectionChrome({
34803
35195
  // src/hooks/useCropOverlay.ts
34804
35196
  function useCropOverlay(params) {
34805
35197
  const { selection, overlayRect } = params;
34806
- const cropInsets = selection ? readElementCropInsets(selection.element) : null;
34807
- const hasCropInsets = Boolean(
35198
+ const cropInsets = selection && !isElementCropLifted(selection.element) ? readElementCropInsets(selection.element) : null;
35199
+ const hasCropInsets2 = Boolean(
34808
35200
  cropInsets && (cropInsets.top > 0 || cropInsets.right > 0 || cropInsets.bottom > 0 || cropInsets.left > 0)
34809
35201
  );
34810
35202
  const sx = overlayRect && overlayRect.editScaleX > 0 ? overlayRect.editScaleX : 1;
34811
35203
  const sy = overlayRect && overlayRect.editScaleY > 0 ? overlayRect.editScaleY : 1;
34812
- const cropOutlineInsetPx = cropInsets && hasCropInsets ? {
35204
+ const cropOutlineInsetPx = cropInsets && hasCropInsets2 ? {
34813
35205
  top: cropInsets.top * sy,
34814
35206
  right: cropInsets.right * sx,
34815
35207
  bottom: cropInsets.bottom * sy,
34816
35208
  left: cropInsets.left * sx
34817
35209
  } : void 0;
34818
- return { hasCropInsets, cropOutlineInsetPx };
35210
+ return { hasCropInsets: hasCropInsets2, cropOutlineInsetPx };
34819
35211
  }
34820
35212
 
34821
35213
  // src/components/editor/domEditOverlayShape.ts
@@ -36270,11 +36662,11 @@ var DomEditOverlay = memo25(function DomEditOverlay2({
36270
36662
  const compRect = useDomEditCompositionRect({ iframeRef, overlayRef });
36271
36663
  const compRectRef = useRef69(compRect);
36272
36664
  compRectRef.current = compRect;
36273
- const { hasCropInsets, cropOutlineInsetPx } = useCropOverlay({
36665
+ const { hasCropInsets: hasCropInsets2, cropOutlineInsetPx } = useCropOverlay({
36274
36666
  selection,
36275
36667
  overlayRect
36276
36668
  });
36277
- const boxClipPath = hasCropInsets ? void 0 : selectionShapeStyles.clipPath;
36669
+ const boxClipPath = hasCropInsets2 ? void 0 : selectionShapeStyles.clipPath;
36278
36670
  const boxChromeClass = resolveBoxChromeClass(Boolean(cropOutlineInsetPx), boxClipPath);
36279
36671
  const { offCanvasRects, offCanvasElementsRef } = useOffCanvasIndicators({
36280
36672
  iframeRef,
@@ -37576,7 +37968,12 @@ async function commitCandidateEdit(edit, targetPath, originalContent, deps, opti
37576
37968
  });
37577
37969
  }
37578
37970
  if (refreshTarget) refreshCommittedEdit(edit.after, deps, options);
37579
- return { status: "committed", version: hashContent(edit.after) };
37971
+ return {
37972
+ status: "committed",
37973
+ version: hashContent(edit.after),
37974
+ before: originalContent,
37975
+ after: edit.after
37976
+ };
37580
37977
  }
37581
37978
  async function persistSdkCandidateMutation(live, targetPath, originalContent, deps, mutate, options, sourceSnapshot) {
37582
37979
  return serializeStudioFileMutation(
@@ -39854,6 +40251,10 @@ var GsapEditBlockedError = class extends Error {
39854
40251
  function assertGsapEditPersisted(outcome) {
39855
40252
  if (outcome.status === "blocked") throw new GsapEditBlockedError(outcome.reason, outcome.detail);
39856
40253
  }
40254
+ async function saveMove(outcome, saveOnElement) {
40255
+ if (outcome.status === "element-offset") return saveOnElement();
40256
+ assertGsapEditPersisted(outcome);
40257
+ }
39857
40258
  function assertGsapAnimationDirectlyEditable(animation) {
39858
40259
  const editability = editabilityForProvenance(animation.provenance);
39859
40260
  if (editability === "unroll") throw new GsapEditBlockedError("unroll-required");
@@ -40034,8 +40435,8 @@ async function commitStaticGsapRotation(selection, newRotation, selector, existi
40034
40435
  );
40035
40436
  }
40036
40437
  async function commitStaticGsapSize(selection, size, selector, existingSet, callbacks) {
40037
- const width = Math.round(size.width);
40038
- const height = Math.round(size.height);
40438
+ const width = roundToLayoutPx(size.width);
40439
+ const height = roundToLayoutPx(size.height);
40039
40440
  if (existingSet) {
40040
40441
  await callbacks.commitMutation(
40041
40442
  selection,
@@ -40063,7 +40464,7 @@ async function commitStaticGsapSize(selection, size, selector, existingSet, call
40063
40464
  );
40064
40465
  }
40065
40466
  function positiveOr(n, fallback) {
40066
- return Number.isFinite(n) && n > 0 ? Math.round(n) : fallback;
40467
+ return Number.isFinite(n) && n > 0 ? roundToLayoutPx(n) : fallback;
40067
40468
  }
40068
40469
  function resolvePriorSize(sizeSet, el, fallbackW, fallbackH) {
40069
40470
  if (sizeSet) {
@@ -40080,8 +40481,8 @@ async function commitKeyframedSizeFromResize(selection, size, selector, sizeSet,
40080
40481
  const ts = resolveTweenStart(animatedTween) ?? 0;
40081
40482
  const td = resolveTweenDuration(animatedTween);
40082
40483
  if (!(td > 0)) return false;
40083
- const newW = Math.round(size.width);
40084
- const newH = Math.round(size.height);
40484
+ const newW = roundToLayoutPx(size.width);
40485
+ const newH = roundToLayoutPx(size.height);
40085
40486
  const prior = resolvePriorSize(sizeSet, selection.element, newW, newH);
40086
40487
  const ct = usePlayerStore.getState().currentTime;
40087
40488
  const pct = Math.max(0, Math.min(100, Math.round((ct - ts) / td * 1e3) / 10));
@@ -40411,249 +40812,6 @@ function commitCreatePath(targetSelector, position, x, y, commit) {
40411
40812
 
40412
40813
  // src/components/editor/useMotionPathData.ts
40413
40814
  import { useEffect as useEffect52, useRef as useRef73, useState as useState45 } from "react";
40414
-
40415
- // src/hooks/gsapRuntimeKeyframes.ts
40416
- import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
40417
- var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
40418
- "ease",
40419
- "duration",
40420
- "delay",
40421
- "stagger",
40422
- "motionPath",
40423
- "overwrite",
40424
- "immediateRender",
40425
- "onComplete",
40426
- "onUpdate",
40427
- "onStart",
40428
- "keyframes"
40429
- ]);
40430
- function timelinesOf(iframe) {
40431
- if (!iframe?.contentWindow) return null;
40432
- try {
40433
- return iframe.contentWindow.__timelines ?? null;
40434
- } catch {
40435
- return null;
40436
- }
40437
- }
40438
- function isXY(p) {
40439
- return !!p && typeof p.x === "number" && typeof p.y === "number";
40440
- }
40441
- function isZeroDurationSet(duration) {
40442
- return !(duration > 0);
40443
- }
40444
- function coordsFromMotionPath(mp) {
40445
- if (!mp || typeof mp !== "object") return null;
40446
- const obj = mp;
40447
- const pathVal = Array.isArray(mp) ? mp : obj.path;
40448
- if (!Array.isArray(pathVal)) return null;
40449
- const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
40450
- if (coords.length < 2) return null;
40451
- const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
40452
- const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
40453
- return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
40454
- }
40455
- function flatTweenKeyframes(vars) {
40456
- const properties = {};
40457
- for (const [k, v] of Object.entries(vars)) {
40458
- if (FLAT_SKIP_KEYS.has(k)) continue;
40459
- if (typeof v === "number") properties[k] = roundTo3(v);
40460
- else if (typeof v === "string") properties[k] = v;
40461
- }
40462
- if (Object.keys(properties).length === 0) return null;
40463
- return [
40464
- { percentage: 0, properties },
40465
- { percentage: 100, properties }
40466
- ];
40467
- }
40468
- function readTween(vars) {
40469
- if (vars.keyframes && typeof vars.keyframes === "object") {
40470
- const parsed = parsePercentageKeyframes(vars.keyframes);
40471
- if (parsed) return parsed;
40472
- }
40473
- const mp = coordsFromMotionPath(vars.motionPath);
40474
- if (mp) {
40475
- const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
40476
- if (shape) {
40477
- const n = shape.waypoints.length;
40478
- const keyframes = shape.waypoints.map((wp, i) => ({
40479
- percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
40480
- properties: { x: wp.x, y: wp.y }
40481
- }));
40482
- return { keyframes, arcPath: shape.arcPath };
40483
- }
40484
- }
40485
- const flat = flatTweenKeyframes(vars);
40486
- return flat ? { keyframes: flat } : null;
40487
- }
40488
- function matchesElement(tween, el) {
40489
- if (!tween.targets) return false;
40490
- for (const t of tween.targets()) {
40491
- if (t === el || el.id && t.id === el.id) return true;
40492
- }
40493
- return false;
40494
- }
40495
- function tweenTiming(tween) {
40496
- const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
40497
- const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
40498
- return {
40499
- start: Number.isFinite(rawStart) ? rawStart : 0,
40500
- duration: Number.isFinite(rawDur) ? rawDur : 0
40501
- };
40502
- }
40503
- function varsCarryChannel(vars, channels) {
40504
- if (!vars) return false;
40505
- for (const ch of channels) {
40506
- if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
40507
- }
40508
- return false;
40509
- }
40510
- function keyframeVarsCarryChannel(vars, channels) {
40511
- const kf = vars?.keyframes;
40512
- if (!kf || typeof kf !== "object") return false;
40513
- const steps = Array.isArray(kf) ? kf : Object.values(kf);
40514
- return steps.some(
40515
- (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
40516
- );
40517
- }
40518
- function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
40519
- const timelines = timelinesOf(iframe);
40520
- if (!timelines) return null;
40521
- let targetEl = null;
40522
- try {
40523
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
40524
- } catch {
40525
- return null;
40526
- }
40527
- if (!targetEl) return null;
40528
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
40529
- const wantChannels = channels && channels.length > 0 ? channels : null;
40530
- let first = null;
40531
- let channelMatch = null;
40532
- for (const tlId of tlIds) {
40533
- const timeline = timelines[tlId];
40534
- if (!timeline?.getChildren) continue;
40535
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
40536
- for (const tween of timeline.getChildren(true)) {
40537
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
40538
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
40539
- const isSet = !(dur > 0);
40540
- if (kind === "set" ? !isSet : isSet) continue;
40541
- if (wantChannels) {
40542
- const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
40543
- if (carries) {
40544
- if (channelMatch === null) channelMatch = { tween, timeline };
40545
- } else if (first === null) {
40546
- first = { tween, timeline };
40547
- }
40548
- continue;
40549
- }
40550
- if (first === null) first = { tween, timeline };
40551
- if (kind === "keyframe" && now2 != null) {
40552
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
40553
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
40554
- }
40555
- }
40556
- }
40557
- return channelMatch ?? first;
40558
- }
40559
- function readCarriesChannel(read, channels) {
40560
- return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
40561
- }
40562
- function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
40563
- const timelines = timelinesOf(iframe);
40564
- if (!timelines) return null;
40565
- let targetEl = null;
40566
- try {
40567
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
40568
- } catch {
40569
- return null;
40570
- }
40571
- if (!targetEl) return null;
40572
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
40573
- if (tlIds.length === 0) return null;
40574
- let firstRead = null;
40575
- for (const tlId of tlIds) {
40576
- const timeline = timelines[tlId];
40577
- if (!timeline?.getChildren) continue;
40578
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
40579
- for (const tween of timeline.getChildren(true)) {
40580
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
40581
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
40582
- if (isZeroDurationSet(dur)) continue;
40583
- const read = readTween(tween.vars);
40584
- if (!read) continue;
40585
- if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
40586
- if (firstRead === null) firstRead = read;
40587
- if (now2 != null) {
40588
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
40589
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
40590
- }
40591
- }
40592
- }
40593
- return firstRead;
40594
- }
40595
- function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
40596
- const timelines = timelinesOf(iframe);
40597
- if (!timelines) return false;
40598
- const tlId = compositionId || Object.keys(timelines).find((k) => typeof timelines[k]?.getChildren === "function");
40599
- if (!tlId) return false;
40600
- const timeline = timelines[tlId];
40601
- if (!timeline?.getChildren) return false;
40602
- let targetEl = null;
40603
- try {
40604
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
40605
- } catch {
40606
- return false;
40607
- }
40608
- if (!targetEl) return false;
40609
- for (const tween of timeline.getChildren(true)) {
40610
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
40611
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
40612
- if (isZeroDurationSet(dur)) continue;
40613
- const read = readTween(tween.vars);
40614
- if (read && (!channels || readCarriesChannel(read, channels))) return true;
40615
- }
40616
- return false;
40617
- }
40618
- function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
40619
- if (!clip || clip.duration <= 0) return keyframes;
40620
- return keyframes.map((kf) => {
40621
- const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
40622
- return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
40623
- });
40624
- }
40625
- function buildEntry(read, start, duration, clip) {
40626
- return {
40627
- keyframes: toClipRelative(read.keyframes, start, duration, clip),
40628
- tweenStart: start,
40629
- tweenDuration: duration,
40630
- ...read.easeEach ? { easeEach: read.easeEach } : {},
40631
- ...read.arcPath ? { arcPath: read.arcPath } : {}
40632
- };
40633
- }
40634
- function addScanEntry(result, tween, clipById) {
40635
- if (!tween.targets || !tween.vars) return;
40636
- const { start, duration } = tweenTiming(tween);
40637
- if (isZeroDurationSet(duration)) return;
40638
- const read = readTween(tween.vars);
40639
- if (!read) return;
40640
- for (const target of tween.targets()) {
40641
- const id = target.id;
40642
- if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
40643
- }
40644
- }
40645
- function scanAllRuntimeKeyframes(iframe, clipById) {
40646
- const result = /* @__PURE__ */ new Map();
40647
- const timelines = timelinesOf(iframe);
40648
- if (!timelines) return result;
40649
- for (const timeline of Object.values(timelines)) {
40650
- if (!timeline?.getChildren) continue;
40651
- for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
40652
- }
40653
- return result;
40654
- }
40655
-
40656
- // src/components/editor/useMotionPathData.ts
40657
40815
  function transformTranslate(el) {
40658
40816
  const t = el.ownerDocument?.defaultView?.getComputedStyle(el).transform;
40659
40817
  if (!t || t === "none") return { x: 0, y: 0 };
@@ -42451,7 +42609,7 @@ async function tryGsapRotationIntercept(selection, angle, animations, iframe, co
42451
42609
  if (!anim) {
42452
42610
  anim = workingAnimations.find((a) => animationWritesAnyProperty(a, ROTATION_CHANNEL_SET)) ?? null;
42453
42611
  }
42454
- const newRotation = Math.round(angle);
42612
+ const newRotation = roundTo3(angle);
42455
42613
  if (!anim || isInstantHold(anim)) {
42456
42614
  const existingSet = anim ?? findRotationSetAnimation(resolvedAnimations, selector, selection.element);
42457
42615
  await commitStaticGsapRotation(selection, newRotation, selector, existingSet, {
@@ -70976,7 +71134,7 @@ function useTimelineGroupEditing({
70976
71134
  async (input) => {
70977
71135
  const sharedPath = allChangesSharePath(input.changes, activeCompPath);
70978
71136
  const canUseSdk = !input.needsExtension && sharedPath !== null && input.eligible && input.sdkChanges.every((change) => change !== null);
70979
- if (!canUseSdk) return false;
71137
+ if (!canUseSdk) return null;
70980
71138
  const result = await sdkTimingBatchPersist(
70981
71139
  input.sdkChanges.filter((change) => change !== null),
70982
71140
  sharedPath,
@@ -70996,7 +71154,7 @@ function useTimelineGroupEditing({
70996
71154
  skipRefresh: true
70997
71155
  }
70998
71156
  );
70999
- return cutoverCommittedOrThrow(result);
71157
+ return cutoverCommittedOrThrow(result) ? { sdkGsap: sdkTimingGsapSync(result) } : null;
71000
71158
  },
71001
71159
  [
71002
71160
  activeCompPath,
@@ -71033,7 +71191,7 @@ function useTimelineGroupEditing({
71033
71191
  const label = options?.label ?? "Move timeline clips";
71034
71192
  return enqueueGroupOperation(label, async (projectId) => {
71035
71193
  await options?.beforeTiming;
71036
- const handledBySdk = await trySdkBatchPersist({
71194
+ const sdk = await trySdkBatchPersist({
71037
71195
  changes,
71038
71196
  sdkChanges: toSdkTimingChanges(changes, (change) => ({
71039
71197
  start: toAuthoredStart(change.element, change.start)
@@ -71044,7 +71202,7 @@ function useTimelineGroupEditing({
71044
71202
  coalesceKey,
71045
71203
  coalesceMs
71046
71204
  });
71047
- if (!handledBySdk) {
71205
+ if (!sdk) {
71048
71206
  await persistServerBatch(
71049
71207
  projectId,
71050
71208
  label,
@@ -71075,6 +71233,7 @@ function useTimelineGroupEditing({
71075
71233
  writeProjectFile,
71076
71234
  activeCompPath,
71077
71235
  changes,
71236
+ sdkGsap: sdk?.sdkGsap,
71078
71237
  resolveChangePath: (element) => targetPathFor(element, activeCompPath),
71079
71238
  mutateChange: (change, changePath) => {
71080
71239
  const delta = change.start - change.element.start;
@@ -71134,7 +71293,7 @@ function useTimelineGroupEditing({
71134
71293
  const coalesceMs = options?.coalesceMs;
71135
71294
  return enqueueGroupOperation("Resize timeline clips", async (projectId) => {
71136
71295
  await options?.beforeTiming;
71137
- const handledBySdk = await trySdkBatchPersist({
71296
+ const sdk = await trySdkBatchPersist({
71138
71297
  changes,
71139
71298
  sdkChanges: toSdkTimingChanges(changes, (change) => ({
71140
71299
  start: toAuthoredStart(change.element, change.start),
@@ -71146,7 +71305,7 @@ function useTimelineGroupEditing({
71146
71305
  coalesceKey,
71147
71306
  coalesceMs
71148
71307
  });
71149
- if (!handledBySdk) {
71308
+ if (!sdk) {
71150
71309
  await persistServerBatch(
71151
71310
  projectId,
71152
71311
  "Resize timeline clips",
@@ -71174,6 +71333,7 @@ function useTimelineGroupEditing({
71174
71333
  writeProjectFile,
71175
71334
  activeCompPath,
71176
71335
  changes,
71336
+ sdkGsap: sdk?.sdkGsap,
71177
71337
  resolveChangePath: (element) => targetPathFor(element, activeCompPath),
71178
71338
  mutateChange: (change, changePath) => {
71179
71339
  const domId = change.element.domId;
@@ -71378,10 +71538,8 @@ function useTimelineEditing({
71378
71538
  );
71379
71539
  };
71380
71540
  const coalesceKey = `timeline-move:${element.hfId ?? element.id}`;
71381
- const finishMoveGsapSync = () => (
71382
- // Every timing writer converges the same GSAP positions after its
71383
- // durable clip-start commit. The SDK owns the attribute write; this
71384
- // sync owns only the dependent animation rewrite and preview refresh.
71541
+ const finishMoveGsapSync = (sdkGsap) => (
71542
+ // One GSAP sync per edit: the SDK commit's own (sdkGsap), else the server rewrite here.
71385
71543
  finishClipTimingFallback({
71386
71544
  iframe: previewIframeRef.current,
71387
71545
  reloadPreview,
@@ -71392,11 +71550,12 @@ function useTimelineEditing({
71392
71550
  coalesceKey,
71393
71551
  recordEdit,
71394
71552
  writeProjectFile,
71395
- edit: { kind: "shift", delta: updates.start - element.start }
71553
+ edit: { kind: "shift", delta: updates.start - element.start },
71554
+ sdkGsap
71396
71555
  }).finally(() => invalidateGsapCache?.())
71397
71556
  );
71398
71557
  const moveFallback = () => enqueueEdit(element, "Move timeline clip", buildMovePatches, coalesceKey).then(
71399
- finishMoveGsapSync
71558
+ () => finishMoveGsapSync()
71400
71559
  );
71401
71560
  return reorderDone.then(() => {
71402
71561
  if (sdkSession && element.hfId && !needsExtension && !trackChanged) {
@@ -71418,7 +71577,7 @@ function useTimelineEditing({
71418
71577
  { label: "Move timeline clip", coalesceKey, skipRefresh: true }
71419
71578
  ).then((result) => {
71420
71579
  if (!cutoverCommittedOrThrow(result)) return moveFallback();
71421
- return finishMoveGsapSync();
71580
+ return finishMoveGsapSync(sdkTimingGsapSync(result));
71422
71581
  });
71423
71582
  }
71424
71583
  return moveFallback();
@@ -71467,7 +71626,7 @@ function useTimelineEditing({
71467
71626
  };
71468
71627
  const hasPbsAdjustment = updates.playbackStart != null || updates.start !== element.start && element.playbackStart != null;
71469
71628
  const coalesceKey = `timeline-resize:${element.hfId ?? element.id}`;
71470
- const finishResizeGsapSync = () => finishClipTimingFallback({
71629
+ const finishResizeGsapSync = (sdkGsap) => finishClipTimingFallback({
71471
71630
  iframe: previewIframeRef.current,
71472
71631
  reloadPreview,
71473
71632
  projectId: projectIdRef.current,
@@ -71481,10 +71640,11 @@ function useTimelineEditing({
71481
71640
  kind: "scale",
71482
71641
  from: { start: toCompositionTime(element, element.start), duration: element.duration },
71483
71642
  to: { start: toCompositionTime(element, updates.start), duration: updates.duration }
71484
- }
71643
+ },
71644
+ sdkGsap
71485
71645
  }).finally(() => invalidateGsapCache?.());
71486
71646
  const resizeFallback = () => enqueueEdit(element, "Resize timeline clip", buildResizePatches, coalesceKey).then(
71487
- finishResizeGsapSync
71647
+ () => finishResizeGsapSync()
71488
71648
  );
71489
71649
  const persistDone = sdkSession && element.hfId && !hasPbsAdjustment && !needsExtension ? sdkTimingPersist(
71490
71650
  element.hfId,
@@ -71504,7 +71664,7 @@ function useTimelineEditing({
71504
71664
  { label: "Resize timeline clip", coalesceKey, skipRefresh: true }
71505
71665
  ).then((result) => {
71506
71666
  if (!cutoverCommittedOrThrow(result)) return resizeFallback();
71507
- return finishResizeGsapSync();
71667
+ return finishResizeGsapSync(sdkTimingGsapSync(result));
71508
71668
  }) : resizeFallback();
71509
71669
  return persistDone.catch((error) => {
71510
71670
  rollbackDuration();
@@ -73925,8 +74085,8 @@ function readDragStart(el, axis) {
73925
74085
  function applyElementPositionOffset(el, gestureOffset) {
73926
74086
  const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
73927
74087
  if (!cs) return "anchored";
73928
- const dx = Math.round(gestureOffset.x - readDragStart(el, "x"));
73929
- const dy = Math.round(gestureOffset.y - readDragStart(el, "y"));
74088
+ const dx = roundTo3(gestureOffset.x - readDragStart(el, "x"));
74089
+ const dy = roundTo3(gestureOffset.y - readDragStart(el, "y"));
73930
74090
  const lifted = el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR) === "static";
73931
74091
  const isStatic = lifted || !/^(relative|absolute|fixed|sticky)$/.test(cs.position);
73932
74092
  if (!isStatic && (cs.left.endsWith("%") || cs.top.endsWith("%"))) return "percent";
@@ -73935,8 +74095,8 @@ function applyElementPositionOffset(el, gestureOffset) {
73935
74095
  const previous = { position: el.style.position, left: el.style.left, top: el.style.top };
73936
74096
  const before = layoutBox(el);
73937
74097
  if (isStatic) el.style.position = "relative";
73938
- el.style.left = `${Math.round(baseLeft) + dx}px`;
73939
- el.style.top = `${Math.round(baseTop) + dy}px`;
74098
+ el.style.left = `${roundTo3(baseLeft + dx)}px`;
74099
+ el.style.top = `${roundTo3(baseTop + dy)}px`;
73940
74100
  const after = layoutBox(el);
73941
74101
  const shiftedExactly = Math.abs(after.left - before.left - dx) <= 1 && Math.abs(after.top - before.top - dy) <= 1 && Math.abs(after.width - before.width) <= 1 && Math.abs(after.height - before.height) <= 1;
73942
74102
  if (!shiftedExactly) {
@@ -74238,8 +74398,11 @@ function useDomGeometryCommits({
74238
74398
  if (gsapFallback) return gsapFallback;
74239
74399
  const beforeSize = captureStudioBoxSize(selection.element);
74240
74400
  const beforeOffset = offset ? captureStudioPathOffset(selection.element) : null;
74401
+ const stageCrop = prepareCropResize(selection.element);
74241
74402
  applyStudioBoxSize(selection.element, next);
74403
+ const crop = stageCrop();
74242
74404
  const patches = buildBoxSizePatches(selection.element);
74405
+ if (crop) patches.push(crop.patch);
74243
74406
  if (offset) {
74244
74407
  applyStudioPathOffset(selection.element, offset);
74245
74408
  patches.push(...buildPathOffsetPatches(selection.element));
@@ -74250,6 +74413,7 @@ function useDomGeometryCommits({
74250
74413
  }).catch((error) => {
74251
74414
  restoreStudioBoxSize(selection.element, beforeSize);
74252
74415
  if (beforeOffset) restoreStudioPathOffset(selection.element, beforeOffset);
74416
+ crop?.revert();
74253
74417
  throw error;
74254
74418
  });
74255
74419
  },
@@ -74520,6 +74684,7 @@ function useDomEditCommits({
74520
74684
  handleDomAddTextField,
74521
74685
  handleDomRemoveTextField,
74522
74686
  stageElementPositionOffset,
74687
+ commitPositionPatchToHtml,
74523
74688
  handleDomPathOffsetCommit,
74524
74689
  handleDomBoxSizeCommit,
74525
74690
  handleDomRotationCommit,
@@ -76874,8 +77039,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
76874
77039
  }
76875
77040
  } else {
76876
77041
  resizeProps = {
76877
- width: Math.round(size.width),
76878
- height: Math.round(size.height)
77042
+ width: roundToLayoutPx(size.width),
77043
+ height: roundToLayoutPx(size.height)
76879
77044
  };
76880
77045
  }
76881
77046
  const finalizeScaleResizeCommit = async () => {
@@ -76901,8 +77066,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
76901
77066
  return true;
76902
77067
  }
76903
77068
  const corrected = {
76904
- x: Math.round(base2.x + residual.x),
76905
- y: Math.round(base2.y + residual.y)
77069
+ x: roundTo3(base2.x + residual.x),
77070
+ y: roundTo3(base2.y + residual.y)
76906
77071
  };
76907
77072
  logResize("scale-finalize", {
76908
77073
  dropPoint: scaleDraftDropPoint,
@@ -77291,8 +77456,8 @@ function useAnimatedPropertyCommit(deps) {
77291
77456
  const commitAnimatedProperties = useCallback137(
77292
77457
  // This is the single routing boundary for set, keyframe, whole-tween, and first-group writes.
77293
77458
  // fallow-ignore-next-line complexity
77294
- async (selection, props) => {
77295
- if (!gsapCommitMutation) return;
77459
+ async (selection, props, commit = gsapCommitMutation) => {
77460
+ if (!commit) return;
77296
77461
  const propEntries = Object.entries(props);
77297
77462
  if (propEntries.length === 0) return;
77298
77463
  const primaryProp = propEntries[0][0];
@@ -77329,7 +77494,7 @@ function useAnimatedPropertyCommit(deps) {
77329
77494
  ),
77330
77495
  pct,
77331
77496
  iframe,
77332
- { commitMutation: gsapCommitMutation },
77497
+ { commitMutation: commit },
77333
77498
  `Edit ${primaryProp} (whole animation)`
77334
77499
  );
77335
77500
  return;
@@ -77342,18 +77507,12 @@ function useAnimatedPropertyCommit(deps) {
77342
77507
  primaryProp,
77343
77508
  selector,
77344
77509
  iframe,
77345
- gsapCommitMutation
77510
+ commit
77346
77511
  );
77347
77512
  return;
77348
77513
  }
77349
77514
  if (!elementHasKeyframes) {
77350
- await commitStaticSet(
77351
- selection,
77352
- propEntries,
77353
- selector,
77354
- selectedGsapAnimations,
77355
- gsapCommitMutation
77356
- );
77515
+ await commitStaticSet(selection, propEntries, selector, selectedGsapAnimations, commit);
77357
77516
  return;
77358
77517
  }
77359
77518
  if (anim && isInstantHold(anim)) {
@@ -77363,7 +77522,7 @@ function useAnimatedPropertyCommit(deps) {
77363
77522
  propEntries,
77364
77523
  selector,
77365
77524
  selectedGsapAnimations,
77366
- gsapCommitMutation
77525
+ commit
77367
77526
  );
77368
77527
  return;
77369
77528
  }
@@ -77379,7 +77538,7 @@ function useAnimatedPropertyCommit(deps) {
77379
77538
  { percentage: 0, properties: { ...newProps, _auto: 1 } },
77380
77539
  { percentage: pct, properties: newProps }
77381
77540
  ];
77382
- await gsapCommitMutation(
77541
+ await commit(
77383
77542
  selection,
77384
77543
  {
77385
77544
  type: "add-with-keyframes",
@@ -77408,10 +77567,6 @@ function useAnimatedPropertyCommit(deps) {
77408
77567
  }
77409
77568
 
77410
77569
  // src/hooks/useGsapAwareEditing.ts
77411
- async function saveMove(outcome, saveOnElement) {
77412
- if (outcome.status === "element-offset") return saveOnElement();
77413
- assertGsapEditPersisted(outcome);
77414
- }
77415
77570
  var groupDragCommitCounter = 0;
77416
77571
  function firstPreflightFailure(results, updates) {
77417
77572
  for (const [index, result] of results.entries()) {
@@ -77432,6 +77587,7 @@ function useGsapAwareEditing({
77432
77587
  trackGsapInteractionFailure,
77433
77588
  stageElementPositionOffset,
77434
77589
  handleDomBoxSizeCommit,
77590
+ commitPositionPatchToHtml,
77435
77591
  addGsapAnimation,
77436
77592
  convertToKeyframes,
77437
77593
  setArcPath,
@@ -77609,6 +77765,8 @@ function useGsapAwareEditing({
77609
77765
  animGroups: targetAnimations.map((a) => `${a.propertyGroup}:${a.method}`)
77610
77766
  });
77611
77767
  let anchorMove = null;
77768
+ const stageCrop = prepareCropResize(selection.element);
77769
+ let cropUndoKey = null;
77612
77770
  return runGestureTransaction({
77613
77771
  element: selection.element,
77614
77772
  label: "Resize layer",
@@ -77635,6 +77793,7 @@ function useGsapAwareEditing({
77635
77793
  makeFetchFallback(selection)
77636
77794
  );
77637
77795
  assertGsapEditPersisted(outcome);
77796
+ cropUndoKey = coalesceKey;
77638
77797
  const ownsDragOffset = outcome.status === "persisted" && outcome.ownsDragOffset === true;
77639
77798
  logResize("intercept-handled", {
77640
77799
  scaleRoute,
@@ -77669,7 +77828,12 @@ function useGsapAwareEditing({
77669
77828
  logResizeSettle(selection.element, "dom-route");
77670
77829
  await handleDomBoxSizeCommit(selection, next, offset);
77671
77830
  },
77672
- afterBufferedCommitsSaved: () => anchorMove?.save() ?? Promise.resolve(),
77831
+ afterBufferedCommitsSaved: async () => {
77832
+ await anchorMove?.save();
77833
+ if (cropUndoKey) {
77834
+ await saveCropResize(stageCrop, selection, commitPositionPatchToHtml, cropUndoKey);
77835
+ }
77836
+ },
77673
77837
  restore: () => {
77674
77838
  anchorMove?.rollback();
77675
77839
  restore();
@@ -77679,6 +77843,7 @@ function useGsapAwareEditing({
77679
77843
  },
77680
77844
  [
77681
77845
  handleDomBoxSizeCommit,
77846
+ commitPositionPatchToHtml,
77682
77847
  stageElementPositionOffset,
77683
77848
  gsapCommitMutation,
77684
77849
  previewIframeRef,
@@ -77716,10 +77881,7 @@ function useGsapAwareEditing({
77716
77881
  getGsapAnimationsForSelection
77717
77882
  ]
77718
77883
  );
77719
- const {
77720
- commitAnimatedProperty: commitAnimatedPropertyRaw,
77721
- commitAnimatedProperties: commitAnimatedPropertiesRaw
77722
- } = useAnimatedPropertyCommit({
77884
+ const { commitAnimatedProperties: commitAnimatedPropertiesRaw } = useAnimatedPropertyCommit({
77723
77885
  selectedGsapAnimations,
77724
77886
  gsapCommitMutation,
77725
77887
  addGsapAnimation: (sel, method, time) => addGsapAnimation(sel, method, time),
@@ -77730,24 +77892,28 @@ function useGsapAwareEditing({
77730
77892
  const commitAnimatedProperties = useCallback138(
77731
77893
  async (selection, properties) => {
77732
77894
  try {
77733
- await commitAnimatedPropertiesRaw(selection, properties);
77895
+ await writeSizeWithCrop(
77896
+ selection,
77897
+ properties,
77898
+ gsapCommitMutation,
77899
+ commitPositionPatchToHtml,
77900
+ (keyed) => commitAnimatedPropertiesRaw(selection, properties, keyed)
77901
+ );
77734
77902
  } catch (error) {
77735
77903
  trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
77736
77904
  throw error;
77737
77905
  }
77738
77906
  },
77739
- [commitAnimatedPropertiesRaw, trackGsapInteractionFailure]
77907
+ [
77908
+ commitAnimatedPropertiesRaw,
77909
+ commitPositionPatchToHtml,
77910
+ gsapCommitMutation,
77911
+ trackGsapInteractionFailure
77912
+ ]
77740
77913
  );
77741
77914
  const commitAnimatedProperty = useCallback138(
77742
- async (selection, property, value) => {
77743
- try {
77744
- await commitAnimatedPropertyRaw(selection, property, value);
77745
- } catch (error) {
77746
- trackGsapInteractionFailure(error, selection, "property", "Edit animated property");
77747
- throw error;
77748
- }
77749
- },
77750
- [commitAnimatedPropertyRaw, trackGsapInteractionFailure]
77915
+ (selection, property, value) => commitAnimatedProperties(selection, { [property]: value }),
77916
+ [commitAnimatedProperties]
77751
77917
  );
77752
77918
  const handleSetArcPath = useCallback138(
77753
77919
  (animId, config) => {
@@ -78252,6 +78418,7 @@ function useDomEditSession({
78252
78418
  handleDomAddTextField,
78253
78419
  handleDomRemoveTextField,
78254
78420
  stageElementPositionOffset,
78421
+ commitPositionPatchToHtml,
78255
78422
  handleDomBoxSizeCommit,
78256
78423
  handleDomManualEditsReset,
78257
78424
  handleDomEditElementsDelete,
@@ -78482,6 +78649,7 @@ function useDomEditSession({
78482
78649
  trackGsapInteractionFailure,
78483
78650
  stageElementPositionOffset,
78484
78651
  handleDomBoxSizeCommit,
78652
+ commitPositionPatchToHtml,
78485
78653
  addGsapAnimation,
78486
78654
  convertToKeyframes,
78487
78655
  setArcPath,
@@ -80155,7 +80323,7 @@ function useAppHotkeys({
80155
80323
 
80156
80324
  // src/hooks/useClipboard.ts
80157
80325
  import { useCallback as useCallback147, useRef as useRef154 } from "react";
80158
- import { EXCLUDED_TAGS, mintHfId, walkCompositionDescendants } from "@hyperframes/parsers/hf-ids";
80326
+ import { EXCLUDED_TAGS, mintHfId, walkCompositionDescendants as walkCompositionDescendants2 } from "@hyperframes/parsers/hf-ids";
80159
80327
 
80160
80328
  // src/utils/clipboardPayload.ts
80161
80329
  function insertAsSibling(source, newHtml, selector, selectorIndex) {
@@ -80354,7 +80522,7 @@ function remintHfIds(html2, parser, taken) {
80354
80522
  return html2.replace(/\sdata-hf-id=("[^"]*"|'[^']*')/g, "");
80355
80523
  }
80356
80524
  const elements = [root];
80357
- walkCompositionDescendants(root, (el) => elements.push(el));
80525
+ walkCompositionDescendants2(root, (el) => elements.push(el));
80358
80526
  for (const el of elements) {
80359
80527
  if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) el.removeAttribute("data-hf-id");
80360
80528
  else el.setAttribute("data-hf-id", mintHfId(el, taken));
@@ -92455,6 +92623,7 @@ function useDomGeometryCommit({
92455
92623
  trackGsapInteractionFailure,
92456
92624
  stageElementPositionOffset,
92457
92625
  handleDomBoxSizeCommit: noDomBoxSizeRoute,
92626
+ commitPositionPatchToHtml: commitWithFreshQueue,
92458
92627
  addGsapAnimation: gsap3.addGsapAnimation,
92459
92628
  convertToKeyframes: gsap3.convertToKeyframes,
92460
92629
  setArcPath: gsap3.setArcPath,