@hyperframes/studio 0.7.103 → 0.7.105

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 (71) hide show
  1. package/dist/assets/{hyperframes-player-DUB-xtR5.js → hyperframes-player-Jx07OQgw.js} +1 -1
  2. package/dist/assets/{index-DHc-xHLb.js → index-CryZ0PGi.js} +1 -1
  3. package/dist/assets/{index-Cez_yvk5.js → index-Dqi2GmOp.js} +1 -1
  4. package/dist/assets/{index-CxC2vn2-.js → index-DuxKGsHZ.js} +214 -214
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +779 -294
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/App.tsx +6 -2
  10. package/src/captions/hooks/useCaptionSync.ts +2 -1
  11. package/src/components/editor/DomEditOverlay.test.ts +89 -2
  12. package/src/components/editor/DomEditOverlay.tsx +40 -23
  13. package/src/components/editor/domEditOverlayGeometry.test.ts +94 -0
  14. package/src/components/editor/domEditOverlayGeometry.ts +59 -2
  15. package/src/components/editor/domEditOverlayGestures.ts +42 -0
  16. package/src/components/editor/domEditOverlayStartGesture.ts +17 -0
  17. package/src/components/editor/groupDragMove.ts +110 -0
  18. package/src/components/editor/groupDropKeepsSelection.test.ts +70 -0
  19. package/src/components/editor/manualEditsDom.ts +15 -13
  20. package/src/components/editor/manualOffsetDrag.test.ts +92 -5
  21. package/src/components/editor/manualOffsetDrag.ts +22 -17
  22. package/src/components/editor/reapplyBoxSizeAfterSeek.test.ts +64 -0
  23. package/src/components/editor/snapEngageTravel.test.ts +65 -0
  24. package/src/components/editor/snapEngine.ts +21 -1
  25. package/src/components/editor/useDomEditOverlayGestures.ts +40 -53
  26. package/src/components/editor/useDomEditOverlayRects.ts +4 -2
  27. package/src/hooks/domSelectionTimelineMirror.ts +73 -0
  28. package/src/hooks/gestureTransaction.test.ts +7 -12
  29. package/src/hooks/gestureTransaction.ts +7 -20
  30. package/src/hooks/gsapRuntimePatch.test.ts +43 -0
  31. package/src/hooks/gsapRuntimePatch.ts +11 -1
  32. package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
  33. package/src/hooks/gsapScriptCommitTypes.ts +20 -0
  34. package/src/hooks/keyframeCacheAstLoad.test.ts +108 -0
  35. package/src/hooks/keyframeCacheAstLoad.ts +29 -1
  36. package/src/hooks/timelineTimingSync.ts +4 -1
  37. package/src/hooks/useDomEditCommits.ts +2 -1
  38. package/src/hooks/useDomEditCommitsHelpers.ts +2 -1
  39. package/src/hooks/useDomEditPreviewSync.ts +23 -0
  40. package/src/hooks/useDomEditSession.test.tsx +43 -62
  41. package/src/hooks/useDomEditSession.ts +10 -2
  42. package/src/hooks/useDomEditWiring.ts +6 -0
  43. package/src/hooks/useDomSelection.test.ts +133 -3
  44. package/src/hooks/useDomSelection.ts +67 -40
  45. package/src/hooks/useDomSelectionSelectionGuards.test.ts +6 -2
  46. package/src/hooks/useElementLifecycleOps.ts +2 -1
  47. package/src/hooks/useExternalFileChangeCoordinator.ts +16 -3
  48. package/src/hooks/useFileManager.ts +2 -8
  49. package/src/hooks/useGroupCommits.ts +2 -1
  50. package/src/hooks/useGsapAnimationFetchFallback.ts +6 -1
  51. package/src/hooks/useGsapAwareEditing.test.tsx +36 -0
  52. package/src/hooks/useGsapAwareEditing.ts +88 -15
  53. package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -5
  54. package/src/hooks/useGsapScriptCommits.test.tsx +193 -7
  55. package/src/hooks/useGsapScriptCommits.ts +52 -15
  56. package/src/hooks/useStudioUrlState.ts +130 -7
  57. package/src/hooks/useTimelineEditing.ts +2 -1
  58. package/src/hooks/useTimelineSelectionPreviewSync.ts +13 -0
  59. package/src/player/hooks/useTimelinePlayer.ts +2 -0
  60. package/src/utils/dragDebug.ts +98 -0
  61. package/src/utils/razorSplitTransaction.test.ts +15 -3
  62. package/src/utils/razorSplitTransaction.ts +2 -0
  63. package/src/utils/reloadDebug.ts +10 -0
  64. package/src/utils/resizeDebug.ts +19 -29
  65. package/src/utils/selectDebug.ts +9 -0
  66. package/src/utils/studioDebug.test.ts +32 -0
  67. package/src/utils/studioDebug.ts +30 -0
  68. package/src/utils/studioFileVersion.ts +16 -1
  69. package/src/utils/studioUrlState.test.ts +163 -1
  70. package/src/utils/studioUrlState.ts +60 -3
  71. package/src/utils/timelineCompositionInsert.ts +2 -1
package/dist/index.js CHANGED
@@ -1134,7 +1134,7 @@ var usePlayerStore = create2((set, get) => ({
1134
1134
  motionPathCreateAvailable: false,
1135
1135
  setMotionPathCreateAvailable: (available) => set({ motionPathCreateAvailable: available }),
1136
1136
  autoKeyframeEnabled: true,
1137
- setAutoKeyframeEnabled: (enabled2) => set({ autoKeyframeEnabled: enabled2 }),
1137
+ setAutoKeyframeEnabled: (enabled) => set({ autoKeyframeEnabled: enabled }),
1138
1138
  selectedElementIds: /* @__PURE__ */ new Set(),
1139
1139
  setSelection: (ids, anchor) => set(resolveElementSelection(ids, anchor)),
1140
1140
  addSelectedElementId: (id) => set((s) => {
@@ -1228,14 +1228,14 @@ var usePlayerStore = create2((set, get) => ({
1228
1228
  writeStudioUiPreferences({ audioMuted: muted });
1229
1229
  set({ audioMuted: muted });
1230
1230
  },
1231
- setLoopEnabled: (enabled2) => set({ loopEnabled: enabled2 }),
1231
+ setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
1232
1232
  setZoomMode: (mode) => set({ zoomMode: mode }),
1233
1233
  clearSelectedElementIds: () => set({ selectedElementIds: /* @__PURE__ */ new Set() }),
1234
1234
  setSelectedElementIds: (ids) => set({ selectedElementIds: new Set(ids) }),
1235
1235
  timelineSnapEnabled: readStudioUiPreferences().timelineSnapEnabled ?? true,
1236
- setTimelineSnapEnabled: (enabled2) => {
1237
- writeStudioUiPreferences({ timelineSnapEnabled: enabled2 });
1238
- set({ timelineSnapEnabled: enabled2 });
1236
+ setTimelineSnapEnabled: (enabled) => {
1237
+ writeStudioUiPreferences({ timelineSnapEnabled: enabled });
1238
+ set({ timelineSnapEnabled: enabled });
1239
1239
  },
1240
1240
  timeDisplayMode: readStudioUiPreferences().timeDisplayMode ?? "time",
1241
1241
  setTimeDisplayMode: (mode) => {
@@ -1552,13 +1552,13 @@ function isAutomatedBrowser() {
1552
1552
  return navigator.webdriver === true;
1553
1553
  }
1554
1554
  var decisions = /* @__PURE__ */ new Map();
1555
- var forcedOutcome = (enabled2) => ({
1556
- enabled: enabled2,
1557
- reason: enabled2 ? "forced_on" : "forced_off"
1555
+ var forcedOutcome = (enabled) => ({
1556
+ enabled,
1557
+ reason: enabled ? "forced_on" : "forced_off"
1558
1558
  });
1559
- var cohortOutcome = (enabled2) => ({
1560
- enabled: enabled2,
1561
- reason: enabled2 ? "in_cohort" : "out_of_cohort"
1559
+ var cohortOutcome = (enabled) => ({
1560
+ enabled,
1561
+ reason: enabled ? "in_cohort" : "out_of_cohort"
1562
1562
  });
1563
1563
  function decideStudioCanary(name) {
1564
1564
  const definition = findCanary(name);
@@ -1589,8 +1589,8 @@ function resolveCanary(name) {
1589
1589
  function canaryEventProperties() {
1590
1590
  return canaryFeatureProperties(
1591
1591
  CANARIES.map((c) => {
1592
- const { enabled: enabled2, reason } = resolveCanary(c.name);
1593
- return { name: c.name, enabled: enabled2, reason };
1592
+ const { enabled, reason } = resolveCanary(c.name);
1593
+ return { name: c.name, enabled, reason };
1594
1594
  })
1595
1595
  );
1596
1596
  }
@@ -11932,6 +11932,45 @@ function patchDocumentRootDuration(doc, contentEnd) {
11932
11932
  return true;
11933
11933
  }
11934
11934
 
11935
+ // src/utils/studioFileVersion.ts
11936
+ var WRITE_TOKEN_TTL_MS = 5 * 6e4;
11937
+ var studioWriteTokens = /* @__PURE__ */ new Map();
11938
+ function pruneStudioWriteTokens(now2) {
11939
+ for (const [token, createdAt] of studioWriteTokens) {
11940
+ if (now2 - createdAt >= WRITE_TOKEN_TTL_MS) studioWriteTokens.delete(token);
11941
+ }
11942
+ }
11943
+ function markStudioWriteToken(token, now2 = Date.now()) {
11944
+ pruneStudioWriteTokens(now2);
11945
+ studioWriteTokens.set(token, now2);
11946
+ }
11947
+ function consumeStudioWriteToken(token, now2 = Date.now()) {
11948
+ pruneStudioWriteTokens(now2);
11949
+ if (!token || !studioWriteTokens.has(token)) return false;
11950
+ studioWriteTokens.delete(token);
11951
+ return true;
11952
+ }
11953
+ async function studioFileContentVersion(content) {
11954
+ const bytes = new TextEncoder().encode(content);
11955
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
11956
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
11957
+ ""
11958
+ );
11959
+ return `"sha256:${hex}"`;
11960
+ }
11961
+ async function studioExpectedFileVersion(versions, path, expectedContent) {
11962
+ if (expectedContent !== void 0) return studioFileContentVersion(expectedContent);
11963
+ return versions.get(path);
11964
+ }
11965
+ function createStudioWriteToken() {
11966
+ return globalThis.crypto.randomUUID();
11967
+ }
11968
+ function studioWriteHeaders() {
11969
+ const token = createStudioWriteToken();
11970
+ markStudioWriteToken(token);
11971
+ return { "X-Hyperframes-Write-Token": token };
11972
+ }
11973
+
11935
11974
  // src/hooks/timelineTimingSync.ts
11936
11975
  var GsapPreviewConvergenceError = class extends Error {
11937
11976
  };
@@ -11973,6 +12012,8 @@ async function rollbackOwnedMutation(projectId, targetPath, expected, restore) {
11973
12012
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutation-rollback/${encodeURIComponent(targetPath)}`,
11974
12013
  {
11975
12014
  method: "POST",
12015
+ // Deliberately unclaimed: a rollback runs because a mutation did not
12016
+ // converge, so let the restored file reload the preview.
11976
12017
  headers: { "Content-Type": "application/json" },
11977
12018
  body: JSON.stringify({ expected, restore })
11978
12019
  }
@@ -12031,7 +12072,7 @@ async function postGsapMutation(projectId, filePath, mutation, fallback) {
12031
12072
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(filePath)}`,
12032
12073
  {
12033
12074
  method: "POST",
12034
- headers: { "Content-Type": "application/json" },
12075
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
12035
12076
  body: JSON.stringify(mutation)
12036
12077
  }
12037
12078
  );
@@ -15791,7 +15832,7 @@ var TrackGapContextMenu = memo13(function TrackGapContextMenu2({
15791
15832
  const overflowY = y + menuHeight - window.innerHeight;
15792
15833
  const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x;
15793
15834
  const adjustedY = overflowY > 0 ? y - overflowY - 8 : y;
15794
- const itemClass = (enabled2) => `w-full flex items-center justify-between px-3 py-1.5 text-xs text-left ${enabled2 ? "text-neutral-300 hover:bg-neutral-800 cursor-pointer" : "text-neutral-600 cursor-not-allowed"}`;
15835
+ const itemClass = (enabled) => `w-full flex items-center justify-between px-3 py-1.5 text-xs text-left ${enabled ? "text-neutral-300 hover:bg-neutral-800 cursor-pointer" : "text-neutral-600 cursor-not-allowed"}`;
15795
15836
  const closeGapTitle = canCloseGap ? void 0 : gapWidth == null ? "No gap here" : "A clip on this track can't be moved";
15796
15837
  const closeAllTitle = canCloseAllGaps ? void 0 : hasAnyGaps ? "A clip on this track can't be moved" : "No gaps on this track";
15797
15838
  return createPortal4(
@@ -17911,7 +17952,7 @@ function extractTimelineVirtualRowSeed(count, pinnedRowIndexes) {
17911
17952
  return [...indexes].sort((left, right) => left - right);
17912
17953
  }
17913
17954
  function useTimelineVirtualRows({
17914
- enabled: enabled2,
17955
+ enabled,
17915
17956
  scrollRef,
17916
17957
  viewport,
17917
17958
  rowGeometry,
@@ -17944,7 +17985,7 @@ function useTimelineVirtualRows({
17944
17985
  );
17945
17986
  const initialOffset = useCallback19(() => viewport.scrollTop, [viewport.scrollTop]);
17946
17987
  const virtualizer = useVirtualizer({
17947
- enabled: enabled2 && viewportReady,
17988
+ enabled: enabled && viewportReady,
17948
17989
  useFlushSync: false,
17949
17990
  count: rowGeometry.rowKeys.length,
17950
17991
  getScrollElement: () => scrollRef.current,
@@ -17957,20 +17998,20 @@ function useTimelineVirtualRows({
17957
17998
  initialOffset
17958
17999
  });
17959
18000
  useEffect22(() => {
17960
- if (!enabled2 || !viewportReady) return;
18001
+ if (!enabled || !viewportReady) return;
17961
18002
  virtualizer.measure();
17962
18003
  scrollRef.current?.dispatchEvent(new Event("scroll"));
17963
- }, [enabled2, scrollRef, sessionEpoch, viewportReady, virtualizer]);
18004
+ }, [enabled, scrollRef, sessionEpoch, viewportReady, virtualizer]);
17964
18005
  const focusedRowIndex = focusedRowKey === void 0 ? -1 : rowGeometry.getRowIndex(focusedRowKey);
17965
18006
  const focusedRowHeight = focusedRowIndex < 0 ? 0 : rowGeometry.getRowHeight(focusedRowIndex);
17966
18007
  useEffect22(() => {
17967
- if (enabled2 && viewportReady && focusedRowIndex >= 0) {
18008
+ if (enabled && viewportReady && focusedRowIndex >= 0) {
17968
18009
  virtualizer.resizeItem(focusedRowIndex, focusedRowHeight);
17969
18010
  }
17970
- }, [enabled2, focusedRowHeight, focusedRowIndex, sessionEpoch, viewportReady, virtualizer]);
18011
+ }, [enabled, focusedRowHeight, focusedRowIndex, sessionEpoch, viewportReady, virtualizer]);
17971
18012
  const compatibilityRows = useMemo23(
17972
- () => enabled2 ? null : rowGeometry.rowKeys.map((rowKey, index) => ({ index, rowKey })),
17973
- [enabled2, rowGeometry]
18013
+ () => enabled ? null : rowGeometry.rowKeys.map((rowKey, index) => ({ index, rowKey })),
18014
+ [enabled, rowGeometry]
17974
18015
  );
17975
18016
  if (compatibilityRows) return compatibilityRows;
17976
18017
  if (!viewportReady) {
@@ -18010,7 +18051,7 @@ function useTimelineRowVirtualization({
18010
18051
  lastScrollLeftRef,
18011
18052
  syncScrollViewport
18012
18053
  }) {
18013
- const enabled2 = STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED;
18054
+ const enabled = STUDIO_TIMELINE_ROW_VIRTUALIZATION_ENABLED;
18014
18055
  const [domFocusedRowKey, setDomFocusedRowKey] = useState16();
18015
18056
  const onTimelineFocus = useCallback20((event) => {
18016
18057
  setDomFocusedRowKey(getFocusedTimelineRowKey(event.target));
@@ -18045,7 +18086,7 @@ function useTimelineRowVirtualization({
18045
18086
  ]
18046
18087
  );
18047
18088
  const virtualRows = useTimelineVirtualRows({
18048
- enabled: enabled2,
18089
+ enabled,
18049
18090
  scrollRef,
18050
18091
  viewport,
18051
18092
  rowGeometry,
@@ -18080,7 +18121,7 @@ function useTimelineRowVirtualization({
18080
18121
  previousLayoutRef.current = rowGeometry;
18081
18122
  }, [lastScrollLeftRef, rowGeometry, scrollRef, sessionEpoch, syncScrollViewport]);
18082
18123
  return {
18083
- enabled: enabled2,
18124
+ enabled,
18084
18125
  virtualRows,
18085
18126
  timelineFocusProps: { onFocus: onTimelineFocus, onBlur: onTimelineBlur }
18086
18127
  };
@@ -20219,6 +20260,29 @@ function useTimelinePlayerLoop({
20219
20260
  return { startRAFLoop, stopRAFLoop, stopReverseLoop };
20220
20261
  }
20221
20262
 
20263
+ // src/utils/studioDebug.ts
20264
+ function makeStudioDebugLogger(name) {
20265
+ let enabled = null;
20266
+ return (stage, data = {}) => {
20267
+ if (enabled === null) {
20268
+ try {
20269
+ enabled = localStorage.getItem(`hf-${name}-debug`) === "1";
20270
+ } catch {
20271
+ enabled = false;
20272
+ }
20273
+ }
20274
+ if (!enabled) return;
20275
+ const details = typeof data === "function" ? data() : data;
20276
+ if (!details) return;
20277
+ console.log(
20278
+ `[hf-${name}] ${JSON.stringify({ stage, t: Math.round(performance.now()), ...details })}`
20279
+ );
20280
+ };
20281
+ }
20282
+
20283
+ // src/utils/reloadDebug.ts
20284
+ var logReload = makeStudioDebugLogger("reload");
20285
+
20222
20286
  // src/player/lib/playbackScrub.ts
20223
20287
  function scrubMusicAtSeek(iframe, nextTime) {
20224
20288
  const s = usePlayerStore.getState();
@@ -20842,6 +20906,7 @@ function useTimelinePlayer() {
20842
20906
  const refreshPlayer = useCallback28(() => {
20843
20907
  const iframe = iframeRef.current;
20844
20908
  if (!iframe) return;
20909
+ logReload("refreshPlayer", () => ({ stack: new Error("refreshPlayer").stack }));
20845
20910
  saveSeekPosition();
20846
20911
  iframe.style.visibility = "hidden";
20847
20912
  const src = iframe.src;
@@ -23763,7 +23828,14 @@ function readElementTransformSnapshot(win, element) {
23763
23828
  if (!DOMMatrixCtor) return null;
23764
23829
  const cs = win.getComputedStyle(element);
23765
23830
  try {
23766
- const matrix = new DOMMatrixCtor(cs.transform === "none" ? "" : cs.transform);
23831
+ let matrix = new DOMMatrixCtor();
23832
+ for (let node = element; node; node = node.parentElement) {
23833
+ const transform = node === element ? cs.transform : win.getComputedStyle(node).transform;
23834
+ if (transform && transform !== "none") {
23835
+ matrix = new DOMMatrixCtor(transform).multiply(matrix);
23836
+ }
23837
+ if (node.hasAttribute("data-composition-id")) break;
23838
+ }
23767
23839
  return { matrix, cs };
23768
23840
  } catch {
23769
23841
  return null;
@@ -23772,7 +23844,12 @@ function readElementTransformSnapshot(win, element) {
23772
23844
  function rotationDegreesFromMatrix(matrix) {
23773
23845
  const a = Number.isFinite(matrix.a) ? matrix.a : 1;
23774
23846
  const b = Number.isFinite(matrix.b) ? matrix.b : 0;
23775
- const deg = Math.atan2(b, a) * 180 / Math.PI;
23847
+ const c = Number.isFinite(matrix.c) ? matrix.c : 0;
23848
+ const d = Number.isFinite(matrix.d) ? matrix.d : 1;
23849
+ const fromX = Math.atan2(b, a) * 180 / Math.PI;
23850
+ const determinant = a * d - b * c;
23851
+ const fromY = Math.atan2(-c, d) * 180 / Math.PI;
23852
+ const deg = determinant < 0 && Math.abs(fromY) < Math.abs(fromX) ? fromY : fromX;
23776
23853
  return Number.isFinite(deg) ? deg : 0;
23777
23854
  }
23778
23855
  var ROTATION_GATE_EPSILON_DEG = 1e-4;
@@ -23903,6 +23980,10 @@ function orientedOverlayRect(overlayEl, iframe, element) {
23903
23980
  angle
23904
23981
  };
23905
23982
  }
23983
+ function orientedVisibleOverlayRect(overlayEl, iframe, element) {
23984
+ const rect = orientedOverlayRect(overlayEl, iframe, element);
23985
+ return rect ? { ...rect, ...hugRectForElement(rect, element) } : null;
23986
+ }
23906
23987
  var OVERLAY_RECT_EPSILON_PX = 0.5;
23907
23988
  var OVERLAY_RECT_ANGLE_EPSILON_DEG = 0.1;
23908
23989
  function rectsEqual(a, b) {
@@ -24234,6 +24315,9 @@ function useCanvasContextMenuState({
24234
24315
  return { contextMenu, closeContextMenu, handleContextMenu };
24235
24316
  }
24236
24317
 
24318
+ // src/utils/selectDebug.ts
24319
+ var logSelect = makeStudioDebugLogger("select");
24320
+
24237
24321
  // src/components/editor/domEditOverlayGestures.ts
24238
24322
  var BLOCKED_MOVE_THRESHOLD_PX = 4;
24239
24323
  var ROTATION_COMMIT_EPSILON_DEGREES = 0.05;
@@ -24242,6 +24326,19 @@ var ROTATED_SNAP_BYPASS_DEGREES = 0.5;
24242
24326
  function focusDomEditOverlayElement(element) {
24243
24327
  element?.focus({ preventScroll: true });
24244
24328
  }
24329
+ function hoverCacheDescribesPoint(cachedElement, elementAtPoint) {
24330
+ if (!cachedElement || !elementAtPoint) return false;
24331
+ return cachedElement === elementAtPoint || cachedElement.contains(elementAtPoint);
24332
+ }
24333
+ function resolveShiftClickCandidate(input) {
24334
+ const describes = hoverCacheDescribesPoint(input.cached?.element, input.elementAtPoint);
24335
+ logSelect("shift-pointerdown", {
24336
+ candidate: input.cached ? input.cached.selector ?? null : null,
24337
+ pointTarget: input.elementAtPoint?.id ?? input.elementAtPoint?.tagName ?? null,
24338
+ cacheIsAboutThisPoint: describes
24339
+ });
24340
+ return describes ? input.cached : null;
24341
+ }
24245
24342
  function resolveResizeCenterAnchorOffset(input) {
24246
24343
  return {
24247
24344
  dx: (input.originWidth - input.overlayWidth) / 2,
@@ -24381,7 +24478,7 @@ function useDomEditOverlayRects({
24381
24478
  for (let i = 0; i < descendants.length; i++) {
24382
24479
  const child = descendants[i];
24383
24480
  if (!child.getBoundingClientRect) continue;
24384
- const r = toVisibleOverlayRect(overlayEl, iframe, child);
24481
+ const r = orientedVisibleOverlayRect(overlayEl, iframe, child);
24385
24482
  if (r && r.width > 2 && r.height > 2) nextChildRects.push(r);
24386
24483
  }
24387
24484
  if (!childRectsEqual(childRectsRef.current, nextChildRects)) {
@@ -25071,25 +25168,16 @@ function queryStudioElements(doc, attr) {
25071
25168
  }
25072
25169
  function reapplyPathOffsets(doc) {
25073
25170
  for (const el of queryStudioElements(doc, STUDIO_PATH_OFFSET_ATTR)) {
25074
- const gsapSkip = gsapAnimatesProperty(el, "x", "y");
25171
+ if (gsapAnimatesProperty(el, "x", "y")) continue;
25075
25172
  const x = el.style.getPropertyValue(STUDIO_OFFSET_X_PROP);
25076
25173
  const y = el.style.getPropertyValue(STUDIO_OFFSET_Y_PROP);
25077
- if (gsapSkip) continue;
25078
- if (x || y) {
25079
- applyStudioPathOffset(
25080
- el,
25081
- {
25082
- x: Number.parseFloat(x) || 0,
25083
- y: Number.parseFloat(y) || 0
25084
- },
25085
- { updateBase: false }
25086
- );
25087
- }
25174
+ if (!x && !y) continue;
25175
+ const offset = { x: Number.parseFloat(x) || 0, y: Number.parseFloat(y) || 0 };
25176
+ applyStudioPathOffset(el, offset, { updateBase: false });
25088
25177
  }
25089
25178
  }
25090
25179
  function reapplyBoxSizes(doc) {
25091
25180
  for (const el of queryStudioElements(doc, STUDIO_BOX_SIZE_ATTR)) {
25092
- if (gsapAnimatesProperty(el, "width", "height")) continue;
25093
25181
  const w = Number.parseFloat(el.style.getPropertyValue(STUDIO_WIDTH_PROP));
25094
25182
  const h = Number.parseFloat(el.style.getPropertyValue(STUDIO_HEIGHT_PROP));
25095
25183
  if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
@@ -25658,12 +25746,6 @@ function readTransformWDivisor(element) {
25658
25746
  return Number.isFinite(w) && w > 0 ? w : 1;
25659
25747
  }
25660
25748
  function measureManualOffsetDragScreenToOffsetMatrix(element, initialOffset, options = {}) {
25661
- if (!element.hasAttribute("data-hf-studio-path-offset") && initialOffset.x === 0 && initialOffset.y === 0) {
25662
- const sx = options.scaleX || 1;
25663
- const sy = options.scaleY || 1;
25664
- const w = readTransformWDivisor(element);
25665
- return { ok: true, matrix: { a: w / sx, b: 0, c: 0, d: w / sy } };
25666
- }
25667
25749
  const probeSize = options.probeSize ?? DEFAULT_OFFSET_PROBE_PX;
25668
25750
  if (!Number.isFinite(probeSize) || probeSize <= 0) {
25669
25751
  return { ok: false, reason: "Invalid movement probe size." };
@@ -26011,6 +26093,10 @@ function readElementGsapNumber(element, prop) {
26011
26093
 
26012
26094
  // src/components/editor/snapEngine.ts
26013
26095
  var SNAP_THRESHOLD_PX = 6;
26096
+ var SNAP_ENGAGE_TRAVEL_PX = 4;
26097
+ function snapEngagedForTravel(dx, dy) {
26098
+ return Math.hypot(dx, dy) >= SNAP_ENGAGE_TRAVEL_PX;
26099
+ }
26014
26100
  var EQUIDISTANCE_TOLERANCE_PX = 1;
26015
26101
  function rectRight(r) {
26016
26102
  return r.left + r.width;
@@ -26173,7 +26259,7 @@ var DISABLED_RESULT = (dx, dy) => ({
26173
26259
  spacingGuides: []
26174
26260
  });
26175
26261
  function resolveSnapAdjustment(input) {
26176
- if (input.disabled || input.threshold <= 0) {
26262
+ if (input.disabled || input.disabledForTravel || input.threshold <= 0) {
26177
26263
  return DISABLED_RESULT(input.proposedDx, input.proposedDy);
26178
26264
  }
26179
26265
  const mr = input.movingRect;
@@ -26383,38 +26469,22 @@ function buildExcludeElements(input) {
26383
26469
  }
26384
26470
 
26385
26471
  // src/utils/resizeDebug.ts
26472
+ var logResize = makeStudioDebugLogger("resize");
26386
26473
  var moveN = 0;
26387
- var enabled = null;
26388
- function isEnabled2() {
26389
- if (enabled === null) {
26390
- try {
26391
- enabled = localStorage.getItem("hf-resize-debug") === "1";
26392
- } catch {
26393
- enabled = false;
26394
- }
26395
- }
26396
- return enabled;
26397
- }
26398
- function logResize(stage, data) {
26399
- if (!isEnabled2()) return;
26400
- console.log(
26401
- `[hf-resize] ${JSON.stringify({ stage, t: Math.round(performance.now()), ...data })}`
26402
- );
26403
- }
26404
26474
  function logResizeMove(data) {
26405
- if (!isEnabled2()) return;
26406
- moveN += 1;
26407
- if (moveN % 8 === 1) logResize("move", { n: moveN, ...data });
26475
+ logResize("move", () => {
26476
+ moveN += 1;
26477
+ return moveN % 8 === 1 ? { n: moveN, ...data } : null;
26478
+ });
26408
26479
  }
26409
26480
  function resetResizeMoveLog() {
26410
26481
  moveN = 0;
26411
26482
  }
26412
26483
  function logResizeSettle(el, tag) {
26413
- if (!isEnabled2()) return;
26414
- const snap = (phase) => {
26484
+ const snapshot = (phase) => {
26415
26485
  const r = el.getBoundingClientRect();
26416
26486
  const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
26417
- logResize("settle", {
26487
+ return {
26418
26488
  tag,
26419
26489
  phase,
26420
26490
  rect: { x: r.x, y: r.y, w: r.width, h: r.height },
@@ -26422,10 +26492,75 @@ function logResizeSettle(el, tag) {
26422
26492
  cssH: cs?.height,
26423
26493
  transform: cs?.transform,
26424
26494
  inlineStyle: el.getAttribute("style")
26425
- });
26495
+ };
26426
26496
  };
26427
- snap("t0");
26428
- setTimeout(() => snap("t200"), 200);
26497
+ logResize("settle", () => {
26498
+ const current = snapshot("t0");
26499
+ el.ownerDocument.defaultView?.setTimeout(
26500
+ () => logResize("settle", () => snapshot("t200")),
26501
+ 200
26502
+ );
26503
+ return current;
26504
+ });
26505
+ }
26506
+
26507
+ // src/utils/dragDebug.ts
26508
+ var logDrag = makeStudioDebugLogger("drag");
26509
+ var moveN2 = 0;
26510
+ function logDragMove(data) {
26511
+ logDrag("move", () => {
26512
+ moveN2 += 1;
26513
+ return moveN2 % 8 === 1 ? { n: moveN2, ...data } : null;
26514
+ });
26515
+ }
26516
+ function resetDragMoveLog() {
26517
+ moveN2 = 0;
26518
+ }
26519
+ function readDragPositions(elements) {
26520
+ const positions = {};
26521
+ for (const { key: key2, element } of elements) {
26522
+ const rect = element.getBoundingClientRect();
26523
+ positions[key2] = `${Math.round(rect.left)},${Math.round(rect.top)}`;
26524
+ }
26525
+ return positions;
26526
+ }
26527
+ function findNonRigidMembers(before, after) {
26528
+ const moves = /* @__PURE__ */ new Map();
26529
+ for (const key2 of Object.keys(after)) {
26530
+ const from = before[key2]?.split(",").map(Number);
26531
+ const to = after[key2]?.split(",").map(Number);
26532
+ if (!from || !to || from.length !== 2 || to.length !== 2) continue;
26533
+ moves.set(key2, `${Math.round(to[0] - from[0])},${Math.round(to[1] - from[1])}`);
26534
+ }
26535
+ const counts = /* @__PURE__ */ new Map();
26536
+ for (const move of moves.values()) counts.set(move, (counts.get(move) ?? 0) + 1);
26537
+ let common = "";
26538
+ let best = 0;
26539
+ for (const [move, count] of counts) {
26540
+ if (count > best) [common, best] = [move, count];
26541
+ }
26542
+ return [...moves].filter(([, move]) => move !== common).map(([key2, move]) => `${key2.split("|")[2] ?? key2} moved ${move}, group moved ${common}`);
26543
+ }
26544
+ function logDragSettle(stage, elements) {
26545
+ logDrag(stage, () => {
26546
+ const at = readDragPositions(elements);
26547
+ const win = elements[0]?.element.ownerDocument.defaultView;
26548
+ if (win) {
26549
+ win.setTimeout(
26550
+ () => logDrag(`${stage}+120ms`, () => ({ at: readDragPositions(elements) })),
26551
+ 120
26552
+ );
26553
+ win.setTimeout(
26554
+ () => logDrag(`${stage}+400ms`, () => ({ at: readDragPositions(elements) })),
26555
+ 400
26556
+ );
26557
+ win.setTimeout(
26558
+ () => logDrag(`${stage}+900ms`, () => ({ at: readDragPositions(elements) })),
26559
+ 900
26560
+ );
26561
+ }
26562
+ return { at };
26563
+ });
26429
26564
  }
26430
26565
 
26431
26566
  // src/components/editor/domEditOverlayStartGesture.ts
@@ -26460,6 +26595,22 @@ function startGroupDrag(e, opts) {
26460
26595
  }
26461
26596
  members.push(result.member);
26462
26597
  }
26598
+ resetDragMoveLog();
26599
+ logDrag("group-start", {
26600
+ // A member whose mapping differs from its neighbours travels a different
26601
+ // distance for the same pointer delta, which is the group coming apart.
26602
+ members: Object.fromEntries(
26603
+ members.map((member) => [
26604
+ member.key,
26605
+ {
26606
+ map: `${member.screenToOffset.a.toFixed(3)},${member.screenToOffset.d.toFixed(3)}`,
26607
+ base: `${Math.round(member.baseGsap.x)},${Math.round(member.baseGsap.y)}`,
26608
+ offset: `${Math.round(member.initialOffset.x)},${Math.round(member.initialOffset.y)}`
26609
+ }
26610
+ ])
26611
+ ),
26612
+ at: readDragPositions(members)
26613
+ });
26463
26614
  const overlayEl = opts.overlayRef.current;
26464
26615
  const iframe = opts.iframeRef.current;
26465
26616
  const snapContext = overlayEl && iframe ? collectSnapContext({
@@ -26624,6 +26775,76 @@ function startGesture(kind, e, opts, options) {
26624
26775
  return true;
26625
26776
  }
26626
26777
 
26778
+ // src/components/editor/groupDragMove.ts
26779
+ function createGroupDragMover(opts, setDraftGroupOverlayItems) {
26780
+ let lastGroupPositions = {};
26781
+ let lastGesture = null;
26782
+ const snapGroupDelta = (groupG, e, proposed) => {
26783
+ const sc = groupG.snapContext;
26784
+ if (!sc?.snapEnabled || sc.targets.length === 0) return proposed;
26785
+ if (!snapEngagedForTravel(proposed.dx, proposed.dy)) {
26786
+ opts.snapGuidesRef.current = null;
26787
+ return proposed;
26788
+ }
26789
+ const groupBounds = resolveDomEditGroupOverlayRect(groupG.originItems.map((i) => i.rect));
26790
+ if (!groupBounds) return proposed;
26791
+ const allTargets = sc.compositionTarget ? [...sc.targets, sc.compositionTarget] : sc.targets;
26792
+ const snap = resolveSnapAdjustment({
26793
+ movingRect: groupBounds,
26794
+ proposedDx: proposed.dx,
26795
+ proposedDy: proposed.dy,
26796
+ targets: allTargets,
26797
+ gridEdges: sc.gridEdges ?? void 0,
26798
+ threshold: SNAP_THRESHOLD_PX,
26799
+ disabled: e.altKey
26800
+ });
26801
+ const movingRect = {
26802
+ ...groupBounds,
26803
+ left: groupBounds.left + snap.dx,
26804
+ top: groupBounds.top + snap.dy
26805
+ };
26806
+ const spacingGuides = e.altKey ? [] : resolveEquidistanceGuides({
26807
+ movingRect,
26808
+ targets: allTargets,
26809
+ threshold: SNAP_THRESHOLD_PX
26810
+ });
26811
+ opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
26812
+ return { dx: snap.dx, dy: snap.dy };
26813
+ };
26814
+ const moveGroupDrag = (groupG, e) => {
26815
+ if (groupG !== lastGesture) {
26816
+ lastGesture = groupG;
26817
+ lastGroupPositions = {};
26818
+ }
26819
+ const { dx, dy } = snapGroupDelta(groupG, e, {
26820
+ dx: e.clientX - groupG.startX,
26821
+ dy: e.clientY - groupG.startY
26822
+ });
26823
+ groupG.lastSnappedDx = dx;
26824
+ groupG.lastSnappedDy = dy;
26825
+ setDraftGroupOverlayItems(
26826
+ groupG.originItems.map((i) => ({
26827
+ ...i,
26828
+ rect: { ...i.rect, left: i.rect.left + dx, top: i.rect.top + dy }
26829
+ }))
26830
+ );
26831
+ const offsets = {};
26832
+ for (const m of groupG.members) {
26833
+ const n = applyManualOffsetDragDraft(m, dx, dy);
26834
+ offsets[m.key] = `${Math.round(n.x)},${Math.round(n.y)}`;
26835
+ }
26836
+ const at = readDragPositions(groupG.members);
26837
+ const px = Math.round(e.clientX - groupG.startX);
26838
+ const py = Math.round(e.clientY - groupG.startY);
26839
+ const trace = { pointer: `${px},${py}`, applied: `${Math.round(dx)},${Math.round(dy)}`, at };
26840
+ const drift = findNonRigidMembers(lastGroupPositions, at);
26841
+ if (drift.length > 0) logDrag("drift", { ...trace, drift });
26842
+ lastGroupPositions = at;
26843
+ logDragMove({ ...trace, offsets });
26844
+ };
26845
+ return moveGroupDrag;
26846
+ }
26847
+
26627
26848
  // src/components/editor/useDomEditOverlayGestures.ts
26628
26849
  function createDomEditOverlayGestureHandlers(opts) {
26629
26850
  const setDraftOverlayRect = (next) => {
@@ -26652,6 +26873,7 @@ function createDomEditOverlayGestureHandlers(opts) {
26652
26873
  };
26653
26874
  const startGroupDrag2 = (e) => startGroupDrag(e, opts);
26654
26875
  const startGesture2 = (kind, e, options) => startGesture(kind, e, opts, options);
26876
+ const moveGroupDrag = createGroupDragMover(opts, setDraftGroupOverlayItems);
26655
26877
  const onPointerMove = (e) => {
26656
26878
  const g = opts.gestureRef.current;
26657
26879
  const groupG = opts.groupGestureRef.current;
@@ -26672,49 +26894,7 @@ function createDomEditOverlayGestureHandlers(opts) {
26672
26894
  return;
26673
26895
  }
26674
26896
  if (groupG) {
26675
- let dx2 = e.clientX - groupG.startX;
26676
- let dy2 = e.clientY - groupG.startY;
26677
- const sc = groupG.snapContext;
26678
- if (sc?.snapEnabled && sc.targets.length > 0) {
26679
- const groupBounds = resolveDomEditGroupOverlayRect(
26680
- groupG.originItems.map((item) => item.rect)
26681
- );
26682
- if (groupBounds) {
26683
- const allTargets = sc.compositionTarget ? [...sc.targets, sc.compositionTarget] : sc.targets;
26684
- const snap = resolveSnapAdjustment({
26685
- movingRect: groupBounds,
26686
- proposedDx: dx2,
26687
- proposedDy: dy2,
26688
- targets: allTargets,
26689
- gridEdges: sc.gridEdges ?? void 0,
26690
- threshold: SNAP_THRESHOLD_PX,
26691
- disabled: e.altKey
26692
- });
26693
- dx2 = snap.dx;
26694
- dy2 = snap.dy;
26695
- const movedRect = {
26696
- left: groupBounds.left + dx2,
26697
- top: groupBounds.top + dy2,
26698
- width: groupBounds.width,
26699
- height: groupBounds.height
26700
- };
26701
- const spacingGuides = e.altKey ? [] : resolveEquidistanceGuides({
26702
- movingRect: movedRect,
26703
- targets: allTargets,
26704
- threshold: SNAP_THRESHOLD_PX
26705
- });
26706
- opts.snapGuidesRef.current = { guides: snap.guides, spacingGuides };
26707
- }
26708
- }
26709
- groupG.lastSnappedDx = dx2;
26710
- groupG.lastSnappedDy = dy2;
26711
- setDraftGroupOverlayItems(
26712
- groupG.originItems.map((item) => ({
26713
- ...item,
26714
- rect: { ...item.rect, left: item.rect.left + dx2, top: item.rect.top + dy2 }
26715
- }))
26716
- );
26717
- for (const member of groupG.members) applyManualOffsetDragDraft(member, dx2, dy2);
26897
+ moveGroupDrag(groupG, e);
26718
26898
  return;
26719
26899
  }
26720
26900
  if (!g || !sel) return;
@@ -26756,6 +26936,9 @@ function createDomEditOverlayGestureHandlers(opts) {
26756
26936
  movingRect,
26757
26937
  proposedDx: dx,
26758
26938
  proposedDy: dy,
26939
+ // Same reason as the group path: a snap on a drag that has not travelled
26940
+ // yet moves the element while the pointer is still.
26941
+ disabledForTravel: !snapEngagedForTravel(dx, dy),
26759
26942
  targets: allTargets,
26760
26943
  gridEdges: sc.gridEdges ?? void 0,
26761
26944
  threshold: SNAP_THRESHOLD_PX,
@@ -26840,9 +27023,9 @@ function createDomEditOverlayGestureHandlers(opts) {
26840
27023
  opts.rafPausedRef.current = false;
26841
27024
  const rawDx = e.clientX - groupG.startX;
26842
27025
  const rawDy = e.clientY - groupG.startY;
27026
+ opts.suppressNextBoxClickRef.current = true;
26843
27027
  if (Math.hypot(rawDx, rawDy) < BLOCKED_MOVE_THRESHOLD_PX) {
26844
27028
  restoreGroupPathOffsets(groupG);
26845
- opts.suppressNextBoxClickRef.current = true;
26846
27029
  return;
26847
27030
  }
26848
27031
  const dx = groupG.lastSnappedDx ?? rawDx;
@@ -26857,12 +27040,27 @@ function createDomEditOverlayGestureHandlers(opts) {
26857
27040
  selection: member.selection,
26858
27041
  next: applyManualOffsetDragCommit(member, dx, dy)
26859
27042
  }));
27043
+ logDrag("drop", {
27044
+ pointer: `${Math.round(rawDx)},${Math.round(rawDy)}`,
27045
+ applied: `${Math.round(dx)},${Math.round(dy)}`,
27046
+ committed: Object.fromEntries(
27047
+ updates.map((update, index) => [
27048
+ groupG.members[index]?.key ?? String(index),
27049
+ `${Math.round(update.next.x)},${Math.round(update.next.y)}`
27050
+ ])
27051
+ ),
27052
+ at: readDragPositions(groupG.members)
27053
+ });
26860
27054
  void Promise.resolve(opts.onGroupPathOffsetCommitRef.current(updates)).catch(() => {
26861
27055
  for (const member of groupG.members) {
26862
27056
  if (member.gestureToken && isStudioManualEditGestureCurrent(member.element, member.gestureToken))
26863
27057
  restoreStudioPathOffset(member.element, member.initialPathOffset);
26864
27058
  }
26865
- }).finally(() => endManualOffsetDragMembers(groupG.members));
27059
+ }).finally(() => {
27060
+ logDrag("committed", { at: readDragPositions(groupG.members) });
27061
+ endManualOffsetDragMembers(groupG.members);
27062
+ logDragSettle("settle", groupG.members);
27063
+ });
26866
27064
  return;
26867
27065
  }
26868
27066
  if (!g || !sel) {
@@ -28194,18 +28392,18 @@ var CanvasContextMenu = memo21(function CanvasContextMenu2({
28194
28392
  },
28195
28393
  children: [
28196
28394
  hasZActions && Z_ACTIONS.map(({ action, label }) => {
28197
- const enabled2 = isZOrderActionEnabled(el, action);
28395
+ const enabled = isZOrderActionEnabled(el, action);
28198
28396
  return /* @__PURE__ */ jsxs47(
28199
28397
  "button",
28200
28398
  {
28201
28399
  type: "button",
28202
- className: `w-full flex items-center px-3 py-1.5 text-xs text-left ${enabled2 ? "text-neutral-300 hover:bg-neutral-800 cursor-pointer" : "text-neutral-600 cursor-not-allowed"}`,
28203
- disabled: !enabled2,
28400
+ className: `w-full flex items-center px-3 py-1.5 text-xs text-left ${enabled ? "text-neutral-300 hover:bg-neutral-800 cursor-pointer" : "text-neutral-600 cursor-not-allowed"}`,
28401
+ disabled: !enabled,
28204
28402
  onPointerDown: (e) => {
28205
28403
  if (e.button !== 0) return;
28206
28404
  e.preventDefault();
28207
28405
  e.stopPropagation();
28208
- if (enabled2) handleZAction(action);
28406
+ if (enabled) handleZAction(action);
28209
28407
  },
28210
28408
  children: [
28211
28409
  /* @__PURE__ */ jsx63(ZActionIcon, { action }),
@@ -28418,6 +28616,7 @@ var DomEditOverlay = memo22(function DomEditOverlay2({
28418
28616
  const handleOverlayMouseDown = (event) => {
28419
28617
  if (!allowCanvasMovement) return;
28420
28618
  if (suppressNextOverlayMouseDownRef.current) {
28619
+ logSelect("mousedown-suppressed", { shift: event.shiftKey });
28421
28620
  suppressNextOverlayMouseDownRef.current = false;
28422
28621
  suppressNextBoxMouseDownRef.current = false;
28423
28622
  suppressNextBoxClickRef.current = false;
@@ -28426,7 +28625,9 @@ var DomEditOverlay = memo22(function DomEditOverlay2({
28426
28625
  return;
28427
28626
  }
28428
28627
  const target = event.target;
28429
- if (target?.closest('[data-dom-edit-selection-box="true"]')) return;
28628
+ const onBox = Boolean(target?.closest('[data-dom-edit-selection-box="true"]'));
28629
+ logSelect("mousedown", { shift: event.shiftKey, onBox });
28630
+ if (onBox) return;
28430
28631
  onCanvasMouseDown(event, { hoverSelection: hoverSelectionRef.current });
28431
28632
  if (event.shiftKey) {
28432
28633
  suppressNextBoxMouseDownRef.current = true;
@@ -28436,7 +28637,16 @@ var DomEditOverlay = memo22(function DomEditOverlay2({
28436
28637
  const handleOverlayPointerDown = (event) => {
28437
28638
  if (!allowCanvasMovement || event.button !== 0) return;
28438
28639
  if (event.shiftKey) {
28439
- const candidate = hoverSelectionRef.current;
28640
+ const shiftIframe = iframeRef.current;
28641
+ const candidate = resolveShiftClickCandidate({
28642
+ cached: hoverSelectionRef.current,
28643
+ elementAtPoint: shiftIframe ? getPreviewTargetFromPointer(
28644
+ shiftIframe,
28645
+ event.clientX,
28646
+ event.clientY,
28647
+ activeCompositionPathRef.current
28648
+ ) : null
28649
+ });
28440
28650
  if (!candidate) return;
28441
28651
  event.preventDefault();
28442
28652
  event.stopPropagation();
@@ -28460,24 +28670,21 @@ var DomEditOverlay = memo22(function DomEditOverlay2({
28460
28670
  const overlayEl = overlayRef.current;
28461
28671
  if (overlayEl) {
28462
28672
  const oRect = overlayEl.getBoundingClientRect();
28673
+ event.preventDefault();
28674
+ event.stopPropagation();
28675
+ suppressNextOverlayMouseDownRef.current = true;
28676
+ event.currentTarget.setPointerCapture(event.pointerId);
28463
28677
  const cx = event.clientX - oRect.left;
28464
28678
  const cy = event.clientY - oRect.top;
28465
- const inComp = cx >= compRect.left && cx <= compRect.left + compRect.width && cy >= compRect.top && cy <= compRect.top + compRect.height;
28466
- if (inComp) {
28467
- event.preventDefault();
28468
- event.stopPropagation();
28469
- suppressNextOverlayMouseDownRef.current = true;
28470
- event.currentTarget.setPointerCapture(event.pointerId);
28471
- marquee.marqueeRef.current = {
28472
- startX: cx,
28473
- startY: cy,
28474
- currentX: cx,
28475
- currentY: cy,
28476
- pointerId: event.pointerId,
28477
- pastThreshold: false
28478
- };
28479
- return;
28480
- }
28679
+ marquee.marqueeRef.current = {
28680
+ startX: cx,
28681
+ startY: cy,
28682
+ currentX: cx,
28683
+ currentY: cy,
28684
+ pointerId: event.pointerId,
28685
+ pastThreshold: false
28686
+ };
28687
+ return;
28481
28688
  }
28482
28689
  }
28483
28690
  };
@@ -28575,7 +28782,8 @@ var DomEditOverlay = memo22(function DomEditOverlay2({
28575
28782
  left: cr.left,
28576
28783
  top: cr.top,
28577
28784
  width: cr.width,
28578
- height: cr.height
28785
+ height: cr.height,
28786
+ transform: cr.angle ? `rotate(${cr.angle}deg)` : void 0
28579
28787
  }
28580
28788
  },
28581
28789
  i
@@ -28862,18 +29070,16 @@ async function dispatchBufferedCommits(calls) {
28862
29070
  }
28863
29071
  return reloadsRequested(calls);
28864
29072
  }
28865
- function traceCommit(stage, data) {
28866
- if (import.meta.env.DEV) console.info(`[hf-commit] ${stage}`, data);
28867
- }
29073
+ var logCommit = makeStudioDebugLogger("commit");
28868
29074
  function runGestureTransaction(tx) {
28869
29075
  const startedAt = performance.now();
28870
29076
  const coalesceKey = `tx:${tx.label}:${++transactionCounter}`;
28871
29077
  let mutationCount = 0;
28872
29078
  let reloadCount = 0;
28873
29079
  const bufferedCommits = [];
28874
- traceCommit("start", { label: tx.label, coalesceKey });
29080
+ logCommit("start", { label: tx.label, coalesceKey });
28875
29081
  tx.settle();
28876
- traceCommit("settled", { label: tx.label, coalesceKey });
29082
+ logCommit("settled", { label: tx.label, coalesceKey });
28877
29083
  const before = !tx.skipPixelAssert ? readPixelRect(tx.element) : null;
28878
29084
  const commit = (commitMutation) => {
28879
29085
  const wrapped = (selection, mutation, options) => {
@@ -28892,19 +29098,12 @@ function runGestureTransaction(tx) {
28892
29098
  return tx.persist(commit).then(async () => {
28893
29099
  reloadCount = await dispatchBufferedCommits(bufferedCommits);
28894
29100
  const durationMs = Math.round(performance.now() - startedAt);
28895
- traceCommit("persisted", { label: tx.label, coalesceKey });
29101
+ logCommit("persisted", { label: tx.label, coalesceKey });
28896
29102
  if (before) {
28897
29103
  const after = readPixelRect(tx.element);
28898
29104
  const delta = pixelDelta(before, after);
28899
29105
  if (exceedsPixelTolerance(delta)) {
28900
- if (import.meta.env.DEV) {
28901
- console.error("[hf-commit] persist changed pixels", {
28902
- label: tx.label,
28903
- before,
28904
- after,
28905
- delta
28906
- });
28907
- }
29106
+ logCommit("persist-changed-pixels", { label: tx.label, before, after, delta });
28908
29107
  trackStudioEvent("commit_invariant_violation", {
28909
29108
  label: tx.label,
28910
29109
  delta_x: roundToOneDecimal(delta.x),
@@ -28932,7 +29131,7 @@ function runGestureTransaction(tx) {
28932
29131
  error_name: error instanceof Error ? error.name : "unknown",
28933
29132
  restore_ran: true
28934
29133
  });
28935
- traceCommit("restore", { label: tx.label, coalesceKey });
29134
+ logCommit("restore", { label: tx.label, coalesceKey });
28936
29135
  throw error;
28937
29136
  });
28938
29137
  }
@@ -31867,7 +32066,7 @@ function useElementLifecycleOps({
31867
32066
  `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
31868
32067
  {
31869
32068
  method: "POST",
31870
- headers: { "Content-Type": "application/json" },
32069
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
31871
32070
  body: JSON.stringify({ target: patchTarget })
31872
32071
  }
31873
32072
  );
@@ -33285,7 +33484,19 @@ import { isStudioHoldSet } from "@hyperframes/core/gsap-parser";
33285
33484
  function hasAnimations(value) {
33286
33485
  return typeof value === "object" && value !== null && "animations" in value && Array.isArray(value.animations);
33287
33486
  }
33288
- async function fetchParsedAnimations(projectId, sourceFile) {
33487
+ var inFlightParses = /* @__PURE__ */ new Map();
33488
+ function fetchParsedAnimations(projectId, sourceFile, options = {}) {
33489
+ const key2 = `${projectId}|${sourceFile}`;
33490
+ if (options.fresh) inFlightParses.delete(key2);
33491
+ const inFlight = inFlightParses.get(key2);
33492
+ if (inFlight) return inFlight;
33493
+ const request = requestParsedAnimations(projectId, sourceFile).finally(() => {
33494
+ if (inFlightParses.get(key2) === request) inFlightParses.delete(key2);
33495
+ });
33496
+ inFlightParses.set(key2, request);
33497
+ return request;
33498
+ }
33499
+ async function requestParsedAnimations(projectId, sourceFile) {
33289
33500
  try {
33290
33501
  const res = await fetch(
33291
33502
  `/api/projects/${encodeURIComponent(projectId)}/gsap-animations/${encodeURIComponent(sourceFile)}`,
@@ -34317,6 +34528,11 @@ function useTimelineSelectionPreviewSync({
34317
34528
  const currentAnchor = currentDomEditSelection ? resolveTimelineIdForSelection(currentDomEditSelection, timelineElements, activeCompPath) : null;
34318
34529
  if (selectedIds.length === 0) {
34319
34530
  missingSelectionKeyRef.current = "";
34531
+ logSelect("timeline-empty", {
34532
+ had: currentIds.length,
34533
+ previousKey: previousSelectedKey.length > 0,
34534
+ clearing: previousSelectedKey.length > 0 && currentIds.length > 0
34535
+ });
34320
34536
  if (previousSelectedKey.length > 0 && currentIds.length > 0) {
34321
34537
  applyDomSelection(null, { revealPanel: false });
34322
34538
  }
@@ -34346,6 +34562,11 @@ function useTimelineSelectionPreviewSync({
34346
34562
  return;
34347
34563
  }
34348
34564
  missingSelectionKeyRef.current = "";
34565
+ logSelect("timeline-sync", {
34566
+ wanted: selectedIds.length,
34567
+ had: currentIds.length,
34568
+ resolved: selections.length
34569
+ });
34349
34570
  if (selections.length === 0) {
34350
34571
  applyDomSelection(null, { revealPanel: false });
34351
34572
  } else if (selections.length === 1) {
@@ -37799,13 +38020,13 @@ function useColorGradingController({
37799
38020
  postColorGrading
37800
38021
  });
37801
38022
  const postCompare = useCallback57(
37802
- (enabled2) => {
38023
+ (enabled) => {
37803
38024
  postRuntimeControlMessage(
37804
38025
  previewIframeRef?.current?.contentWindow,
37805
38026
  "set-color-grading-compare",
37806
38027
  {
37807
38028
  target,
37808
- compare: { enabled: enabled2, position: 1, lineWidth: 0 }
38029
+ compare: { enabled, position: 1, lineWidth: 0 }
37809
38030
  }
37810
38031
  );
37811
38032
  },
@@ -37886,8 +38107,8 @@ function useColorGradingController({
37886
38107
  ]
37887
38108
  );
37888
38109
  const commitCompare = useCallback57(
37889
- (enabled2) => {
37890
- const nextEnabled = enabled2 && isHfColorGradingActive(grading);
38110
+ (enabled) => {
38111
+ const nextEnabled = enabled && isHfColorGradingActive(grading);
37891
38112
  setCompareEnabled(nextEnabled);
37892
38113
  if (nextEnabled) postColorGrading(grading);
37893
38114
  postCompare(nextEnabled);
@@ -39572,11 +39793,11 @@ function useVariablePromoteChannel(channel) {
39572
39793
  import { Fragment as Fragment25, jsx as jsx86, jsxs as jsxs66 } from "react/jsx-runtime";
39573
39794
  function PromotableControl({
39574
39795
  channel,
39575
- enabled: enabled2 = true,
39796
+ enabled = true,
39576
39797
  children
39577
39798
  }) {
39578
39799
  const promote = useVariablePromoteChannel(channel);
39579
- const danglingId = enabled2 && promote && promote.boundId != null && promote.declaration == null ? promote.boundId : null;
39800
+ const danglingId = enabled && promote && promote.boundId != null && promote.declaration == null ? promote.boundId : null;
39580
39801
  useEffect51(() => {
39581
39802
  if (danglingId != null) {
39582
39803
  console.warn(
@@ -39584,7 +39805,7 @@ function PromotableControl({
39584
39805
  );
39585
39806
  }
39586
39807
  }, [danglingId]);
39587
- if (!promote || !enabled2) return /* @__PURE__ */ jsx86(Fragment25, { children: children({ bound: false }) });
39808
+ if (!promote || !enabled) return /* @__PURE__ */ jsx86(Fragment25, { children: children({ bound: false }) });
39588
39809
  const bound = promote.boundId != null && promote.declaration != null;
39589
39810
  const canPromote = promote.action != null && !bound;
39590
39811
  const defaultValue = promote.declaration?.default;
@@ -42301,8 +42522,8 @@ var AnimationCard = memo29(function AnimationCard2({
42301
42522
  animation.properties.x != null || animation.properties.y != null ? 1 : 0,
42302
42523
  (animation.keyframes?.keyframes?.length ?? 0) - 1
42303
42524
  ),
42304
- onToggle: (enabled2) => onSetArcPath(animation.id, {
42305
- enabled: enabled2,
42525
+ onToggle: (enabled) => onSetArcPath(animation.id, {
42526
+ enabled,
42306
42527
  segments: animation.arcPath?.segments
42307
42528
  }),
42308
42529
  onUpdateSegment: (index, update) => onUpdateArcSegment?.(animation.id, index, update),
@@ -46775,7 +46996,7 @@ function PropertyPanelColorSecondary({
46775
46996
  {
46776
46997
  label: "Selection enabled",
46777
46998
  checked: selected.enabled,
46778
- onChange: (enabled2) => replaceSelected({ ...selected, enabled: enabled2 })
46999
+ onChange: (enabled) => replaceSelected({ ...selected, enabled })
46779
47000
  }
46780
47001
  ),
46781
47002
  /* @__PURE__ */ jsxs97(
@@ -51854,7 +52075,7 @@ function useCaptionSync(projectId) {
51854
52075
  const overrides = buildOverrides(state.model);
51855
52076
  fetch(`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`, {
51856
52077
  method: "PUT",
51857
- headers: { "Content-Type": "text/plain" },
52078
+ headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
51858
52079
  body: JSON.stringify(overrides, null, 2)
51859
52080
  }).catch((error) => {
51860
52081
  trackEvent("studio_caption_autosave_failed", { error: String(error) });
@@ -52548,40 +52769,6 @@ function usePanelLayout(initialState2) {
52548
52769
  // src/hooks/useFileManager.ts
52549
52770
  import { useState as useState84, useCallback as useCallback72, useMemo as useMemo49, useRef as useRef92 } from "react";
52550
52771
 
52551
- // src/utils/studioFileVersion.ts
52552
- var WRITE_TOKEN_TTL_MS = 5 * 6e4;
52553
- var studioWriteTokens = /* @__PURE__ */ new Map();
52554
- function pruneStudioWriteTokens(now2) {
52555
- for (const [token, createdAt] of studioWriteTokens) {
52556
- if (now2 - createdAt >= WRITE_TOKEN_TTL_MS) studioWriteTokens.delete(token);
52557
- }
52558
- }
52559
- function markStudioWriteToken(token, now2 = Date.now()) {
52560
- pruneStudioWriteTokens(now2);
52561
- studioWriteTokens.set(token, now2);
52562
- }
52563
- function consumeStudioWriteToken(token, now2 = Date.now()) {
52564
- pruneStudioWriteTokens(now2);
52565
- if (!token || !studioWriteTokens.has(token)) return false;
52566
- studioWriteTokens.delete(token);
52567
- return true;
52568
- }
52569
- async function studioFileContentVersion(content) {
52570
- const bytes = new TextEncoder().encode(content);
52571
- const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
52572
- const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(
52573
- ""
52574
- );
52575
- return `"sha256:${hex}"`;
52576
- }
52577
- async function studioExpectedFileVersion(versions, path, expectedContent) {
52578
- if (expectedContent !== void 0) return studioFileContentVersion(expectedContent);
52579
- return versions.get(path);
52580
- }
52581
- function createStudioWriteToken() {
52582
- return globalThis.crypto.randomUUID();
52583
- }
52584
-
52585
52772
  // src/hooks/useFileTree.ts
52586
52773
  import { useState as useState83, useCallback as useCallback70, useEffect as useEffect75, useMemo as useMemo48 } from "react";
52587
52774
  function useFileTree({ projectId, projectIdRef }) {
@@ -52827,8 +53014,6 @@ function useFileManager({
52827
53014
  }
52828
53015
  }
52829
53016
  await retryStudioSave(async () => {
52830
- const writeToken = createStudioWriteToken();
52831
- markStudioWriteToken(writeToken);
52832
53017
  let response;
52833
53018
  try {
52834
53019
  response = await fetch(
@@ -52837,7 +53022,7 @@ function useFileManager({
52837
53022
  method: "PUT",
52838
53023
  headers: {
52839
53024
  "Content-Type": "text/plain",
52840
- "X-Hyperframes-Write-Token": writeToken,
53025
+ ...studioWriteHeaders(),
52841
53026
  ...expectedVersion ? { "If-Match": expectedVersion } : { "If-None-Match": "*" }
52842
53027
  },
52843
53028
  body: content
@@ -53704,6 +53889,7 @@ async function requestAtomicCut(projectId, intents) {
53704
53889
  });
53705
53890
  }
53706
53891
  const transactionToken = `cut:${crypto.randomUUID()}`;
53892
+ markStudioWriteToken(transactionToken);
53707
53893
  const response = await fetch(buildProjectApiPath(projectId, "/file-mutations/split-batch"), {
53708
53894
  method: "POST",
53709
53895
  headers: {
@@ -53900,7 +54086,7 @@ async function insertTimelineComposition(input) {
53900
54086
  ),
53901
54087
  {
53902
54088
  method: "POST",
53903
- headers: { "Content-Type": "application/json" },
54089
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
53904
54090
  body: JSON.stringify({
53905
54091
  sourcePath: input.sourcePath,
53906
54092
  start: input.start,
@@ -55007,7 +55193,7 @@ function useTimelineEditing({
55007
55193
  `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`,
55008
55194
  {
55009
55195
  method: "POST",
55010
- headers: { "Content-Type": "application/json" },
55196
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
55011
55197
  body: JSON.stringify({ target: patchTarget })
55012
55198
  }
55013
55199
  );
@@ -55407,6 +55593,42 @@ function useStudioTestHooks({
55407
55593
  }, [applyDomSelection, buildDomSelectionFromTarget, previewIframeRef]);
55408
55594
  }
55409
55595
 
55596
+ // src/hooks/domSelectionTimelineMirror.ts
55597
+ function announceTimelineSelection(deps, group, primary) {
55598
+ const {
55599
+ timelineElements,
55600
+ getTimelineSelectionSet: getTimelineSelectionSet2,
55601
+ setSelectedTimelineElementId,
55602
+ setTimelineSelectionSet
55603
+ } = deps;
55604
+ if (!primary) {
55605
+ setTimelineSelectionSet(/* @__PURE__ */ new Set());
55606
+ setSelectedTimelineElementId(null);
55607
+ return;
55608
+ }
55609
+ const timelineIdFor = (selection) => findMatchingTimelineElementId(selection, timelineElements) ?? findTimelineIdByAncestor(
55610
+ selection.element,
55611
+ timelineElements,
55612
+ selection.sourceFile || "index.html"
55613
+ );
55614
+ const members = group.map(timelineIdFor).filter((id) => Boolean(id));
55615
+ const anchor = timelineIdFor(primary);
55616
+ const publishedMembers = new Set(members);
55617
+ if (anchor) publishedMembers.add(anchor);
55618
+ const timelineAnchor = anchor ?? members[0] ?? null;
55619
+ logSelect("announce", {
55620
+ group: group.length,
55621
+ published: publishedMembers.size,
55622
+ anchor,
55623
+ anchorPublished: anchor != null && publishedMembers.has(anchor)
55624
+ });
55625
+ if (!timelineAnchor) return;
55626
+ if (group.length > 1 || !getTimelineSelectionSet2().has(timelineAnchor)) {
55627
+ setTimelineSelectionSet(publishedMembers);
55628
+ }
55629
+ setSelectedTimelineElementId(timelineAnchor, { preserveSet: true });
55630
+ }
55631
+
55410
55632
  // src/hooks/useDomSelection.ts
55411
55633
  function useDomSelection({
55412
55634
  projectId,
@@ -55416,7 +55638,9 @@ function useDomSelection({
55416
55638
  captionEditMode,
55417
55639
  previewIframeRef,
55418
55640
  timelineElements,
55641
+ getTimelineSelectionSet: getTimelineSelectionSet2,
55419
55642
  setSelectedTimelineElementId,
55643
+ setTimelineSelectionSet,
55420
55644
  setRightCollapsed,
55421
55645
  setRightPanelTab,
55422
55646
  previewIframe,
@@ -55439,15 +55663,34 @@ function useDomSelection({
55439
55663
  domEditGroupSelectionsRef.current = domEditGroupSelections;
55440
55664
  domEditHoverSelectionRef.current = domEditHoverSelection;
55441
55665
  activeGroupElementRef.current = activeGroupElement;
55666
+ const announceTimelineSelection2 = useCallback81(
55667
+ (group, primary) => announceTimelineSelection(
55668
+ {
55669
+ timelineElements,
55670
+ getTimelineSelectionSet: getTimelineSelectionSet2,
55671
+ setSelectedTimelineElementId,
55672
+ setTimelineSelectionSet
55673
+ },
55674
+ group,
55675
+ primary
55676
+ ),
55677
+ [
55678
+ getTimelineSelectionSet2,
55679
+ setSelectedTimelineElementId,
55680
+ setTimelineSelectionSet,
55681
+ timelineElements
55682
+ ]
55683
+ );
55442
55684
  const applyDomSelection = useCallback81(
55443
55685
  // fallow-ignore-next-line complexity
55444
55686
  (selection, options) => {
55445
55687
  if (!selection) {
55688
+ logSelect("clear", { hadGroup: domEditGroupSelectionsRef.current.length });
55446
55689
  domEditSelectionRef.current = null;
55447
55690
  domEditGroupSelectionsRef.current = [];
55448
55691
  setDomEditSelection(null);
55449
55692
  setDomEditGroupSelections([]);
55450
- setSelectedTimelineElementId(null);
55693
+ announceTimelineSelection2([], null);
55451
55694
  return;
55452
55695
  }
55453
55696
  const isAdditiveSelection = Boolean(options?.additive);
@@ -55457,6 +55700,13 @@ function useDomSelection({
55457
55700
  const wasInGroup = domEditSelectionInGroup(currentGroup, selection);
55458
55701
  const nextGroup = options?.preserveGroup ? replaceDomEditGroupSelection(currentGroup, selection) : isAdditiveSelection ? toggleDomEditGroupSelection(currentGroup, selection) : [selection];
55459
55702
  const nextSelection = options?.preserveGroup ? selection : isAdditiveSelection && wasInGroup ? domEditSelectionsTargetSame(currentSelection, selection) ? nextGroup[0] ?? null : domEditSelectionInGroup(nextGroup, currentSelection) ? currentSelection : nextGroup[0] ?? null : selection;
55703
+ logSelect("apply", {
55704
+ additive: isAdditiveSelection,
55705
+ target: selection.selector ?? selection.id ?? null,
55706
+ wasInGroup,
55707
+ prevGroup: previousGroup.length,
55708
+ nextGroup: nextGroup.length
55709
+ });
55460
55710
  domEditSelectionRef.current = nextSelection;
55461
55711
  domEditGroupSelectionsRef.current = nextGroup;
55462
55712
  setDomEditSelection(nextSelection);
@@ -55473,17 +55723,12 @@ function useDomSelection({
55473
55723
  setRightPanelTab("design");
55474
55724
  }
55475
55725
  }
55476
- const nextSelectedTimelineId = findMatchingTimelineElementId(nextSelection, timelineElements) ?? findTimelineIdByAncestor(
55477
- nextSelection.element,
55478
- timelineElements,
55479
- nextSelection.sourceFile || "index.html"
55480
- );
55481
- setSelectedTimelineElementId(nextSelectedTimelineId, { preserveSet: true });
55726
+ announceTimelineSelection2(nextGroup, nextSelection);
55482
55727
  return;
55483
55728
  }
55484
- setSelectedTimelineElementId(null);
55729
+ announceTimelineSelection2([], null);
55485
55730
  },
55486
- [setSelectedTimelineElementId, timelineElements, setRightCollapsed, setRightPanelTab]
55731
+ [announceTimelineSelection2, setRightCollapsed, setRightPanelTab]
55487
55732
  );
55488
55733
  const clearDomSelection = useCallback81(() => {
55489
55734
  applyDomSelection(null, { revealPanel: false });
@@ -55595,6 +55840,8 @@ function useDomSelection({
55595
55840
  },
55596
55841
  [applyDomSelection, buildDomSelectionForTimelineElement]
55597
55842
  );
55843
+ const refreshDomEditGroupSelectionsFromPreviewRef = useRef98(async () => {
55844
+ });
55598
55845
  const refreshDomEditSelectionFromPreview = useCallback81(
55599
55846
  // fallow-ignore-next-line complexity
55600
55847
  async (selection) => {
@@ -55608,6 +55855,15 @@ function useDomSelection({
55608
55855
  if (!doc) return;
55609
55856
  const element = findElementForSelection(doc, selection, activeCompPath);
55610
55857
  if (!element) {
55858
+ const group = domEditGroupSelectionsRef.current;
55859
+ logSelect("refresh-lost", {
55860
+ target: selection.selector ?? selection.id ?? null,
55861
+ group: group.length
55862
+ });
55863
+ if (group.length > 1) {
55864
+ await refreshDomEditGroupSelectionsFromPreviewRef.current(group);
55865
+ return;
55866
+ }
55611
55867
  applyDomSelection(null, { revealPanel: false });
55612
55868
  return;
55613
55869
  }
@@ -55646,22 +55902,13 @@ function useDomSelection({
55646
55902
  domEditGroupSelectionsRef.current = nextGroup;
55647
55903
  setDomEditSelection(nextSelection);
55648
55904
  setDomEditGroupSelections(nextGroup);
55649
- if (nextSelection) {
55650
- setSelectedTimelineElementId(
55651
- findMatchingTimelineElementId(nextSelection, timelineElements)
55652
- );
55653
- } else {
55654
- setSelectedTimelineElementId(null);
55655
- }
55905
+ announceTimelineSelection2(nextGroup, nextSelection);
55656
55906
  },
55657
- [
55658
- activeCompPath,
55659
- buildDomSelectionFromTarget,
55660
- setSelectedTimelineElementId,
55661
- timelineElements,
55662
- previewIframeRef
55663
- ]
55907
+ [activeCompPath, announceTimelineSelection2, buildDomSelectionFromTarget, previewIframeRef]
55664
55908
  );
55909
+ useEffect79(() => {
55910
+ refreshDomEditGroupSelectionsFromPreviewRef.current = refreshDomEditGroupSelectionsFromPreview;
55911
+ }, [refreshDomEditGroupSelectionsFromPreview]);
55665
55912
  useEffect79(() => {
55666
55913
  updateDomEditHoverSelection(null);
55667
55914
  }, [activeCompPath, projectId, previewIframe, refreshKey, updateDomEditHoverSelection]);
@@ -55692,6 +55939,7 @@ function useDomSelection({
55692
55939
  const applyMarqueeSelection = useCallback81(
55693
55940
  // fallow-ignore-next-line complexity
55694
55941
  (selections, additive) => {
55942
+ logSelect("marquee", { hits: selections.length, additive });
55695
55943
  if (selections.length === 0) {
55696
55944
  if (!additive) applyDomSelection(null, { revealPanel: false });
55697
55945
  return;
@@ -55715,14 +55963,9 @@ function useDomSelection({
55715
55963
  domEditGroupSelectionsRef.current = nextGroup;
55716
55964
  setDomEditSelection(nextSelection);
55717
55965
  setDomEditGroupSelections(nextGroup);
55718
- const nextTimelineId = findMatchingTimelineElementId(nextSelection, timelineElements) ?? findTimelineIdByAncestor(
55719
- nextSelection.element,
55720
- timelineElements,
55721
- nextSelection.sourceFile || "index.html"
55722
- );
55723
- setSelectedTimelineElementId(nextTimelineId);
55966
+ announceTimelineSelection2(nextGroup, nextSelection);
55724
55967
  },
55725
- [applyDomSelection, timelineElements, setSelectedTimelineElementId]
55968
+ [applyDomSelection, announceTimelineSelection2]
55726
55969
  );
55727
55970
  return {
55728
55971
  // State
@@ -57132,7 +57375,7 @@ async function assignGsapTargetAutoIdIfNeeded({
57132
57375
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
57133
57376
  {
57134
57377
  method: "POST",
57135
- headers: { "Content-Type": "application/json" },
57378
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
57136
57379
  body: JSON.stringify(patchBody)
57137
57380
  }
57138
57381
  );
@@ -57198,7 +57441,7 @@ async function patchElementBatches(projectId, batches) {
57198
57441
  `/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
57199
57442
  {
57200
57443
  method: "POST",
57201
- headers: { "Content-Type": "application/json" },
57444
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
57202
57445
  body
57203
57446
  }
57204
57447
  );
@@ -57323,7 +57566,7 @@ function useDomEditCommits({
57323
57566
  `/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
57324
57567
  {
57325
57568
  method: "POST",
57326
- headers: { "Content-Type": "application/json" },
57569
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
57327
57570
  body: JSON.stringify(patchBody)
57328
57571
  }
57329
57572
  );
@@ -57556,7 +57799,7 @@ async function commitStructuralMutation(pid, targetPath, route, body, label, dep
57556
57799
  `/api/projects/${pid}/file-mutations/${route}/${encodeURIComponent(targetPath)}`,
57557
57800
  {
57558
57801
  method: "POST",
57559
- headers: { "Content-Type": "application/json" },
57802
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
57560
57803
  body: JSON.stringify(body)
57561
57804
  }
57562
57805
  );
@@ -57785,7 +58028,7 @@ function applyChange(tween, change) {
57785
58028
  return rebuildKeyframeTween(tween, change.pct, change.props);
57786
58029
  return false;
57787
58030
  }
57788
- function patchRuntimeTweenInPlace(iframe, selector, change, compositionId) {
58031
+ function patchRuntimeTweenInPlace(iframe, selector, change, compositionId, deferSeek = false) {
57789
58032
  if (!iframe) return false;
57790
58033
  if (change.kind === "global-set") return applyGlobalSet(iframe, selector, change.props);
57791
58034
  try {
@@ -57803,7 +58046,7 @@ function patchRuntimeTweenInPlace(iframe, selector, change, compositionId) {
57803
58046
  if (change.kind !== "keyframe-rebuild") {
57804
58047
  tween.invalidate?.();
57805
58048
  }
57806
- seekToCurrent(iframe, timeline);
58049
+ if (!deferSeek) seekToCurrent(iframe, timeline);
57807
58050
  return true;
57808
58051
  } catch {
57809
58052
  return false;
@@ -58722,7 +58965,7 @@ async function mutateGsapScript(projectId, sourceFile, mutation) {
58722
58965
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations/${encodeURIComponent(sourceFile)}`,
58723
58966
  {
58724
58967
  method: "POST",
58725
- headers: { "Content-Type": "application/json" },
58968
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
58726
58969
  body: JSON.stringify(mutation)
58727
58970
  }
58728
58971
  );
@@ -58736,7 +58979,7 @@ async function mutateGsapScriptBatch(projectId, sourceFile, mutations) {
58736
58979
  `/api/projects/${encodeURIComponent(projectId)}/gsap-mutations-batch/${encodeURIComponent(sourceFile)}`,
58737
58980
  {
58738
58981
  method: "POST",
58739
- headers: { "Content-Type": "application/json" },
58982
+ headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
58740
58983
  body: JSON.stringify({ mutations })
58741
58984
  }
58742
58985
  );
@@ -58777,7 +59020,7 @@ async function runMutationRequest(mutations, options, showToast, request) {
58777
59020
  }
58778
59021
  function finishUnchangedMutation(iframe, result, options, reloadPreview) {
58779
59022
  if (result.changed !== false) return false;
58780
- if (!options.skipReload && options.instantPatch) {
59023
+ if (!options.skipReload && (instantPatchesFor(options).length > 0 || options.previewFallbackLatch?.pending)) {
58781
59024
  applyPreviewSync(iframe, result, options, reloadPreview);
58782
59025
  }
58783
59026
  return true;
@@ -58834,15 +59077,31 @@ function softReloadOrEscalate(iframe, scriptText, reloadPreview, origin, authore
58834
59077
  if (result === "cannot-soft-reload") reloadPreview();
58835
59078
  }
58836
59079
  function applyPreviewSync(iframe, result, options, reloadPreview) {
58837
- if (options.instantPatch) {
58838
- const patched = patchRuntimeTweenInPlace(
58839
- iframe,
58840
- options.instantPatch.selector,
58841
- options.instantPatch.change
59080
+ const patches = instantPatchesFor(options);
59081
+ let needsFallback = options.previewFallbackLatch?.pending === true;
59082
+ if (patches.length > 0) {
59083
+ const deferSeek = options.deferPreviewSync === true;
59084
+ const missed = patches.find(
59085
+ (patch, index) => !patchRuntimeTweenInPlace(
59086
+ iframe,
59087
+ patch.selector,
59088
+ patch.change,
59089
+ void 0,
59090
+ deferSeek || index < patches.length - 1
59091
+ )
58842
59092
  );
58843
- if (patched) return;
58844
- trackStudioEvent("gsap_instant_patch_fallback", { selector: options.instantPatch.selector });
59093
+ if (missed) {
59094
+ trackStudioEvent("gsap_instant_patch_fallback", { selector: missed.selector });
59095
+ needsFallback = true;
59096
+ }
59097
+ if (!needsFallback) return;
58845
59098
  }
59099
+ if (options.deferPreviewSync && options.previewFallbackLatch) {
59100
+ options.previewFallbackLatch.pending = needsFallback;
59101
+ return;
59102
+ }
59103
+ if (options.deferPreviewSync && !needsFallback) return;
59104
+ if (options.previewFallbackLatch) options.previewFallbackLatch.pending = false;
58846
59105
  if (options.softReload && result.scriptText) {
58847
59106
  softReloadOrEscalate(
58848
59107
  iframe,
@@ -58855,6 +59114,12 @@ function applyPreviewSync(iframe, result, options, reloadPreview) {
58855
59114
  reloadPreview();
58856
59115
  }
58857
59116
  }
59117
+ function instantPatchesFor(options) {
59118
+ return [
59119
+ ...options.instantPatches ?? [],
59120
+ ...options.instantPatch ? [options.instantPatch] : []
59121
+ ];
59122
+ }
58858
59123
  function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef, editHistory, domEditSaveTimestampRef, reloadPreview, onCacheInvalidate, onFileContentChanged, showToast, sdkSession, publishSdkSession, writeProjectFile, forceReloadSdkSession }) {
58859
59124
  const activeProjectId = projectIdRef.current;
58860
59125
  const activeCompPathRef = useRef104(activeCompPath);
@@ -58924,7 +59189,9 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
58924
59189
  );
58925
59190
  if (!result) return;
58926
59191
  options.onResult?.(result);
58927
- await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, options);
59192
+ const instantPatches = calls.map(({ options: callOptions }) => callOptions.instantPatch).filter((patch) => patch !== void 0);
59193
+ const { instantPatch: _instantPatch, ...batchOptions } = options;
59194
+ await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, instantPatches.length > 0 ? { ...batchOptions, instantPatches } : batchOptions);
58928
59195
  }, [showToast, finalizeSuccessfulMutation]);
58929
59196
  const commitMutation = useMemo50(() => {
58930
59197
  const serializeFile = (file, task) => {
@@ -59050,8 +59317,10 @@ function useDomEditPreviewSync({
59050
59317
  activeCompPath,
59051
59318
  captionEditMode,
59052
59319
  domEditSelectionRef,
59320
+ domEditGroupSelectionsRef,
59053
59321
  domEditSelection,
59054
59322
  applyDomSelection,
59323
+ refreshDomEditGroupSelectionsFromPreview,
59055
59324
  buildDomSelectionFromTarget,
59056
59325
  refreshPreviewDocumentVersion,
59057
59326
  syncPreviewHistoryHotkey,
@@ -59076,6 +59345,15 @@ function useDomEditPreviewSync({
59076
59345
  reapplyPositionEditsAfterSeek(doc);
59077
59346
  const nextElement = findElementForSelection(doc, currentSelection, activeCompPath);
59078
59347
  if (!nextElement) {
59348
+ const group = domEditGroupSelectionsRef.current;
59349
+ logSelect("preview-sync-lost", {
59350
+ target: currentSelection.selector ?? currentSelection.id ?? null,
59351
+ group: group.length
59352
+ });
59353
+ if (group.length > 1) {
59354
+ await refreshDomEditGroupSelectionsFromPreview(group);
59355
+ return;
59356
+ }
59079
59357
  applyDomSelection(null, { revealPanel: false });
59080
59358
  return;
59081
59359
  }
@@ -59103,8 +59381,10 @@ function useDomEditPreviewSync({
59103
59381
  applyDomSelection,
59104
59382
  buildDomSelectionFromTarget,
59105
59383
  captionEditMode,
59384
+ domEditGroupSelectionsRef,
59106
59385
  domEditSelectionRef,
59107
59386
  previewIframe,
59387
+ refreshDomEditGroupSelectionsFromPreview,
59108
59388
  refreshPreviewDocumentVersion,
59109
59389
  syncPreviewHistoryHotkey,
59110
59390
  applyStudioManualEditsToPreviewRef,
@@ -59140,11 +59420,12 @@ function selectElementAnimationsOrRetry(parsed, target) {
59140
59420
  if (parsed.animations.length === 0) return { kind: "cold" };
59141
59421
  return { kind: "resolved", animations: getAnimationsForElement(parsed.animations, target) };
59142
59422
  }
59143
- async function fetchElementAnimationsWithRetry(projectId, gsapSourceFile, target, failOnFetchError) {
59423
+ async function fetchElementAnimationsWithRetry(projectId, gsapSourceFile, target, failOnFetchError, fresh) {
59144
59424
  let coldAttempts = 0;
59145
59425
  let errorAttempts = 0;
59146
59426
  for (; ; ) {
59147
- const parsed = await fetchParsedAnimations(projectId, gsapSourceFile);
59427
+ const parsed = await fetchParsedAnimations(projectId, gsapSourceFile, { fresh });
59428
+ fresh = false;
59148
59429
  const outcome = selectElementAnimationsOrRetry(parsed, target);
59149
59430
  if (outcome.kind === "resolved") return outcome.animations;
59150
59431
  if (outcome.kind === "fetch-error") {
@@ -59170,7 +59451,8 @@ function useGsapAnimationFetchFallback(projectId, gsapSourceFile) {
59170
59451
  projectId,
59171
59452
  gsapSourceFile,
59172
59453
  target,
59173
- options?.failOnFetchError === true
59454
+ options?.failOnFetchError === true,
59455
+ options?.fresh === true
59174
59456
  );
59175
59457
  },
59176
59458
  [projectId, gsapSourceFile]
@@ -59185,12 +59467,12 @@ function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
59185
59467
  trackStudioSaveFailure({
59186
59468
  source: "gsap_commit",
59187
59469
  error,
59188
- filePath: selection.sourceFile ?? activeCompPath ?? "index.html",
59470
+ filePath: selection?.sourceFile ?? activeCompPath ?? "index.html",
59189
59471
  mutationType,
59190
59472
  label,
59191
- targetId: selection.id,
59192
- targetSelector: selection.selector,
59193
- targetSourceFile: selection.sourceFile
59473
+ targetId: selection?.id,
59474
+ targetSelector: selection?.selector,
59475
+ targetSourceFile: selection?.sourceFile
59194
59476
  });
59195
59477
  showToast(
59196
59478
  isGsapEditBlockedError(error) ? error.message : "Failed to save animated edit.",
@@ -59502,6 +59784,8 @@ function useDomEditWiring({
59502
59784
  activeCompPath,
59503
59785
  domEditSelection,
59504
59786
  domEditSelectionRef,
59787
+ domEditGroupSelectionsRef,
59788
+ refreshDomEditGroupSelectionsFromPreview,
59505
59789
  previewIframeRef,
59506
59790
  previewIframe,
59507
59791
  captionEditMode,
@@ -59615,8 +59899,10 @@ function useDomEditWiring({
59615
59899
  activeCompPath,
59616
59900
  captionEditMode,
59617
59901
  domEditSelectionRef,
59902
+ domEditGroupSelectionsRef,
59618
59903
  domEditSelection,
59619
59904
  applyDomSelection,
59905
+ refreshDomEditGroupSelectionsFromPreview,
59620
59906
  buildDomSelectionFromTarget,
59621
59907
  refreshPreviewDocumentVersion,
59622
59908
  syncPreviewHistoryHotkey,
@@ -60302,6 +60588,14 @@ function useAnimatedPropertyCommit(deps) {
60302
60588
 
60303
60589
  // src/hooks/useGsapAwareEditing.ts
60304
60590
  var groupDragCommitCounter = 0;
60591
+ function firstPreflightFailure(results, updates) {
60592
+ for (const [index, result] of results.entries()) {
60593
+ if (result.status !== "rejected") continue;
60594
+ const selection = updates[index]?.selection;
60595
+ if (selection) return { error: result.reason, selection };
60596
+ }
60597
+ return null;
60598
+ }
60305
60599
  function useGsapAwareEditing({
60306
60600
  domEditSelection,
60307
60601
  selectedGsapAnimations,
@@ -60349,14 +60643,37 @@ function useGsapAwareEditing({
60349
60643
  async (updates) => {
60350
60644
  if (!gsapCommitMutation || updates.length === 0) return;
60351
60645
  const coalesceKey = `group-drag:${++groupDragCommitCounter}`;
60352
- const coalescedCommit = (selection, mutation, options) => gsapCommitMutation(selection, mutation, {
60646
+ let renderOnCommit = false;
60647
+ const previewFallbackLatch = { pending: false };
60648
+ const withGroupOptions = (options) => ({
60353
60649
  ...options,
60354
60650
  coalesceKey,
60355
- coalesceMs: Number.POSITIVE_INFINITY
60651
+ coalesceMs: Number.POSITIVE_INFINITY,
60652
+ deferPreviewSync: !renderOnCommit,
60653
+ previewFallbackLatch
60356
60654
  });
60655
+ const queued = [];
60656
+ const flushQueued = async () => {
60657
+ if (queued.length === 0) return;
60658
+ const calls = queued.splice(0, queued.length);
60659
+ if (!gsapCommitMutation.batch) {
60660
+ for (const call of calls) {
60661
+ await gsapCommitMutation(call.selection, call.mutation, call.options);
60662
+ }
60663
+ return;
60664
+ }
60665
+ await gsapCommitMutation.batch(calls, {
60666
+ ...calls.at(-1)?.options ?? { label: "Move animated layer (group)" },
60667
+ label: "Move animated layer (group)"
60668
+ });
60669
+ };
60670
+ const coalescedCommit = (selection, mutation, options) => {
60671
+ queued.push({ selection, mutation, options: withGroupOptions(options) });
60672
+ return Promise.resolve();
60673
+ };
60357
60674
  const preflightAnimations = /* @__PURE__ */ new Map();
60358
- for (const { selection } of updates) {
60359
- try {
60675
+ const preflightResults = await Promise.allSettled(
60676
+ updates.map(async ({ selection }) => {
60360
60677
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
60361
60678
  preflightAnimations.set(selection, animations);
60362
60679
  const outcome = await tryGsapDragIntercept(
@@ -60369,12 +60686,20 @@ function useGsapAwareEditing({
60369
60686
  { preflightOnly: true }
60370
60687
  );
60371
60688
  assertGsapEditPersisted(outcome);
60372
- } catch (error) {
60373
- trackGsapInteractionFailure(error, selection, "drag", "Move animated layer (group)");
60374
- throw error;
60375
- }
60689
+ })
60690
+ );
60691
+ const preflightFailure = firstPreflightFailure(preflightResults, updates);
60692
+ if (preflightFailure) {
60693
+ trackGsapInteractionFailure(
60694
+ preflightFailure.error,
60695
+ preflightFailure.selection,
60696
+ "drag",
60697
+ "Move animated layer (group)"
60698
+ );
60699
+ throw preflightFailure.error;
60376
60700
  }
60377
- for (const { selection, next } of updates) {
60701
+ for (const [index, { selection, next }] of updates.entries()) {
60702
+ renderOnCommit = index === updates.length - 1;
60378
60703
  try {
60379
60704
  const outcome = await tryGsapDragIntercept(
60380
60705
  selection,
@@ -60382,7 +60707,13 @@ function useGsapAwareEditing({
60382
60707
  preflightAnimations.get(selection) ?? [],
60383
60708
  previewIframeRef.current,
60384
60709
  coalescedCommit,
60385
- makeFetchFallback(selection),
60710
+ // The intercept re-reads the file to resolve a stale or shared tween.
60711
+ // Anything already queued has to be on disk before that read, or it
60712
+ // resolves against a file missing writes it is about to build on.
60713
+ async () => {
60714
+ await flushQueued();
60715
+ return makeFetchFallback(selection, { fresh: true })();
60716
+ },
60386
60717
  { preflightPassed: true }
60387
60718
  );
60388
60719
  assertGsapEditPersisted(outcome);
@@ -60391,6 +60722,12 @@ function useGsapAwareEditing({
60391
60722
  throw error;
60392
60723
  }
60393
60724
  }
60725
+ try {
60726
+ await flushQueued();
60727
+ } catch (error) {
60728
+ trackGsapInteractionFailure(error, null, "drag", "Move animated layer (group)");
60729
+ throw error;
60730
+ }
60394
60731
  },
60395
60732
  [gsapCommitMutation, previewIframeRef, makeFetchFallback, trackGsapInteractionFailure]
60396
60733
  );
@@ -60787,13 +61124,14 @@ function useKeyframeEaseCommits({
60787
61124
  function useDomEditSession({
60788
61125
  projectId,
60789
61126
  activeCompPath,
60790
- isMasterView,
60791
61127
  compIdToSrc,
60792
61128
  captionEditMode,
60793
61129
  compositionLoading,
60794
61130
  previewIframeRef,
60795
61131
  timelineElements,
61132
+ getTimelineSelectionSet: getTimelineSelectionSet2,
60796
61133
  setSelectedTimelineElementId,
61134
+ setTimelineSelectionSet,
60797
61135
  setRightCollapsed,
60798
61136
  setRightPanelTab,
60799
61137
  showToast,
@@ -60823,6 +61161,7 @@ function useDomEditSession({
60823
61161
  publishSdkSession,
60824
61162
  forceReloadSdkSession
60825
61163
  }) {
61164
+ const isMasterView = !activeCompPath || activeCompPath === "index.html";
60826
61165
  void _setRefreshKey;
60827
61166
  const {
60828
61167
  domEditSelection,
@@ -60841,6 +61180,7 @@ function useDomEditSession({
60841
61180
  buildDomSelectionForTimelineElement,
60842
61181
  handleTimelineElementSelect,
60843
61182
  refreshDomEditSelectionFromPreview,
61183
+ refreshDomEditGroupSelectionsFromPreview,
60844
61184
  applyMarqueeSelection
60845
61185
  } = useDomSelection({
60846
61186
  projectId,
@@ -60850,7 +61190,9 @@ function useDomEditSession({
60850
61190
  captionEditMode,
60851
61191
  previewIframeRef,
60852
61192
  timelineElements,
61193
+ getTimelineSelectionSet: getTimelineSelectionSet2,
60853
61194
  setSelectedTimelineElementId,
61195
+ setTimelineSelectionSet,
60854
61196
  setRightCollapsed,
60855
61197
  setRightPanelTab,
60856
61198
  previewIframe,
@@ -61061,6 +61403,8 @@ function useDomEditSession({
61061
61403
  activeCompPath,
61062
61404
  domEditSelection,
61063
61405
  domEditSelectionRef,
61406
+ domEditGroupSelectionsRef,
61407
+ refreshDomEditGroupSelectionsFromPreview,
61064
61408
  previewIframeRef,
61065
61409
  previewIframe,
61066
61410
  captionEditMode,
@@ -61611,6 +61955,7 @@ function useExternalFileChangeCoordinator({
61611
61955
  }, [loadConflictSnapshot, projectId, recoveryFilePath]);
61612
61956
  const reloadAcceptedGeneration = useCallback104(
61613
61957
  (path) => {
61958
+ logReload("reload", { path, by: "external-change coordinator" });
61614
61959
  reloadPreview();
61615
61960
  reloadSdkSession(path);
61616
61961
  },
@@ -61632,10 +61977,21 @@ function useExternalFileChangeCoordinator({
61632
61977
  const pendingTimelinePaths = pendingTimelineEditPathRef.current;
61633
61978
  pendingTimelinePaths.delete(path);
61634
61979
  const content = readFileChangeContent(payload);
61635
- if (consumeStudioWriteToken(readFileChangeWriteToken(payload))) return;
61636
- if (content != null && isSelfWriteEcho(path, content)) return;
61980
+ const token = readFileChangeWriteToken(payload);
61981
+ logReload("file-change", { path, token: token ?? null, hasContent: content != null });
61982
+ if (consumeStudioWriteToken(token)) {
61983
+ logReload("suppressed", { path, why: "own write token" });
61984
+ return;
61985
+ }
61986
+ if (content != null && isSelfWriteEcho(path, content)) {
61987
+ logReload("suppressed", { path, why: "own content echo" });
61988
+ return;
61989
+ }
61637
61990
  const identity = eventIdentity(path, payload);
61638
- if (!allowDuplicate && identity != null && identity === lastEventIdentityRef.current) return;
61991
+ if (!allowDuplicate && identity != null && identity === lastEventIdentityRef.current) {
61992
+ logReload("suppressed", { path, why: "duplicate event" });
61993
+ return;
61994
+ }
61639
61995
  lastEventIdentityRef.current = identity;
61640
61996
  const generation = ++generationRef.current;
61641
61997
  const result = await drainPendingChanges();
@@ -63966,17 +64322,50 @@ function parseNumber(value) {
63966
64322
  function parseTab(value) {
63967
64323
  return VALID_TABS.includes(value) ? value : null;
63968
64324
  }
64325
+ function optionalString(value) {
64326
+ return typeof value === "string" ? value : void 0;
64327
+ }
64328
+ function normalizedIndex(value) {
64329
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
64330
+ }
64331
+ function parseSelectionTarget(value) {
64332
+ if (!value || typeof value !== "object") return null;
64333
+ const sourceFile = optionalString(Reflect.get(value, "sourceFile"));
64334
+ const id = optionalString(Reflect.get(value, "id"));
64335
+ const selector = optionalString(Reflect.get(value, "selector"));
64336
+ if (!id && !selector) return null;
64337
+ return {
64338
+ sourceFile,
64339
+ id,
64340
+ selector,
64341
+ selectorIndex: normalizedIndex(Reflect.get(value, "selectorIndex"))
64342
+ };
64343
+ }
64344
+ function parseGroup(value) {
64345
+ if (!value) return void 0;
64346
+ let parsed;
64347
+ try {
64348
+ parsed = JSON.parse(value);
64349
+ } catch {
64350
+ const legacy = value.split(",").map((id) => id.trim()).filter(Boolean).map((id) => ({ id }));
64351
+ return legacy.length > 0 ? legacy : void 0;
64352
+ }
64353
+ if (!Array.isArray(parsed)) return void 0;
64354
+ const targets = parsed.map(parseSelectionTarget).filter((target) => target !== null);
64355
+ return targets.length > 0 ? targets : void 0;
64356
+ }
63969
64357
  function normalizeSelection(params) {
63970
64358
  const sourceFile = params.get("selFile") || void 0;
63971
64359
  const id = params.get("selId") || void 0;
63972
64360
  const selector = params.get("selSelector") || void 0;
63973
- const selectorIndex = parseNumber(params.get("selIndex"));
63974
64361
  if (!sourceFile && !id && !selector) return null;
64362
+ const selectorIndex = parseNumber(params.get("selIndex"));
63975
64363
  return {
63976
64364
  sourceFile,
63977
64365
  id,
63978
64366
  selector,
63979
- selectorIndex: selectorIndex != null ? Math.max(0, Math.floor(selectorIndex)) : void 0
64367
+ selectorIndex: selectorIndex != null ? Math.max(0, Math.floor(selectorIndex)) : void 0,
64368
+ group: parseGroup(params.get("selGroup"))
63980
64369
  };
63981
64370
  }
63982
64371
  function defaultStudioUrlState() {
@@ -64023,13 +64412,15 @@ function buildStudioHash(projectId, state) {
64023
64412
  if (typeof state.selection.selectorIndex === "number") {
64024
64413
  params.set("selIndex", String(Math.max(0, Math.floor(state.selection.selectorIndex))));
64025
64414
  }
64415
+ if (state.selection.group?.length) {
64416
+ params.set("selGroup", JSON.stringify(state.selection.group));
64417
+ }
64026
64418
  }
64027
64419
  return buildProjectHash(projectId, params);
64028
64420
  }
64029
64421
 
64030
64422
  // src/hooks/useStudioUrlState.ts
64031
- function toPersistedSelection(selection) {
64032
- if (!selection) return null;
64423
+ function toPersistedTarget(selection) {
64033
64424
  if (!selection.id && !selection.selector) return null;
64034
64425
  return {
64035
64426
  sourceFile: selection.sourceFile || void 0,
@@ -64038,11 +64429,78 @@ function toPersistedSelection(selection) {
64038
64429
  selectorIndex: selection.selectorIndex ?? void 0
64039
64430
  };
64040
64431
  }
64432
+ function selectionTargetKey(selection) {
64433
+ return [
64434
+ selection.sourceFile ?? "",
64435
+ selection.id ?? "",
64436
+ selection.selector ?? "",
64437
+ selection.selectorIndex ?? ""
64438
+ ].join("|");
64439
+ }
64440
+ function toPersistedSelection(selection, group = []) {
64441
+ if (!selection) return null;
64442
+ const primary = toPersistedTarget(selection);
64443
+ if (!primary) return null;
64444
+ const primaryKey = selectionTargetKey(primary);
64445
+ const members = /* @__PURE__ */ new Map();
64446
+ for (const member of group) {
64447
+ const target = toPersistedTarget(member);
64448
+ if (!target) continue;
64449
+ const key2 = selectionTargetKey(target);
64450
+ if (key2 !== primaryKey) members.set(key2, target);
64451
+ }
64452
+ return {
64453
+ ...primary,
64454
+ group: members.size > 0 ? [...members.values()] : void 0
64455
+ };
64456
+ }
64041
64457
  function replaceHash(nextHash) {
64042
64458
  if (typeof window === "undefined") return;
64043
64459
  if (window.location.hash === nextHash) return;
64044
64460
  window.history.replaceState(null, "", nextHash);
64045
64461
  }
64462
+ function findUrlSelectionElement(doc, target, fallbackSourceFile, activeCompPath) {
64463
+ return findElementForSelection(
64464
+ doc,
64465
+ {
64466
+ sourceFile: target.sourceFile ?? fallbackSourceFile,
64467
+ id: target.id,
64468
+ selector: target.selector,
64469
+ selectorIndex: target.selectorIndex
64470
+ },
64471
+ activeCompPath
64472
+ );
64473
+ }
64474
+ async function buildOptionalDomSelection(element, buildDomSelection) {
64475
+ if (!element) return null;
64476
+ return buildDomSelection(element, { preferClipAncestor: false });
64477
+ }
64478
+ async function resolveUrlSelections({
64479
+ doc,
64480
+ primaryElement,
64481
+ selection,
64482
+ group,
64483
+ activeCompPath,
64484
+ isCurrent,
64485
+ buildDomSelection
64486
+ }) {
64487
+ const primary = await buildDomSelection(primaryElement, { preferClipAncestor: false });
64488
+ if (!isCurrent()) return null;
64489
+ if (!primary) return [];
64490
+ const members = [primary];
64491
+ for (const member of group) {
64492
+ const element = findUrlSelectionElement(
64493
+ doc,
64494
+ member,
64495
+ selection.sourceFile ?? "",
64496
+ activeCompPath
64497
+ );
64498
+ const resolved = await buildOptionalDomSelection(element, buildDomSelection);
64499
+ if (!isCurrent()) return null;
64500
+ if (resolved) members.push(resolved);
64501
+ }
64502
+ return members;
64503
+ }
64046
64504
  function useStudioUrlState({
64047
64505
  projectId,
64048
64506
  activeCompPath,
@@ -64055,6 +64513,8 @@ function useStudioUrlState({
64055
64513
  rightCollapsed,
64056
64514
  activeCompPathHydrated,
64057
64515
  domEditSelection,
64516
+ domEditGroupSelections,
64517
+ applyMarqueeSelection,
64058
64518
  buildDomSelectionFromTarget,
64059
64519
  applyDomSelection,
64060
64520
  setRightPanelTab,
@@ -64067,6 +64527,7 @@ function useStudioUrlState({
64067
64527
  const [selectionHydrated, setSelectionHydrated] = useState97(initialState2.selection == null);
64068
64528
  const pendingSelectionRef = useRef120(initialState2.selection);
64069
64529
  const stableTimeRef = useRef120(initialState2.currentTime);
64530
+ const selectionApplySeqRef = useRef120(0);
64070
64531
  const buildUrlState = useCallback117(
64071
64532
  () => ({
64072
64533
  activeCompPath,
@@ -64074,12 +64535,13 @@ function useStudioUrlState({
64074
64535
  rightPanelTab,
64075
64536
  rightCollapsed,
64076
64537
  timelineVisible: null,
64077
- selection: hydratedSelectionRef.current ? toPersistedSelection(domEditSelection) : pendingSelectionRef.current
64538
+ selection: hydratedSelectionRef.current ? toPersistedSelection(domEditSelection, domEditGroupSelections) : pendingSelectionRef.current
64078
64539
  }),
64079
- [activeCompPath, domEditSelection, rightCollapsed, rightPanelTab]
64540
+ [activeCompPath, domEditGroupSelections, domEditSelection, rightCollapsed, rightPanelTab]
64080
64541
  );
64081
64542
  const applyUrlSelection = useCallback117(
64082
64543
  (selection) => {
64544
+ const applySeq = ++selectionApplySeqRef.current;
64083
64545
  if (!selection) {
64084
64546
  applyDomSelection(null, { revealPanel: false });
64085
64547
  return true;
@@ -64105,12 +64567,31 @@ function useStudioUrlState({
64105
64567
  applyDomSelection(null, { revealPanel: false });
64106
64568
  return true;
64107
64569
  }
64108
- void buildDomSelectionFromTarget(element, { preferClipAncestor: false }).then((resolved) => {
64109
- applyDomSelection(resolved, { revealPanel: false });
64570
+ const group = selection.group ?? [];
64571
+ void resolveUrlSelections({
64572
+ doc,
64573
+ primaryElement: element,
64574
+ selection,
64575
+ group,
64576
+ activeCompPath,
64577
+ isCurrent: () => applySeq === selectionApplySeqRef.current,
64578
+ buildDomSelection: buildDomSelectionFromTarget
64579
+ }).then((members) => {
64580
+ if (!members) return;
64581
+ const primary = members[0];
64582
+ if (!primary) return applyDomSelection(null, { revealPanel: false });
64583
+ if (group.length === 0) return applyDomSelection(primary, { revealPanel: false });
64584
+ applyMarqueeSelection(members, false);
64110
64585
  });
64111
64586
  return true;
64112
64587
  },
64113
- [activeCompPath, applyDomSelection, buildDomSelectionFromTarget, previewIframeRef]
64588
+ [
64589
+ activeCompPath,
64590
+ applyDomSelection,
64591
+ applyMarqueeSelection,
64592
+ buildDomSelectionFromTarget,
64593
+ previewIframeRef
64594
+ ]
64114
64595
  );
64115
64596
  useEffect92(() => {
64116
64597
  if (!projectId || hydratedSeekRef.current || compositionLoading) return;
@@ -73055,7 +73536,7 @@ function isTextInput(el) {
73055
73536
  return el.isContentEditable === true;
73056
73537
  }
73057
73538
  function useKeyframeKeyboard({
73058
- enabled: enabled2,
73539
+ enabled,
73059
73540
  onAddKeyframe,
73060
73541
  onDeleteKeyframe,
73061
73542
  onPrevKeyframe,
@@ -73066,7 +73547,7 @@ function useKeyframeKeyboard({
73066
73547
  }) {
73067
73548
  const handler = useCallback146(
73068
73549
  (e) => {
73069
- if (!enabled2) return;
73550
+ if (!enabled) return;
73070
73551
  if (isTextInput(document.activeElement)) return;
73071
73552
  if (e.metaKey || e.ctrlKey) return;
73072
73553
  const hasSelectedKeyframes = usePlayerStore.getState().selectedKeyframes.size > 0;
@@ -73105,7 +73586,7 @@ function useKeyframeKeyboard({
73105
73586
  }
73106
73587
  },
73107
73588
  [
73108
- enabled2,
73589
+ enabled,
73109
73590
  onAddKeyframe,
73110
73591
  onDeleteKeyframe,
73111
73592
  onPrevKeyframe,
@@ -73116,10 +73597,10 @@ function useKeyframeKeyboard({
73116
73597
  ]
73117
73598
  );
73118
73599
  useEffect110(() => {
73119
- if (!enabled2) return;
73600
+ if (!enabled) return;
73120
73601
  window.addEventListener("keydown", handler, { capture: true });
73121
73602
  return () => window.removeEventListener("keydown", handler, { capture: true });
73122
- }, [enabled2, handler]);
73603
+ }, [enabled, handler]);
73123
73604
  }
73124
73605
 
73125
73606
  // src/components/TimelineToolbar.tsx
@@ -75094,6 +75575,7 @@ function useTimelineAddAtPlayhead(addAsset, addComposition) {
75094
75575
 
75095
75576
  // src/App.tsx
75096
75577
  import { jsx as jsx184, jsxs as jsxs153 } from "react/jsx-runtime";
75578
+ var getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
75097
75579
  function StudioApp() {
75098
75580
  const { projectId, resolving, waitingForServer } = useServerConnection();
75099
75581
  const initialUrlStateRef = useRef142(readStudioUrlStateFromWindow());
@@ -75119,9 +75601,9 @@ function StudioApp() {
75119
75601
  const captionSync = useCaptionSync(projectId);
75120
75602
  const timelineElements = usePlayerStore((s) => s.elements);
75121
75603
  const setSelectedTimelineElementId = usePlayerStore((s) => s.setSelectedElementId);
75604
+ const setTimelineSelectionSet = usePlayerStore((s) => s.setSelectedElementIds);
75122
75605
  const timelineDuration2 = usePlayerStore((s) => s.duration);
75123
75606
  const isPlaying = usePlayerStore((s) => s.isPlaying);
75124
- const isMasterView = !activeCompPath || activeCompPath === "index.html";
75125
75607
  const effectiveTimelineDuration = useMemo66(() => {
75126
75608
  const maxEnd = timelineElements.length > 0 ? Math.max(...timelineElements.map((el) => el.start + el.duration)) : 0;
75127
75609
  return Math.max(timelineDuration2, maxEnd);
@@ -75299,13 +75781,14 @@ function StudioApp() {
75299
75781
  const domEditSession = useDomEditSession({
75300
75782
  projectId,
75301
75783
  activeCompPath,
75302
- isMasterView,
75303
75784
  compIdToSrc,
75304
75785
  captionEditMode,
75305
75786
  compositionLoading,
75306
75787
  previewIframeRef,
75307
75788
  timelineElements,
75789
+ getTimelineSelectionSet,
75308
75790
  setSelectedTimelineElementId,
75791
+ setTimelineSelectionSet,
75309
75792
  setRightCollapsed: panelLayout.setRightCollapsed,
75310
75793
  setRightPanelTab: panelLayout.setRightPanelTab,
75311
75794
  showToast,
@@ -75444,6 +75927,8 @@ function StudioApp() {
75444
75927
  rightCollapsed: panelLayout.rightCollapsed,
75445
75928
  activeCompPathHydrated,
75446
75929
  domEditSelection: domEditSession.domEditSelection,
75930
+ domEditGroupSelections: domEditSession.domEditGroupSelections,
75931
+ applyMarqueeSelection: domEditSession.applyMarqueeSelection,
75447
75932
  buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
75448
75933
  applyDomSelection: domEditSession.applyDomSelection,
75449
75934
  setRightPanelTab: panelLayout.setRightPanelTab,