@hyperframes/studio 0.8.20 → 0.8.22

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 (37) hide show
  1. package/dist/assets/hyperframes-player-DmtkkaPN.js +460 -0
  2. package/dist/assets/{index-CBSKXKAr.js → index-BIf0zQNt.js} +164 -164
  3. package/dist/assets/{index-Cehosfnu.js → index-DXR3nsh2.js} +1 -1
  4. package/dist/assets/{index-BL1zYTW0.js → index-DivnGS5G.js} +1 -1
  5. package/dist/index.d.ts +28 -0
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +1170 -227
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/App.tsx +2 -0
  11. package/src/contexts/StudioContext.tsx +10 -0
  12. package/src/hooks/useStudioContextValue.ts +9 -0
  13. package/src/player/hooks/timelineSyncHydration.test.ts +96 -0
  14. package/src/player/hooks/timelineSyncHydration.ts +47 -1
  15. package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
  16. package/src/player/hooks/useExpandedTimelineElements.ts +52 -29
  17. package/src/player/hooks/useTimelineSyncCallbacks.ts +4 -0
  18. package/src/player/store/playerStore.ts +17 -2
  19. package/src/player/store/timelineElement.ts +21 -0
  20. package/src/webmcp/StudioAgentTools.tsx +105 -10
  21. package/src/webmcp/toolResult.ts +1 -1
  22. package/src/webmcp/tools/animationTools.test.ts +244 -0
  23. package/src/webmcp/tools/animationTools.ts +276 -0
  24. package/src/webmcp/tools/contentTools.test.ts +275 -0
  25. package/src/webmcp/tools/contentTools.ts +217 -0
  26. package/src/webmcp/tools/frameTools.test.ts +136 -0
  27. package/src/webmcp/tools/frameTools.ts +133 -0
  28. package/src/webmcp/tools/inspectTools.test.ts +197 -0
  29. package/src/webmcp/tools/inspectTools.ts +208 -0
  30. package/src/webmcp/tools/selectionTools.test.ts +164 -0
  31. package/src/webmcp/tools/selectionTools.ts +154 -0
  32. package/src/webmcp/tools/transformTools.test.ts +179 -0
  33. package/src/webmcp/tools/transformTools.ts +205 -0
  34. package/src/webmcp/useStudioAgentTools.test.tsx +71 -22
  35. package/src/webmcp/useStudioAgentTools.ts +195 -1
  36. package/src/webmcp/webmcpTestUtils.ts +91 -0
  37. package/dist/assets/hyperframes-player-BEKxuimO.js +0 -459
package/dist/index.js CHANGED
@@ -1618,7 +1618,8 @@ function createTimelineResetState() {
1618
1618
  beatPersist: null,
1619
1619
  clipManifest: null,
1620
1620
  clipParentMap: /* @__PURE__ */ new Map(),
1621
- domClipChildren: []
1621
+ domClipChildren: [],
1622
+ subCompositionHostState: /* @__PURE__ */ new Map()
1622
1623
  };
1623
1624
  }
1624
1625
  var usePlayerStore = create2((set, get) => ({
@@ -1744,6 +1745,8 @@ var usePlayerStore = create2((set, get) => ({
1744
1745
  setClipParentMap: (map) => set({ clipParentMap: map }),
1745
1746
  domClipChildren: [],
1746
1747
  setDomClipChildren: (children) => set({ domClipChildren: children }),
1748
+ subCompositionHostState: /* @__PURE__ */ new Map(),
1749
+ setSubCompositionHostState: (state) => set({ subCompositionHostState: state }),
1747
1750
  setIsPlaying: (playing) => {
1748
1751
  if (get().isPlaying === playing) return;
1749
1752
  set({ isPlaying: playing });
@@ -4352,22 +4355,12 @@ function childGroupState(flat, domChild) {
4352
4355
  audioGroupAutomation: source.audioGroupAutomation
4353
4356
  };
4354
4357
  }
4355
- function hostElementState(flat) {
4356
- if (!flat) return {};
4357
- return {
4358
- hidden: flat.hidden,
4359
- timelineLocked: flat.timelineLocked,
4360
- timelineRole: flat.timelineRole,
4361
- // Same reason as the three above: these are read off the host element, which
4362
- // an expanded child is built without. Missing them, an audio child inside a
4363
- // sub-composition reserved no automation height and drew no lanes, while the
4364
- // property panel — reading the live DOM selection rather than this row —
4365
- // still showed the chain and its toggles.
4366
- fxChain: flat.fxChain,
4367
- automation: flat.automation
4368
- };
4369
- }
4370
- function buildChildElements(siblings, display2, editBasis, expandedHostKey, elements, domChildrenById) {
4358
+ function hostElementState(flat, live) {
4359
+ if (!flat) return { ...live };
4360
+ const { hidden, timelineLocked, timelineRole, fxChain, automation } = flat;
4361
+ return { hidden, timelineLocked, timelineRole, fxChain, automation, ...live };
4362
+ }
4363
+ function buildChildElements(siblings, display2, editBasis, expandedHostKey, elements, domChildrenById, hostStateById) {
4371
4364
  const result = [];
4372
4365
  for (const child of siblings) {
4373
4366
  const clamped = clampChildToParent(child, display2.start, display2.end);
@@ -4388,7 +4381,10 @@ function buildChildElements(siblings, display2, editBasis, expandedHostKey, elem
4388
4381
  });
4389
4382
  result.push({
4390
4383
  ...base2,
4391
- ...hostElementState(elements.find((element) => element.key === key2)),
4384
+ ...hostElementState(
4385
+ elements.find((element) => element.key === key2),
4386
+ domId ? hostStateById.get(domId) : void 0
4387
+ ),
4392
4388
  ...childGroupState(
4393
4389
  elements.find((element) => element.key === key2),
4394
4390
  domId ? domChildrenById.get(domId) : void 0
@@ -4448,7 +4444,7 @@ function domSiblingClips(domClipChildren, siblingParentId, host) {
4448
4444
  })
4449
4445
  );
4450
4446
  }
4451
- function buildExpandedElements(elements, manifest, parentMap, topLevelId, siblingParentId, domClipChildren = []) {
4447
+ function buildExpandedElements(elements, manifest, parentMap, topLevelId, siblingParentId, domClipChildren = [], subCompositionHostState = /* @__PURE__ */ new Map()) {
4452
4448
  const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
4453
4449
  if (!topLevelElement) return filterToTopLevel(elements, parentMap);
4454
4450
  const parentHost = manifest.find((c) => c.id === siblingParentId);
@@ -4476,7 +4472,8 @@ function buildExpandedElements(elements, manifest, parentMap, topLevelId, siblin
4476
4472
  editBasis,
4477
4473
  parentKey,
4478
4474
  elements,
4479
- domChildrenById
4475
+ domChildrenById,
4476
+ subCompositionHostState
4480
4477
  );
4481
4478
  if (expanded.length === 0) return filterToTopLevel(elements, parentMap);
4482
4479
  const drillPath = /* @__PURE__ */ new Set();
@@ -4496,6 +4493,7 @@ function useExpandedTimelineElements() {
4496
4493
  const clipManifest = usePlayerStore((s) => s.clipManifest);
4497
4494
  const clipParentMap = usePlayerStore((s) => s.clipParentMap);
4498
4495
  const domClipChildren = usePlayerStore((s) => s.domClipChildren);
4496
+ const subCompositionHostState = usePlayerStore((s) => s.subCompositionHostState);
4499
4497
  const selectedElementId = usePlayerStore((s) => s.selectedElementId);
4500
4498
  const currentTime = usePlayerStore((s) => s.currentTime);
4501
4499
  const { rawId, selectedRawId } = useMemo3(() => {
@@ -4526,9 +4524,18 @@ function useExpandedTimelineElements() {
4526
4524
  clipParentMap,
4527
4525
  topLevel,
4528
4526
  immediateParent,
4529
- domClipChildren
4527
+ domClipChildren,
4528
+ subCompositionHostState
4530
4529
  );
4531
- }, [elements, clipManifest, clipParentMap, domClipChildren, rawId, selectedRawId]);
4530
+ }, [
4531
+ elements,
4532
+ clipManifest,
4533
+ clipParentMap,
4534
+ domClipChildren,
4535
+ subCompositionHostState,
4536
+ rawId,
4537
+ selectedRawId
4538
+ ]);
4532
4539
  }
4533
4540
 
4534
4541
  // src/player/components/timelineTheme.ts
@@ -21204,6 +21211,7 @@ function StudioShellProvider({
21204
21211
  showToast,
21205
21212
  previewIframeRef,
21206
21213
  editHistory,
21214
+ writeBlockedReason,
21207
21215
  handleUndo,
21208
21216
  handleRedo,
21209
21217
  renderQueue,
@@ -21219,6 +21227,7 @@ function StudioShellProvider({
21219
21227
  showToast,
21220
21228
  previewIframeRef,
21221
21229
  editHistory,
21230
+ writeBlockedReason,
21222
21231
  handleUndo,
21223
21232
  handleRedo,
21224
21233
  renderQueue,
@@ -21235,6 +21244,7 @@ function StudioShellProvider({
21235
21244
  setActiveCompPath,
21236
21245
  showToast,
21237
21246
  previewIframeRef,
21247
+ writeBlockedReason,
21238
21248
  handleUndo,
21239
21249
  handleRedo,
21240
21250
  waitForPendingDomEditSaves,
@@ -24843,6 +24853,32 @@ function collectSubCompositionDomChildren(iframeDoc, clips, parentMap) {
24843
24853
  }
24844
24854
  return out;
24845
24855
  }
24856
+ function readSubCompositionHostState(el) {
24857
+ const state = {};
24858
+ if (el.hasAttribute("data-hidden")) state.hidden = true;
24859
+ if (el.hasAttribute("data-timeline-locked")) state.timelineLocked = true;
24860
+ const timelineRole = el.getAttribute("data-timeline-role");
24861
+ if (timelineRole) state.timelineRole = timelineRole;
24862
+ const fxChain = el.getAttribute("data-fx-chain");
24863
+ if (fxChain) state.fxChain = fxChain;
24864
+ const automation = el.getAttribute("data-automation");
24865
+ if (automation) state.automation = automation;
24866
+ return Object.keys(state).length > 0 ? state : null;
24867
+ }
24868
+ function collectSubCompositionHostState(iframeDoc, clips) {
24869
+ const out = /* @__PURE__ */ new Map();
24870
+ if (!iframeDoc) return out;
24871
+ for (const clip of clips) {
24872
+ if (clip.kind !== "composition" || !clip.id) continue;
24873
+ const hostEl = iframeDoc.getElementById(clip.id);
24874
+ if (!hostEl) continue;
24875
+ for (const el of Array.from(hostEl.querySelectorAll("[id]"))) {
24876
+ const state = readSubCompositionHostState(el);
24877
+ if (state) out.set(el.id, state);
24878
+ }
24879
+ }
24880
+ return out;
24881
+ }
24846
24882
  function safeContentDocument(iframe) {
24847
24883
  try {
24848
24884
  return iframe?.contentDocument ?? null;
@@ -25000,6 +25036,7 @@ function useTimelineSyncCallbacks({
25000
25036
  const domClipChildren = collectSubCompositionDomChildren(iframeDoc, data.clips, parentMap);
25001
25037
  usePlayerStore.getState().setClipParentMap(parentMap);
25002
25038
  usePlayerStore.getState().setDomClipChildren(domClipChildren);
25039
+ usePlayerStore.getState().setSubCompositionHostState(collectSubCompositionHostState(iframeDoc, data.clips));
25003
25040
  } catch {
25004
25041
  }
25005
25042
  const els = buildTimelineElementsFromClips(filtered, iframeDoc);
@@ -40584,7 +40621,7 @@ function useTimelineSelectionPreviewSync({
40584
40621
  }
40585
40622
 
40586
40623
  // src/webmcp/StudioAgentTools.tsx
40587
- import { useCallback as useCallback60 } from "react";
40624
+ import { useCallback as useCallback60, useMemo as useMemo37 } from "react";
40588
40625
 
40589
40626
  // src/webmcp/useStudioAgentTools.ts
40590
40627
  import { useEffect as useEffect49, useRef as useRef68 } from "react";
@@ -40731,6 +40768,43 @@ function patchTargetAddress(target) {
40731
40768
  selectorIndex: target.selectorIndex
40732
40769
  };
40733
40770
  }
40771
+ function parseElementHandle(handle) {
40772
+ const separatorAt = handle.indexOf(SEPARATOR);
40773
+ if (separatorAt <= 0) return null;
40774
+ const scheme = handle.slice(0, separatorAt);
40775
+ const rest = handle.slice(separatorAt + 1);
40776
+ if (!rest) return null;
40777
+ if (scheme === "hf" || scheme === "dom") return { scheme, value: rest, index: 0 };
40778
+ if (scheme !== "sel") return null;
40779
+ const indexAt = rest.lastIndexOf(INDEX_SEPARATOR);
40780
+ if (indexAt <= 0) return { scheme, value: rest, index: 0 };
40781
+ const index = Number(rest.slice(indexAt + 1));
40782
+ if (!Number.isInteger(index) || index < 0) return { scheme, value: rest, index: 0 };
40783
+ return { scheme, value: rest.slice(0, indexAt), index };
40784
+ }
40785
+ function resolveElementHandle(doc, handle) {
40786
+ const parsed = parseElementHandle(handle);
40787
+ if (!parsed) return null;
40788
+ if (parsed.scheme === "dom") return asHtmlElement(doc, doc.getElementById(parsed.value));
40789
+ if (parsed.scheme === "hf") {
40790
+ return asHtmlElement(doc, doc.querySelector(`[data-hf-id="${cssEscape(parsed.value)}"]`));
40791
+ }
40792
+ let matches;
40793
+ try {
40794
+ matches = doc.querySelectorAll(parsed.value);
40795
+ } catch {
40796
+ return null;
40797
+ }
40798
+ return asHtmlElement(doc, matches.item(parsed.index));
40799
+ }
40800
+ function cssEscape(value) {
40801
+ return typeof CSS?.escape === "function" ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
40802
+ }
40803
+ function asHtmlElement(doc, node) {
40804
+ if (!node) return null;
40805
+ const ctor = doc.defaultView?.HTMLElement;
40806
+ return ctor && node instanceof ctor ? node : null;
40807
+ }
40734
40808
 
40735
40809
  // src/webmcp/tools/lookTools.ts
40736
40810
  var DEFAULT_LIMIT = 200;
@@ -40816,6 +40890,744 @@ var STUDIO_LOOK_DESCRIPTION = [
40816
40890
  "you did not do, a human pressed undo and your earlier edits are gone."
40817
40891
  ].join(" ");
40818
40892
 
40893
+ // src/webmcp/tools/selectionTools.ts
40894
+ async function studioSelect(deps, handle) {
40895
+ if (typeof handle !== "string" || !handle.trim()) {
40896
+ return toolFailure("invalid", "handle must be a non-empty string", "Call studio_look first.");
40897
+ }
40898
+ const doc = deps.getPreviewDocument();
40899
+ if (!doc) {
40900
+ return toolFailure(
40901
+ "blocked",
40902
+ "the preview is not mounted yet",
40903
+ "Wait for the composition to load, then retry."
40904
+ );
40905
+ }
40906
+ const element = resolveElementHandle(doc, handle);
40907
+ if (!element) {
40908
+ return toolFailure(
40909
+ "invalid",
40910
+ `no element matches handle ${handle}`,
40911
+ "The composition may have changed. Call studio_look for current handles."
40912
+ );
40913
+ }
40914
+ const selection = await deps.buildSelection(element);
40915
+ if (!selection) {
40916
+ return toolFailure(
40917
+ "blocked",
40918
+ `${handle} resolved to an element Studio cannot select`,
40919
+ "Try a parent or child element from studio_look."
40920
+ );
40921
+ }
40922
+ deps.applySelection(selection);
40923
+ return toolOk({
40924
+ handle: mintElementHandle(patchTargetAddress(selection)),
40925
+ label: selection.label,
40926
+ tagName: selection.tagName,
40927
+ box: selection.boundingBox
40928
+ });
40929
+ }
40930
+ function studioSeek(deps, time) {
40931
+ if (typeof time !== "number" || !Number.isFinite(time)) {
40932
+ return toolFailure("invalid", "time must be a finite number of seconds");
40933
+ }
40934
+ const before = deps.readPlayhead();
40935
+ deps.requestSeek(time);
40936
+ const after = deps.readPlayhead();
40937
+ const moved = after.currentTime !== before.currentTime;
40938
+ if (!moved && before.currentTime !== time) {
40939
+ return toolFailure(
40940
+ "blocked",
40941
+ `the playhead did not move; it is still at ${after.currentTime}`,
40942
+ "The preview may not be ready. Check studio_look, then retry."
40943
+ );
40944
+ }
40945
+ return toolOk({
40946
+ playhead: after.currentTime,
40947
+ duration: after.duration,
40948
+ isPlaying: after.isPlaying,
40949
+ moved
40950
+ });
40951
+ }
40952
+ var STUDIO_SELECT_INPUT_SCHEMA = {
40953
+ type: "object",
40954
+ properties: {
40955
+ handle: { type: "string", description: "An element handle from studio_look." }
40956
+ },
40957
+ required: ["handle"],
40958
+ additionalProperties: false
40959
+ };
40960
+ var STUDIO_SELECT_DESCRIPTION = [
40961
+ "Select an element in HyperFrames Studio, exactly as clicking it would:",
40962
+ "the human sees the same selection box and inspector.",
40963
+ "Takes a handle from studio_look. Most editing tools act on the CURRENT selection,",
40964
+ "so call this first, then the edit.",
40965
+ "Returns `ok: true` with the resulting selection, or `ok: false` with `kind`, `reason` and a `hint`."
40966
+ ].join(" ");
40967
+ var STUDIO_SEEK_INPUT_SCHEMA = {
40968
+ type: "object",
40969
+ properties: {
40970
+ time: { type: "number", minimum: 0, description: "Playhead position in seconds." }
40971
+ },
40972
+ required: ["time"],
40973
+ additionalProperties: false
40974
+ };
40975
+ var STUDIO_SEEK_DESCRIPTION = [
40976
+ "Move the playhead to a time in seconds. Pauses playback.",
40977
+ "Out-of-range times are clamped by the player, so check the returned `playhead`",
40978
+ "for where it actually landed rather than assuming it matched your request.",
40979
+ "Returns `ok: true`, or `ok: false` with `kind`, `reason` and a `hint`."
40980
+ ].join(" ");
40981
+
40982
+ // src/utils/projectRouting.ts
40983
+ var PROJECT_HASH_PREFIX = "#project/";
40984
+ function decodeHashProjectId(value) {
40985
+ try {
40986
+ return decodeURIComponent(value);
40987
+ } catch {
40988
+ return value;
40989
+ }
40990
+ }
40991
+ function normalizeHashParams(params) {
40992
+ if (!params) return new URLSearchParams();
40993
+ if (params instanceof URLSearchParams) return params;
40994
+ const next = new URLSearchParams();
40995
+ for (const [key2, value] of Object.entries(params)) {
40996
+ if (!key2 || value == null || value === "") continue;
40997
+ next.set(key2, value);
40998
+ }
40999
+ return next;
41000
+ }
41001
+ function encodeProjectId(projectId) {
41002
+ return encodeURIComponent(projectId);
41003
+ }
41004
+ function buildProjectHash(projectId, params) {
41005
+ const search = normalizeHashParams(params).toString();
41006
+ return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : ""}`;
41007
+ }
41008
+ function parseProjectHashRoute(hash) {
41009
+ if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;
41010
+ const route = hash.slice(PROJECT_HASH_PREFIX.length);
41011
+ const queryIndex = route.indexOf("?");
41012
+ const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;
41013
+ if (!encodedProjectId || encodedProjectId.includes("/")) return null;
41014
+ const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : "";
41015
+ return {
41016
+ projectId: decodeHashProjectId(encodedProjectId),
41017
+ params: new URLSearchParams(rawParams)
41018
+ };
41019
+ }
41020
+ function parseProjectIdFromHash(hash) {
41021
+ return parseProjectHashRoute(hash)?.projectId ?? null;
41022
+ }
41023
+ function buildProjectApiPath(projectId, suffix = "") {
41024
+ const normalizedSuffix = suffix && !suffix.startsWith("/") ? `/${suffix}` : suffix;
41025
+ return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;
41026
+ }
41027
+
41028
+ // src/utils/frameCapture.ts
41029
+ function normalizeCompositionPath(compositionPath) {
41030
+ return compositionPath && compositionPath !== "master" ? compositionPath : "index.html";
41031
+ }
41032
+ function buildFrameCaptureUrl({
41033
+ projectId,
41034
+ compositionPath,
41035
+ currentTime,
41036
+ origin = window.location.origin
41037
+ }) {
41038
+ const compPath = normalizeCompositionPath(compositionPath);
41039
+ const url = new URL(
41040
+ buildProjectApiPath(projectId, `/thumbnail/${encodeURIComponent(compPath)}`),
41041
+ origin
41042
+ );
41043
+ url.searchParams.set("t", Math.max(0, currentTime).toFixed(3));
41044
+ url.searchParams.set("format", "png");
41045
+ url.searchParams.set("output", "source");
41046
+ url.searchParams.set("v", String(Date.now()));
41047
+ return url.toString();
41048
+ }
41049
+ function buildFrameCaptureFilename(compositionPath, currentTime) {
41050
+ const compPath = normalizeCompositionPath(compositionPath);
41051
+ const base2 = compPath.split("/").pop()?.replace(/\.html$/i, "") || "frame";
41052
+ const frameTime = Math.max(0, currentTime).toFixed(3).replace(".", "-");
41053
+ return `${base2}-${frameTime}s.png`;
41054
+ }
41055
+
41056
+ // src/webmcp/tools/frameTools.ts
41057
+ var DEFAULT_SETTLE_MS = 150;
41058
+ var MAX_SETTLE_MS = 5e3;
41059
+ async function studioFrame(deps, input = {}) {
41060
+ const projectId = deps.getProjectId();
41061
+ if (!projectId) {
41062
+ return toolFailure("blocked", "no project is open");
41063
+ }
41064
+ if (input.time !== void 0) {
41065
+ if (typeof input.time !== "number" || !Number.isFinite(input.time) || input.time < 0) {
41066
+ return toolFailure("invalid", "time must be a non-negative, finite number of seconds");
41067
+ }
41068
+ deps.requestSeek(input.time);
41069
+ }
41070
+ const settledMs = clampSettle(input.settleMs);
41071
+ if (settledMs > 0) await deps.wait(settledMs);
41072
+ const { currentTime } = deps.readPlayhead();
41073
+ const compositionPath = deps.getCompositionPath();
41074
+ const url = buildFrameCaptureUrl({ projectId, compositionPath, currentTime });
41075
+ const probe2 = await deps.probeFrame(url);
41076
+ if (!probe2.ok) {
41077
+ return toolFailure(
41078
+ "failed",
41079
+ `the renderer returned ${probe2.status} for this frame`,
41080
+ "The composition may not build. Try `hyperframes check`."
41081
+ );
41082
+ }
41083
+ return toolOk({
41084
+ url,
41085
+ time: currentTime,
41086
+ compositionPath: compositionPath ?? "index.html",
41087
+ settledMs
41088
+ });
41089
+ }
41090
+ function clampSettle(requested) {
41091
+ if (requested === void 0) return DEFAULT_SETTLE_MS;
41092
+ if (typeof requested !== "number" || !Number.isFinite(requested) || requested < 0) {
41093
+ return DEFAULT_SETTLE_MS;
41094
+ }
41095
+ return Math.min(requested, MAX_SETTLE_MS);
41096
+ }
41097
+ var STUDIO_FRAME_INPUT_SCHEMA = {
41098
+ type: "object",
41099
+ properties: {
41100
+ time: {
41101
+ type: "number",
41102
+ minimum: 0,
41103
+ description: "Seconds. Omit to capture wherever the playhead already is."
41104
+ },
41105
+ settleMs: {
41106
+ type: "integer",
41107
+ minimum: 0,
41108
+ maximum: MAX_SETTLE_MS,
41109
+ description: `Wait this long before capturing so a just-made edit is included. Default ${DEFAULT_SETTLE_MS}.`
41110
+ }
41111
+ },
41112
+ additionalProperties: false
41113
+ };
41114
+ var STUDIO_FRAME_DESCRIPTION = [
41115
+ "Render the composition to a PNG at a given time and return its URL, so you can",
41116
+ "SEE the result instead of inferring it from source. Use this to judge a change:",
41117
+ "edit, capture the instant it affects, look, adjust.",
41118
+ "The frame is rendered from the file on disk, not the live preview.",
41119
+ "A capture taken immediately after an edit can therefore predate that edit, because",
41120
+ "the render cache is cleared by a file watcher. The tool waits briefly to cover that;",
41121
+ "raise `settleMs` if a frame still looks stale, rather than concluding the edit failed.",
41122
+ "Returns `ok: true` with `url` and the `time` actually captured, or `ok: false`."
41123
+ ].join(" ");
41124
+
41125
+ // src/webmcp/tools/inspectTools.ts
41126
+ function describeAnimation(animation) {
41127
+ return {
41128
+ animationId: animation.id,
41129
+ method: animation.method,
41130
+ target: animation.targetSelector,
41131
+ position: animation.position,
41132
+ duration: animation.duration ?? null,
41133
+ ease: animation.ease ?? null,
41134
+ properties: animation.properties,
41135
+ hasKeyframes: animation.keyframes !== void 0,
41136
+ hasArcPath: animation.arcPath !== void 0
41137
+ };
41138
+ }
41139
+ function describe(selection, deps, isCurrentSelection) {
41140
+ const { capabilities } = selection;
41141
+ const gsap3 = deps.getGsapDiagnostics();
41142
+ const animations = isCurrentSelection ? gsap3.animations.map(describeAnimation) : [];
41143
+ let animationEditingBlocked = null;
41144
+ if (!isCurrentSelection) {
41145
+ animationEditingBlocked = "animations are only readable for the current selection";
41146
+ } else if (gsap3.multipleTimelines) {
41147
+ animationEditingBlocked = "this composition has multiple GSAP timelines";
41148
+ } else if (gsap3.unsupportedTimelinePattern) {
41149
+ animationEditingBlocked = "this composition's timeline pattern is not editable by Studio";
41150
+ }
41151
+ return toolOk({
41152
+ handle: mintElementHandle(patchTargetAddress(selection)),
41153
+ label: selection.label,
41154
+ tagName: selection.tagName,
41155
+ sourceFile: selection.sourceFile,
41156
+ box: selection.boundingBox,
41157
+ text: selection.textContent,
41158
+ textFields: selection.textFields.map((field) => ({
41159
+ key: field.key,
41160
+ label: field.label,
41161
+ value: field.value,
41162
+ tagName: field.tagName
41163
+ })),
41164
+ styles: selection.computedStyles,
41165
+ inlineStyles: selection.inlineStyles,
41166
+ dataAttributes: selection.dataAttributes,
41167
+ can: {
41168
+ editStyles: capabilities.canEditStyles,
41169
+ move: capabilities.canMove || capabilities.canApplyManualOffset,
41170
+ resize: capabilities.canResize || capabilities.canApplyManualSize,
41171
+ rotate: capabilities.canApplyManualRotation,
41172
+ crop: capabilities.canCrop,
41173
+ editText: selection.textFields.length > 0,
41174
+ reasonIfDisabled: capabilities.reasonIfDisabled ?? null
41175
+ },
41176
+ animations,
41177
+ animationEditingBlocked,
41178
+ isCurrentSelection
41179
+ });
41180
+ }
41181
+ async function studioInspect(deps, input = {}) {
41182
+ const current = deps.getCurrentSelection();
41183
+ if (!input.handle) {
41184
+ if (!current) {
41185
+ return toolFailure(
41186
+ "invalid",
41187
+ "nothing is selected and no handle was given",
41188
+ "Pass a handle from studio_look, or call studio_select first."
41189
+ );
41190
+ }
41191
+ return describe(current, deps, true);
41192
+ }
41193
+ const doc = deps.getPreviewDocument();
41194
+ if (!doc) return toolFailure("blocked", "the preview is not mounted yet");
41195
+ const element = resolveElementHandle(doc, input.handle);
41196
+ if (!element) {
41197
+ return toolFailure(
41198
+ "invalid",
41199
+ `no element matches handle ${input.handle}`,
41200
+ "Call studio_look for current handles."
41201
+ );
41202
+ }
41203
+ const selection = await deps.buildSelection(element);
41204
+ if (!selection) {
41205
+ return toolFailure("blocked", `${input.handle} resolved to an element Studio cannot inspect`);
41206
+ }
41207
+ return describe(selection, deps, current?.element === element);
41208
+ }
41209
+ var STUDIO_INSPECT_INPUT_SCHEMA = {
41210
+ type: "object",
41211
+ properties: {
41212
+ handle: {
41213
+ type: "string",
41214
+ description: "An element handle from studio_look. Omit to inspect the current selection."
41215
+ }
41216
+ },
41217
+ additionalProperties: false
41218
+ };
41219
+ var STUDIO_INSPECT_DESCRIPTION = [
41220
+ "Everything about one element: its resolved styles, its text fields, its box,",
41221
+ "its GSAP animations, and crucially what it will and will not accept.",
41222
+ "Read this BEFORE editing. `can` tells you which edits are possible and",
41223
+ "`can.reasonIfDisabled` says why one is not, so you can avoid a write that would be refused.",
41224
+ "Animations are only readable for the CURRENT selection; `animationEditingBlocked` says when",
41225
+ "and why animation editing is unavailable.",
41226
+ "Returns `ok: true`, or `ok: false` with `kind`, `reason` and a `hint`."
41227
+ ].join(" ");
41228
+
41229
+ // src/webmcp/tools/contentTools.ts
41230
+ var DECLINE_HINTS = {
41231
+ "no-selection": { kind: "invalid", hint: "Call studio_select first." },
41232
+ "no-project": { kind: "blocked" },
41233
+ "geometry-property": {
41234
+ kind: "blocked",
41235
+ hint: "Position and size are not editable as styles. Use the transform tools."
41236
+ },
41237
+ "styles-not-editable": {
41238
+ kind: "blocked",
41239
+ hint: "studio_inspect reports why, in can.reasonIfDisabled."
41240
+ },
41241
+ "not-text-editable": {
41242
+ kind: "blocked",
41243
+ hint: "This element has no editable text. studio_inspect lists its textFields."
41244
+ },
41245
+ "persist-failed": { kind: "failed", hint: "The write did not reach the file. Check Studio." }
41246
+ };
41247
+ function fromOutcome(outcome, what) {
41248
+ if (outcome.ok) return null;
41249
+ const mapped = DECLINE_HINTS[outcome.reason] ?? { kind: "failed" };
41250
+ return toolFailure(mapped.kind, `${what} was not applied: ${outcome.reason}`, mapped.hint);
41251
+ }
41252
+ function guardWrite(deps) {
41253
+ const blocked = deps.getWriteBlockedReason();
41254
+ if (blocked) {
41255
+ return toolFailure("blocked", blocked, "Resolve it in Studio, then retry.");
41256
+ }
41257
+ if (!deps.getCurrentSelection()) {
41258
+ return toolFailure("invalid", "nothing is selected", "Call studio_select first.");
41259
+ }
41260
+ return null;
41261
+ }
41262
+ async function studioSetText(deps, input) {
41263
+ if (typeof input.text !== "string") {
41264
+ return toolFailure("invalid", "text must be a string");
41265
+ }
41266
+ const blocked = guardWrite(deps);
41267
+ if (blocked) return blocked;
41268
+ const selection = deps.getCurrentSelection();
41269
+ if (!selection) return toolFailure("invalid", "nothing is selected");
41270
+ const fields = selection.textFields;
41271
+ const requested = typeof input.field === "string" && input.field ? input.field : void 0;
41272
+ if (requested && !fields.some((candidate) => candidate.key === requested)) {
41273
+ return toolFailure(
41274
+ "invalid",
41275
+ `this element has no text field "${requested}"`,
41276
+ `Its fields are: ${fields.map((candidate) => candidate.key).join(", ") || "none"}.`
41277
+ );
41278
+ }
41279
+ const field = requested ?? (fields.length === 1 ? fields[0]?.key : void 0);
41280
+ if (!field) {
41281
+ if (fields.length === 0) {
41282
+ return toolFailure(
41283
+ "blocked",
41284
+ "this element has no editable text field",
41285
+ "studio_inspect lists an element's textFields."
41286
+ );
41287
+ }
41288
+ return toolFailure(
41289
+ "invalid",
41290
+ `this element has ${fields.length} text fields, so one must be named`,
41291
+ `Pass field as one of: ${fields.map((candidate) => candidate.key).join(", ")}.`
41292
+ );
41293
+ }
41294
+ const before = selection.textContent ?? null;
41295
+ const outcome = await deps.setText(input.text, field);
41296
+ const failure = fromOutcome(outcome, "the text");
41297
+ if (failure) return failure;
41298
+ return toolOk({ text: input.text, changed: before !== input.text });
41299
+ }
41300
+ async function studioSetStyle(deps, input) {
41301
+ const styles = input.styles;
41302
+ if (typeof styles !== "object" || styles === null || Array.isArray(styles)) {
41303
+ return toolFailure("invalid", "styles must be an object of CSS property to value");
41304
+ }
41305
+ const entries = Object.entries(styles).filter(
41306
+ (entry2) => typeof entry2[1] === "string"
41307
+ );
41308
+ if (entries.length === 0) {
41309
+ return toolFailure("invalid", "styles must contain at least one string value");
41310
+ }
41311
+ const blocked = guardWrite(deps);
41312
+ if (blocked) return blocked;
41313
+ const applied = {};
41314
+ const rejected = {};
41315
+ for (const [property, value] of entries) {
41316
+ const outcome = await deps.setStyle(property, value);
41317
+ if (outcome.ok) applied[property] = value;
41318
+ else rejected[property] = outcome.reason;
41319
+ }
41320
+ if (Object.keys(applied).length === 0) {
41321
+ const reasons = Object.entries(rejected).map(([property, reason]) => `${property}: ${reason}`).join(", ");
41322
+ return toolFailure("blocked", `no style was applied (${reasons})`);
41323
+ }
41324
+ return toolOk({ applied, rejected });
41325
+ }
41326
+ var STUDIO_SET_TEXT_INPUT_SCHEMA = {
41327
+ type: "object",
41328
+ properties: {
41329
+ text: { type: "string", description: "The new text content." },
41330
+ field: {
41331
+ type: "string",
41332
+ description: "Which text field to write, from studio_inspect. Omit for the element's own text."
41333
+ }
41334
+ },
41335
+ required: ["text"],
41336
+ additionalProperties: false
41337
+ };
41338
+ var STUDIO_SET_TEXT_DESCRIPTION = [
41339
+ "Set the text of the CURRENTLY SELECTED element. Call studio_select first.",
41340
+ "This is the edit a synthetic double-click cannot reach, because Studio's canvas",
41341
+ "takes pointer capture and recognises the double press itself.",
41342
+ "Returns `ok: true` with the resulting text and whether it changed, or `ok: false`",
41343
+ "with `kind`, `reason` and usually a `hint` naming what to do instead."
41344
+ ].join(" ");
41345
+ var STUDIO_SET_STYLE_INPUT_SCHEMA = {
41346
+ type: "object",
41347
+ properties: {
41348
+ styles: {
41349
+ type: "object",
41350
+ description: 'CSS property to value, for example {"color": "red", "font-size": "48px"}.',
41351
+ additionalProperties: { type: "string" }
41352
+ }
41353
+ },
41354
+ required: ["styles"],
41355
+ additionalProperties: false
41356
+ };
41357
+ var STUDIO_SET_STYLE_DESCRIPTION = [
41358
+ "Set inline styles on the CURRENTLY SELECTED element. Call studio_select first.",
41359
+ "Each property is a separate commit, so N properties produce N undo entries.",
41360
+ "Position and size properties (left, top, width, height) are refused here on purpose;",
41361
+ "they belong to the transform tools.",
41362
+ "Returns `ok: true` with `applied` and `rejected` maps, so a partial success is visible",
41363
+ "as a partial success rather than reported as a whole one."
41364
+ ].join(" ");
41365
+
41366
+ // src/webmcp/tools/transformTools.ts
41367
+ var NO_OP_HINT = "Move and rotate are written as GSAP code; a composition with no GSAP timeline has nothing to write to. studio_inspect reports the element's animations.";
41368
+ function readNumber(value) {
41369
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
41370
+ }
41371
+ function guard(deps) {
41372
+ const blocked = deps.getWriteBlockedReason();
41373
+ if (blocked) return toolFailure("blocked", blocked, "Resolve it in Studio, then retry.");
41374
+ if (!deps.getCurrentSelection()) {
41375
+ return toolFailure("invalid", "nothing is selected", "Call studio_select first.");
41376
+ }
41377
+ return null;
41378
+ }
41379
+ function parsePair(a, b, names, min = Number.NEGATIVE_INFINITY) {
41380
+ const first = readNumber(a);
41381
+ const second = readNumber(b);
41382
+ if (first === null && second === null) return { pair: null };
41383
+ if (first === null || second === null) {
41384
+ return toolFailure("invalid", `${names[0]} and ${names[1]} must be given together`);
41385
+ }
41386
+ if (first < min || second < min) {
41387
+ return toolFailure("invalid", `${names[0]} and ${names[1]} must be at least ${min}`);
41388
+ }
41389
+ return { pair: [first, second] };
41390
+ }
41391
+ function isFailure(value) {
41392
+ return "ok" in value;
41393
+ }
41394
+ function parseRequest(input) {
41395
+ const move = parsePair(input.x, input.y, ["x", "y"]);
41396
+ if (isFailure(move)) return move;
41397
+ const size = parsePair(input.width, input.height, ["width", "height"], 0);
41398
+ if (isFailure(size)) return size;
41399
+ const rotate2 = readNumber(input.rotate);
41400
+ if (!move.pair && !size.pair && rotate2 === null) {
41401
+ return toolFailure(
41402
+ "invalid",
41403
+ "give at least one of x, y, width, height, rotate as a finite number"
41404
+ );
41405
+ }
41406
+ return {
41407
+ move: move.pair ? { x: move.pair[0], y: move.pair[1] } : null,
41408
+ size: size.pair ? { width: size.pair[0], height: size.pair[1] } : null,
41409
+ rotate: rotate2
41410
+ };
41411
+ }
41412
+ async function studioTransform(deps, input) {
41413
+ const request = parseRequest(input);
41414
+ if (isFailure(request)) return request;
41415
+ const blocked = guard(deps);
41416
+ if (blocked) return blocked;
41417
+ const selection = deps.getCurrentSelection();
41418
+ if (!selection) return toolFailure("invalid", "nothing is selected");
41419
+ const applied = [];
41420
+ const unchanged = {};
41421
+ if (request.size) {
41422
+ const before = deps.readBox(selection);
41423
+ await deps.resizeTo(selection, request.size);
41424
+ const after = deps.readBox(selection);
41425
+ if (after.width !== before.width || after.height !== before.height) applied.push("resize");
41426
+ else unchanged.resize = "the element's size did not change";
41427
+ }
41428
+ if (request.move) {
41429
+ const before = deps.readBox(selection);
41430
+ await deps.moveTo(selection, request.move);
41431
+ const after = deps.readBox(selection);
41432
+ if (after.x !== before.x || after.y !== before.y) applied.push("move");
41433
+ else unchanged.move = `the element did not move. ${NO_OP_HINT}`;
41434
+ }
41435
+ if (request.rotate !== null) {
41436
+ await deps.rotateTo(selection, { angle: request.rotate });
41437
+ applied.push("rotate");
41438
+ }
41439
+ if (applied.length === 0) {
41440
+ return toolFailure(
41441
+ "blocked",
41442
+ `nothing changed: ${Object.values(unchanged).join("; ")}`,
41443
+ NO_OP_HINT
41444
+ );
41445
+ }
41446
+ return toolOk({ box: deps.readBox(selection), applied, unchanged });
41447
+ }
41448
+ var STUDIO_TRANSFORM_INPUT_SCHEMA = {
41449
+ type: "object",
41450
+ properties: {
41451
+ x: { type: "number", description: "New x offset in pixels. Must be paired with y." },
41452
+ y: { type: "number", description: "New y offset in pixels. Must be paired with x." },
41453
+ width: { type: "number", minimum: 0, description: "New width. Must be paired with height." },
41454
+ height: { type: "number", minimum: 0, description: "New height. Must be paired with width." },
41455
+ rotate: { type: "number", description: "Rotation in degrees." }
41456
+ },
41457
+ additionalProperties: false
41458
+ };
41459
+ var STUDIO_TRANSFORM_DESCRIPTION = [
41460
+ "Move, resize or rotate the CURRENTLY SELECTED element, the way a drag would.",
41461
+ "Call studio_select first. Give x with y, and width with height.",
41462
+ "The result's `box` is READ BACK after the write, not echoed from your request, and",
41463
+ "`applied` lists what actually took effect. Check it.",
41464
+ "Move and rotate are written as GSAP code, so in a composition with no GSAP timeline they",
41465
+ "do nothing; that shows up in `unchanged` rather than as a false success.",
41466
+ "Rotation is reported as dispatched rather than verified, because the CSS `rotate` property",
41467
+ "does not appear in the element's computed transform.",
41468
+ "Returns `ok: true`, or `ok: false` with `kind`, `reason` and a `hint`."
41469
+ ].join(" ");
41470
+
41471
+ // src/webmcp/tools/animationTools.ts
41472
+ var METHODS = ["to", "from", "set", "fromTo"];
41473
+ var INSPECT_HINT = "Call studio_inspect to see the result.";
41474
+ function guard2(deps) {
41475
+ const blocked = deps.getWriteBlockedReason();
41476
+ if (blocked) return toolFailure("blocked", blocked, "Resolve it in Studio, then retry.");
41477
+ if (!deps.getCurrentSelection()) {
41478
+ return toolFailure("invalid", "nothing is selected", "Call studio_select first.");
41479
+ }
41480
+ return null;
41481
+ }
41482
+ function readAnimationId(value) {
41483
+ return typeof value === "string" && value.trim() ? value : null;
41484
+ }
41485
+ async function studioAddAnimation(deps, input) {
41486
+ const method = METHODS.find((candidate) => candidate === input.method);
41487
+ if (!method) {
41488
+ return toolFailure("invalid", `method must be one of ${METHODS.join(", ")}`);
41489
+ }
41490
+ const blocked = guard2(deps);
41491
+ if (blocked) return blocked;
41492
+ const { currentTime } = deps.readPlayhead();
41493
+ deps.addAnimation(method);
41494
+ return toolOk({
41495
+ method,
41496
+ insertedAtSeconds: currentTime,
41497
+ dispatched: true
41498
+ });
41499
+ }
41500
+ async function studioUpdateAnimation(deps, input) {
41501
+ const animationId = readAnimationId(input.animationId);
41502
+ if (!animationId) {
41503
+ return toolFailure("invalid", "animationId must be a non-empty string", INSPECT_HINT);
41504
+ }
41505
+ const updates = {};
41506
+ if (typeof input.duration === "number" && Number.isFinite(input.duration)) {
41507
+ if (input.duration < 0) return toolFailure("invalid", "duration must not be negative");
41508
+ updates.duration = input.duration;
41509
+ }
41510
+ if (typeof input.ease === "string" && input.ease.trim()) updates.ease = input.ease;
41511
+ if (typeof input.position === "number" && Number.isFinite(input.position)) {
41512
+ updates.position = input.position;
41513
+ }
41514
+ if (Object.keys(updates).length === 0) {
41515
+ return toolFailure("invalid", "give at least one of duration, ease, position");
41516
+ }
41517
+ const blocked = guard2(deps);
41518
+ if (blocked) return blocked;
41519
+ const landed = await deps.updateAnimation(animationId, updates);
41520
+ if (!landed) {
41521
+ return toolFailure(
41522
+ "failed",
41523
+ `the update to ${animationId} did not land`,
41524
+ "The animation id may be stale. studio_inspect lists the current ones."
41525
+ );
41526
+ }
41527
+ return toolOk({ animationId, updated: updates });
41528
+ }
41529
+ async function studioAddKeyframe(deps, input) {
41530
+ const animationId = readAnimationId(input.animationId);
41531
+ if (!animationId) {
41532
+ return toolFailure("invalid", "animationId must be a non-empty string", INSPECT_HINT);
41533
+ }
41534
+ const percent3 = input.percent;
41535
+ if (typeof percent3 !== "number" || !Number.isFinite(percent3) || percent3 < 0 || percent3 > 100) {
41536
+ return toolFailure("invalid", "percent must be a number between 0 and 100");
41537
+ }
41538
+ const raw = input.properties;
41539
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
41540
+ return toolFailure("invalid", "properties must be an object of GSAP property to value");
41541
+ }
41542
+ const properties = {};
41543
+ for (const [key2, value] of Object.entries(raw)) {
41544
+ if (typeof value === "number" || typeof value === "string") properties[key2] = value;
41545
+ }
41546
+ if (Object.keys(properties).length === 0) {
41547
+ return toolFailure("invalid", "properties must contain at least one number or string value");
41548
+ }
41549
+ const blocked = guard2(deps);
41550
+ if (blocked) return blocked;
41551
+ await deps.addKeyframe(animationId, percent3, properties);
41552
+ return toolOk({ animationId, percent: percent3, properties, dispatched: true });
41553
+ }
41554
+ async function studioDeleteAnimation(deps, input) {
41555
+ const animationId = readAnimationId(input.animationId);
41556
+ if (!animationId) {
41557
+ return toolFailure("invalid", "animationId must be a non-empty string", INSPECT_HINT);
41558
+ }
41559
+ const blocked = guard2(deps);
41560
+ if (blocked) return blocked;
41561
+ deps.deleteAnimation(animationId);
41562
+ return toolOk({ animationId, dispatched: true });
41563
+ }
41564
+ var DISPATCH_CAVEAT = `Reports what was dispatched, not what landed: the handler underneath does not report back. ${INSPECT_HINT}`;
41565
+ var STUDIO_ADD_ANIMATION_INPUT_SCHEMA = {
41566
+ type: "object",
41567
+ properties: {
41568
+ method: { type: "string", enum: METHODS, description: "The GSAP method to add." }
41569
+ },
41570
+ required: ["method"],
41571
+ additionalProperties: false
41572
+ };
41573
+ var STUDIO_ADD_ANIMATION_DESCRIPTION = [
41574
+ "Add a GSAP animation to the CURRENTLY SELECTED element. Call studio_select first.",
41575
+ "It is inserted AT THE PLAYHEAD, which this tool does not control: call studio_seek first",
41576
+ "to choose when it starts. The result reports where the playhead actually was.",
41577
+ DISPATCH_CAVEAT
41578
+ ].join(" ");
41579
+ var STUDIO_UPDATE_ANIMATION_INPUT_SCHEMA = {
41580
+ type: "object",
41581
+ properties: {
41582
+ animationId: { type: "string", description: "An animation id from studio_inspect." },
41583
+ duration: { type: "number", minimum: 0, description: "Duration in seconds." },
41584
+ ease: { type: "string", description: "A GSAP ease, for example power2.out." },
41585
+ position: { type: "number", description: "Start position in seconds." }
41586
+ },
41587
+ required: ["animationId"],
41588
+ additionalProperties: false
41589
+ };
41590
+ var STUDIO_UPDATE_ANIMATION_DESCRIPTION = [
41591
+ "Change an existing animation's duration, ease or position.",
41592
+ "This is the one animation tool that CONFIRMS its write, so a failure here is real",
41593
+ "and usually means a stale animationId. Get current ids from studio_inspect."
41594
+ ].join(" ");
41595
+ var STUDIO_ADD_KEYFRAME_INPUT_SCHEMA = {
41596
+ type: "object",
41597
+ properties: {
41598
+ animationId: { type: "string", description: "An animation id from studio_inspect." },
41599
+ percent: {
41600
+ type: "number",
41601
+ minimum: 0,
41602
+ maximum: 100,
41603
+ description: "Where in the tween, 0 to 100."
41604
+ },
41605
+ properties: {
41606
+ type: "object",
41607
+ description: 'GSAP property to value, for example {"y": -50, "opacity": 0}.'
41608
+ }
41609
+ },
41610
+ required: ["animationId", "percent", "properties"],
41611
+ additionalProperties: false
41612
+ };
41613
+ var STUDIO_ADD_KEYFRAME_DESCRIPTION = [
41614
+ "Add a keyframe to an existing animation at a percentage through it.",
41615
+ "All the properties land in one commit, so they are one undo entry.",
41616
+ DISPATCH_CAVEAT
41617
+ ].join(" ");
41618
+ var STUDIO_DELETE_ANIMATION_INPUT_SCHEMA = {
41619
+ type: "object",
41620
+ properties: {
41621
+ animationId: { type: "string", description: "An animation id from studio_inspect." }
41622
+ },
41623
+ required: ["animationId"],
41624
+ additionalProperties: false
41625
+ };
41626
+ var STUDIO_DELETE_ANIMATION_DESCRIPTION = [
41627
+ "Remove an animation from the currently selected element. Undo reverses it.",
41628
+ DISPATCH_CAVEAT
41629
+ ].join(" ");
41630
+
40819
41631
  // src/webmcp/useStudioAgentTools.ts
40820
41632
  var log2 = makeStudioDebugLogger("webmcp");
40821
41633
  function reportRegistration(report, native) {
@@ -40844,9 +41656,117 @@ function buildStudioTools(depsRef) {
40844
41656
  "studio_look",
40845
41657
  async () => buildStudioLook(depsRef.current.getSnapshot(), input)
40846
41658
  )
41659
+ },
41660
+ {
41661
+ name: "studio_select",
41662
+ title: "Select an element",
41663
+ description: STUDIO_SELECT_DESCRIPTION,
41664
+ inputSchema: STUDIO_SELECT_INPUT_SCHEMA,
41665
+ annotations: { readOnlyHint: false, untrustedContentHint: true },
41666
+ execute: (input) => runToolBody(
41667
+ "studio_select",
41668
+ () => studioSelect(depsRef.current, readStringInput(input, "handle"))
41669
+ )
41670
+ },
41671
+ {
41672
+ name: "studio_seek",
41673
+ title: "Move the playhead",
41674
+ description: STUDIO_SEEK_DESCRIPTION,
41675
+ inputSchema: STUDIO_SEEK_INPUT_SCHEMA,
41676
+ annotations: { readOnlyHint: false },
41677
+ execute: (input) => runToolBody(
41678
+ "studio_seek",
41679
+ async () => studioSeek(depsRef.current, readNumberInput(input, "time"))
41680
+ )
41681
+ },
41682
+ {
41683
+ name: "studio_frame",
41684
+ title: "See the composition",
41685
+ description: STUDIO_FRAME_DESCRIPTION,
41686
+ inputSchema: STUDIO_FRAME_INPUT_SCHEMA,
41687
+ annotations: { readOnlyHint: true, untrustedContentHint: true },
41688
+ execute: (input) => runToolBody("studio_frame", () => studioFrame(depsRef.current, input))
41689
+ },
41690
+ {
41691
+ name: "studio_inspect",
41692
+ title: "Inspect one element",
41693
+ description: STUDIO_INSPECT_DESCRIPTION,
41694
+ inputSchema: STUDIO_INSPECT_INPUT_SCHEMA,
41695
+ annotations: { readOnlyHint: true, untrustedContentHint: true },
41696
+ execute: (input) => runToolBody(
41697
+ "studio_inspect",
41698
+ () => studioInspect(depsRef.current, input)
41699
+ )
41700
+ },
41701
+ {
41702
+ name: "studio_set_text",
41703
+ title: "Set an element's text",
41704
+ description: STUDIO_SET_TEXT_DESCRIPTION,
41705
+ inputSchema: STUDIO_SET_TEXT_INPUT_SCHEMA,
41706
+ annotations: { readOnlyHint: false, untrustedContentHint: true },
41707
+ execute: (input) => runToolBody("studio_set_text", () => studioSetText(depsRef.current, input))
41708
+ },
41709
+ {
41710
+ name: "studio_set_style",
41711
+ title: "Set an element's styles",
41712
+ description: STUDIO_SET_STYLE_DESCRIPTION,
41713
+ inputSchema: STUDIO_SET_STYLE_INPUT_SCHEMA,
41714
+ annotations: { readOnlyHint: false },
41715
+ execute: (input) => runToolBody("studio_set_style", () => studioSetStyle(depsRef.current, input))
41716
+ },
41717
+ {
41718
+ name: "studio_transform",
41719
+ title: "Move, resize or rotate",
41720
+ description: STUDIO_TRANSFORM_DESCRIPTION,
41721
+ inputSchema: STUDIO_TRANSFORM_INPUT_SCHEMA,
41722
+ annotations: { readOnlyHint: false },
41723
+ execute: (input) => runToolBody(
41724
+ "studio_transform",
41725
+ () => studioTransform(depsRef.current, input)
41726
+ )
41727
+ },
41728
+ {
41729
+ name: "studio_add_animation",
41730
+ title: "Add an animation",
41731
+ description: STUDIO_ADD_ANIMATION_DESCRIPTION,
41732
+ inputSchema: STUDIO_ADD_ANIMATION_INPUT_SCHEMA,
41733
+ annotations: { readOnlyHint: false },
41734
+ execute: (input) => runToolBody("studio_add_animation", () => studioAddAnimation(depsRef.current, input))
41735
+ },
41736
+ {
41737
+ name: "studio_update_animation",
41738
+ title: "Change an animation",
41739
+ description: STUDIO_UPDATE_ANIMATION_DESCRIPTION,
41740
+ inputSchema: STUDIO_UPDATE_ANIMATION_INPUT_SCHEMA,
41741
+ annotations: { readOnlyHint: false },
41742
+ execute: (input) => runToolBody("studio_update_animation", () => studioUpdateAnimation(depsRef.current, input))
41743
+ },
41744
+ {
41745
+ name: "studio_add_keyframe",
41746
+ title: "Add a keyframe",
41747
+ description: STUDIO_ADD_KEYFRAME_DESCRIPTION,
41748
+ inputSchema: STUDIO_ADD_KEYFRAME_INPUT_SCHEMA,
41749
+ annotations: { readOnlyHint: false },
41750
+ execute: (input) => runToolBody("studio_add_keyframe", () => studioAddKeyframe(depsRef.current, input))
41751
+ },
41752
+ {
41753
+ name: "studio_delete_animation",
41754
+ title: "Remove an animation",
41755
+ description: STUDIO_DELETE_ANIMATION_DESCRIPTION,
41756
+ inputSchema: STUDIO_DELETE_ANIMATION_INPUT_SCHEMA,
41757
+ annotations: { readOnlyHint: false },
41758
+ execute: (input) => runToolBody("studio_delete_animation", () => studioDeleteAnimation(depsRef.current, input))
40847
41759
  }
40848
41760
  ];
40849
41761
  }
41762
+ function readStringInput(input, key2) {
41763
+ const value = Reflect.get(input, key2);
41764
+ return typeof value === "string" ? value : "";
41765
+ }
41766
+ function readNumberInput(input, key2) {
41767
+ const value = Reflect.get(input, key2);
41768
+ return typeof value === "number" ? value : Number.NaN;
41769
+ }
40850
41770
  function useStudioAgentTools(deps) {
40851
41771
  const depsRef = useRef68(deps);
40852
41772
  depsRef.current = deps;
@@ -40878,8 +41798,27 @@ function useStudioAgentTools(deps) {
40878
41798
 
40879
41799
  // src/webmcp/StudioAgentTools.tsx
40880
41800
  function StudioAgentTools() {
40881
- const { projectId, activeCompPath, editHistory } = useStudioShellContext();
40882
- const { domEditSelection, selectedGsapAnimations } = useDomEditSelectionContext();
41801
+ const { projectId, activeCompPath, editHistory, writeBlockedReason } = useStudioShellContext();
41802
+ const {
41803
+ domEditSelection,
41804
+ selectedGsapAnimations,
41805
+ gsapMultipleTimelines,
41806
+ gsapUnsupportedTimelinePattern
41807
+ } = useDomEditSelectionContext();
41808
+ const {
41809
+ previewIframeRef,
41810
+ buildDomSelectionFromTarget,
41811
+ applyDomSelection,
41812
+ handleDomTextCommit,
41813
+ handleDomStyleCommit,
41814
+ handleDomPathOffsetCommit,
41815
+ handleDomBoxSizeCommit,
41816
+ handleDomRotationCommit,
41817
+ handleGsapAddAnimation,
41818
+ handleGsapUpdateMeta,
41819
+ handleGsapAddKeyframeBatch,
41820
+ handleGsapDeleteAnimation
41821
+ } = useDomEditActionsContext();
40883
41822
  const getSnapshot = useCallback60(() => {
40884
41823
  const player = usePlayerStore.getState();
40885
41824
  return {
@@ -40899,7 +41838,82 @@ function StudioAgentTools() {
40899
41838
  }
40900
41839
  };
40901
41840
  }, [projectId, activeCompPath, domEditSelection, selectedGsapAnimations, editHistory]);
40902
- useStudioAgentTools({ getSnapshot });
41841
+ const deps = useMemo37(
41842
+ () => ({
41843
+ getSnapshot,
41844
+ getPreviewDocument: () => previewIframeRef.current?.contentDocument ?? null,
41845
+ buildSelection: (element) => buildDomSelectionFromTarget(element),
41846
+ applySelection: (selection) => applyDomSelection(selection, { revealPanel: true }),
41847
+ requestSeek: (time) => usePlayerStore.getState().requestSeek(time),
41848
+ readPlayhead: () => {
41849
+ const player = usePlayerStore.getState();
41850
+ return {
41851
+ currentTime: player.currentTime,
41852
+ duration: player.duration,
41853
+ isPlaying: player.isPlaying
41854
+ };
41855
+ },
41856
+ getProjectId: () => projectId,
41857
+ getCompositionPath: () => activeCompPath,
41858
+ // HEAD, not GET: the tool only needs to know the frame renders. Pulling
41859
+ // the PNG here would download it once for nothing, since the agent
41860
+ // fetches the URL itself.
41861
+ probeFrame: async (url) => {
41862
+ try {
41863
+ const response = await fetch(url, { method: "HEAD" });
41864
+ return { ok: response.ok, status: response.status };
41865
+ } catch {
41866
+ return { ok: false, status: 0 };
41867
+ }
41868
+ },
41869
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
41870
+ getCurrentSelection: () => domEditSelection,
41871
+ getWriteBlockedReason: () => writeBlockedReason,
41872
+ setText: (value, fieldKey) => handleDomTextCommit(value, fieldKey),
41873
+ setStyle: (property, value) => handleDomStyleCommit(property, value),
41874
+ // Measured, not authored: the tool compares this before and after to
41875
+ // tell a real change from a handler that did nothing and resolved.
41876
+ readBox: (selection) => {
41877
+ const rect = selection.element.getBoundingClientRect();
41878
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
41879
+ },
41880
+ moveTo: (selection, next) => handleDomPathOffsetCommit(selection, next),
41881
+ resizeTo: (selection, next) => handleDomBoxSizeCommit(selection, next),
41882
+ rotateTo: (selection, next) => handleDomRotationCommit(selection, next),
41883
+ addAnimation: (method) => handleGsapAddAnimation(method),
41884
+ updateAnimation: (animationId, updates) => handleGsapUpdateMeta(animationId, updates),
41885
+ addKeyframe: (animationId, percent3, properties) => handleGsapAddKeyframeBatch(animationId, percent3, properties),
41886
+ deleteAnimation: (animationId) => handleGsapDeleteAnimation(animationId),
41887
+ getGsapDiagnostics: () => ({
41888
+ animations: selectedGsapAnimations,
41889
+ multipleTimelines: gsapMultipleTimelines,
41890
+ unsupportedTimelinePattern: gsapUnsupportedTimelinePattern
41891
+ })
41892
+ }),
41893
+ [
41894
+ getSnapshot,
41895
+ previewIframeRef,
41896
+ buildDomSelectionFromTarget,
41897
+ applyDomSelection,
41898
+ projectId,
41899
+ activeCompPath,
41900
+ writeBlockedReason,
41901
+ handleDomTextCommit,
41902
+ handleDomStyleCommit,
41903
+ handleDomPathOffsetCommit,
41904
+ handleDomBoxSizeCommit,
41905
+ handleDomRotationCommit,
41906
+ handleGsapAddAnimation,
41907
+ handleGsapUpdateMeta,
41908
+ handleGsapAddKeyframeBatch,
41909
+ handleGsapDeleteAnimation,
41910
+ domEditSelection,
41911
+ selectedGsapAnimations,
41912
+ gsapMultipleTimelines,
41913
+ gsapUnsupportedTimelinePattern
41914
+ ]
41915
+ );
41916
+ useStudioAgentTools(deps);
40903
41917
  return null;
40904
41918
  }
40905
41919
 
@@ -41227,13 +42241,13 @@ var SourceEditor = memo28(function SourceEditor2({
41227
42241
  });
41228
42242
 
41229
42243
  // src/components/editor/PropertyPanel.tsx
41230
- import { memo as memo32, useMemo as useMemo52, useRef as useRef99, useState as useState90 } from "react";
42244
+ import { memo as memo32, useMemo as useMemo53, useRef as useRef99, useState as useState90 } from "react";
41231
42245
 
41232
42246
  // src/components/editor/InspectorHeaderActions.tsx
41233
42247
  import { Eye as Eye3, EyeSlash as EyeSlash2 } from "@phosphor-icons/react";
41234
42248
 
41235
42249
  // src/contexts/DesignPanelInputContext.tsx
41236
- import { useCallback as useCallback63, useContext as useContext6, useMemo as useMemo37 } from "react";
42250
+ import { useCallback as useCallback63, useContext as useContext6, useMemo as useMemo38 } from "react";
41237
42251
 
41238
42252
  // src/utils/designInputTracking.ts
41239
42253
  var COALESCE_WINDOW_MS = 600;
@@ -41276,7 +42290,7 @@ function DesignPanelInputProvider({
41276
42290
  children
41277
42291
  }) {
41278
42292
  const parent = useContext6(DesignPanelInputContext);
41279
- const value = useMemo37(
42293
+ const value = useMemo38(
41280
42294
  () => ({ ui: ui ?? parent.ui, section: section ?? parent.section }),
41281
42295
  [ui, section, parent.ui, parent.section]
41282
42296
  );
@@ -42891,7 +43905,7 @@ function MediaSection({
42891
43905
  import { isHfColorGradingActive as isHfColorGradingActive2 } from "@hyperframes/core/color-grading";
42892
43906
 
42893
43907
  // src/components/editor/propertyPanelColorGradingControls.tsx
42894
- import { useMemo as useMemo38, useRef as useRef74, useState as useState53 } from "react";
43908
+ import { useMemo as useMemo39, useRef as useRef74, useState as useState53 } from "react";
42895
43909
  import {
42896
43910
  HF_COLOR_GRADING_PRESETS,
42897
43911
  normalizeHfColorGrading
@@ -43335,7 +44349,7 @@ function ColorGradingControls({
43335
44349
  const [detailSettings, setDetailSettings] = useState53(null);
43336
44350
  const [lutImporting, setLutImporting] = useState53(false);
43337
44351
  const [lutImportError, setLutImportError] = useState53(null);
43338
- const lutAssets = useMemo38(
44352
+ const lutAssets = useMemo39(
43339
44353
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
43340
44354
  [assets]
43341
44355
  );
@@ -43615,7 +44629,7 @@ function ColorGradingControls({
43615
44629
  }
43616
44630
 
43617
44631
  // src/components/editor/useColorGradingController.ts
43618
- import { useCallback as useCallback68, useEffect as useEffect57, useMemo as useMemo39, useRef as useRef76, useState as useState55 } from "react";
44632
+ import { useCallback as useCallback68, useEffect as useEffect57, useMemo as useMemo40, useRef as useRef76, useState as useState55 } from "react";
43619
44633
  import {
43620
44634
  HF_COLOR_GRADING_ATTR,
43621
44635
  hasHfColorGradingAuthoredValues,
@@ -44027,7 +45041,7 @@ function useColorGradingController({
44027
45041
  state: "pending",
44028
45042
  message: "Waiting for runtime"
44029
45043
  }));
44030
- const selectedAssetPath = useMemo39(
45044
+ const selectedAssetPath = useMemo40(
44031
45045
  () => selectedMediaAssetPath(element, projectId),
44032
45046
  [element, projectId]
44033
45047
  );
@@ -44074,7 +45088,7 @@ function useColorGradingController({
44074
45088
  trackStudioPendingEdit(onSetAttributeLive(COLOR_GRADING_DATA_KEY, value));
44075
45089
  };
44076
45090
  }, [identityKey]);
44077
- const target = useMemo39(
45091
+ const target = useMemo40(
44078
45092
  () => ({
44079
45093
  id: element.id ?? null,
44080
45094
  hfId: element.hfId ?? null,
@@ -45052,7 +46066,7 @@ function ColorField({
45052
46066
  }
45053
46067
 
45054
46068
  // src/components/editor/propertyPanelFont.tsx
45055
- import { useEffect as useEffect59, useMemo as useMemo40, useRef as useRef78, useState as useState57 } from "react";
46069
+ import { useEffect as useEffect59, useMemo as useMemo41, useRef as useRef78, useState as useState57 } from "react";
45056
46070
 
45057
46071
  // src/components/editor/fontAssets.ts
45058
46072
  var FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i;
@@ -45274,13 +46288,13 @@ function FontFamilyField({
45274
46288
  setImportingFonts(false);
45275
46289
  }
45276
46290
  };
45277
- const projectFontAssets = useMemo40(
46291
+ const projectFontAssets = useMemo41(
45278
46292
  () => uniqueFontOptions(
45279
46293
  importedFonts.map((f) => ({ family: f.family, source: "Imported" }))
45280
46294
  ),
45281
46295
  [importedFonts]
45282
46296
  );
45283
- const options = useMemo40(() => {
46297
+ const options = useMemo41(() => {
45284
46298
  const documentFonts = collectDocumentFontFamilies();
45285
46299
  const googleSet = new Set(googleFonts.map((f) => f.toLowerCase()));
45286
46300
  const taggedLocal = localFonts.map(
@@ -45300,7 +46314,7 @@ function FontFamilyField({
45300
46314
  ])
45301
46315
  );
45302
46316
  }, [currentFamily, googleFonts, localFonts, projectFontAssets]);
45303
- const filteredOptions = useMemo40(() => {
46317
+ const filteredOptions = useMemo41(() => {
45304
46318
  const matches = options.filter((o) => fontMatchesQuery(o.family, query));
45305
46319
  if (query.trim()) return matches.slice(0, 200);
45306
46320
  const bySource = /* @__PURE__ */ new Map();
@@ -45536,7 +46550,7 @@ function FontFamilyField({
45536
46550
  import { useEffect as useEffect61 } from "react";
45537
46551
 
45538
46552
  // src/components/panels/VariablesBindElement.tsx
45539
- import { useMemo as useMemo41, useState as useState59 } from "react";
46553
+ import { useMemo as useMemo42, useState as useState59 } from "react";
45540
46554
 
45541
46555
  // src/components/panels/VariablesValueControls.tsx
45542
46556
  import { useState as useState58 } from "react";
@@ -45818,7 +46832,7 @@ function VariablesBindElement({
45818
46832
  }) {
45819
46833
  const [activeKey, setActiveKey] = useState59(null);
45820
46834
  const [idDraft, setIdDraft] = useState59("");
45821
- const actions = useMemo41(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
46835
+ const actions = useMemo42(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
45822
46836
  if (actions.length === 0) return null;
45823
46837
  const active = actions.find((a) => a.key === activeKey) ?? null;
45824
46838
  const trimmedId = sanitizeId(idDraft);
@@ -45929,7 +46943,7 @@ function uniqueId(base2, existing) {
45929
46943
  }
45930
46944
 
45931
46945
  // src/contexts/VariablePromoteContext.tsx
45932
- import { useContext as useContext7, useEffect as useEffect60, useMemo as useMemo42, useState as useState60 } from "react";
46946
+ import { useContext as useContext7, useEffect as useEffect60, useMemo as useMemo43, useState as useState60 } from "react";
45933
46947
  import { jsx as jsx100 } from "react/jsx-runtime";
45934
46948
  var VariablePromoteContext = createStableContext(
45935
46949
  "VariablePromoteContext",
@@ -45952,15 +46966,15 @@ function VariablePromoteProvider({
45952
46966
  if (!session) return;
45953
46967
  return session.on("change", () => setRevision((r) => r + 1));
45954
46968
  }, [session]);
45955
- const actions = useMemo42(() => {
46969
+ const actions = useMemo43(() => {
45956
46970
  if (!session || !selection) return [];
45957
46971
  return buildBindActions(selection, session);
45958
46972
  }, [session, selection, revision]);
45959
- const declarations = useMemo42(() => {
46973
+ const declarations = useMemo43(() => {
45960
46974
  if (!session) return [];
45961
46975
  return session.getVariableDeclarations();
45962
46976
  }, [session, revision]);
45963
- const value = useMemo42(
46977
+ const value = useMemo43(
45964
46978
  () => ({ session, selection, actions, declarations, persist: persist2, onPersistError }),
45965
46979
  [session, selection, actions, declarations, persist2, onPersistError]
45966
46980
  );
@@ -45969,7 +46983,7 @@ function VariablePromoteProvider({
45969
46983
  function useVariablePromoteChannel(channel) {
45970
46984
  const ctx3 = useContext7(VariablePromoteContext);
45971
46985
  const key2 = channel.kind === "style" ? `style:${channel.prop}` : channel.kind;
45972
- return useMemo42(() => {
46986
+ return useMemo43(() => {
45973
46987
  if (!ctx3 || !ctx3.session || !ctx3.selection?.hfId) return null;
45974
46988
  const { session, selection, actions, declarations, persist: persist2, onPersistError } = ctx3;
45975
46989
  const hfId = selection.hfId;
@@ -46404,7 +47418,7 @@ function parseColorString(value) {
46404
47418
  }
46405
47419
 
46406
47420
  // src/components/editor/propertyPanelFill.tsx
46407
- import { useMemo as useMemo43, useRef as useRef79, useState as useState61 } from "react";
47421
+ import { useMemo as useMemo44, useRef as useRef79, useState as useState61 } from "react";
46408
47422
  import { jsx as jsx102, jsxs as jsxs79 } from "react/jsx-runtime";
46409
47423
  function normalizeProjectPath(value) {
46410
47424
  const trimmed = value.trim();
@@ -46449,8 +47463,8 @@ function ImageFillField({
46449
47463
  const fileInputRef = useRef79(null);
46450
47464
  const [uploading, setUploading] = useState61(false);
46451
47465
  const [uploadError, setUploadError] = useState61(null);
46452
- const imageAssets = useMemo43(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
46453
- const selectedAsset = useMemo43(
47466
+ const imageAssets = useMemo44(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
47467
+ const selectedAsset = useMemo44(
46454
47468
  () => resolveSelectedAsset(value, sourceFile, imageAssets),
46455
47469
  [imageAssets, sourceFile, value]
46456
47470
  );
@@ -47932,7 +48946,7 @@ import { useCallback as useCallback73, useState as useState66 } from "react";
47932
48946
  import { useShallow } from "zustand/react/shallow";
47933
48947
 
47934
48948
  // src/components/editor/AnimationCard.tsx
47935
- import { memo as memo30, useCallback as useCallback72, useEffect as useEffect64, useMemo as useMemo44, useRef as useRef82, useState as useState65 } from "react";
48949
+ import { memo as memo30, useCallback as useCallback72, useEffect as useEffect64, useMemo as useMemo45, useRef as useRef82, useState as useState65 } from "react";
47936
48950
  import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
47937
48951
 
47938
48952
  // src/components/editor/gsapAnimationHelpers.ts
@@ -48473,21 +49487,21 @@ var AnimationCard = memo30(function AnimationCard2({
48473
49487
  segment?.scrollIntoView({ block: "nearest", behavior: "smooth" });
48474
49488
  pendingAutoScrollRef.current = false;
48475
49489
  }, [expandedKfPct]);
48476
- const usedProps = useMemo44(
49490
+ const usedProps = useMemo45(
48477
49491
  () => new Set(Object.keys(animation.properties)),
48478
49492
  [animation.properties]
48479
49493
  );
48480
- const availableProps = useMemo44(
49494
+ const availableProps = useMemo45(
48481
49495
  () => SUPPORTED_PROPS.filter(
48482
49496
  (p) => !usedProps.has(p) && (animation.method === "set" || !BOOLEAN_PROPS.has(p))
48483
49497
  ),
48484
49498
  [usedProps, animation.method]
48485
49499
  );
48486
- const usedFromProps = useMemo44(
49500
+ const usedFromProps = useMemo45(
48487
49501
  () => new Set(Object.keys(animation.fromProperties ?? {})),
48488
49502
  [animation.fromProperties]
48489
49503
  );
48490
- const availableFromProps = useMemo44(
49504
+ const availableFromProps = useMemo45(
48491
49505
  () => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p) && !BOOLEAN_PROPS.has(p)),
48492
49506
  [usedFromProps]
48493
49507
  );
@@ -48534,7 +49548,7 @@ var AnimationCard = memo30(function AnimationCard2({
48534
49548
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
48535
49549
  const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
48536
49550
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
48537
- const summary = useMemo44(() => buildTweenSummary(animation), [animation]);
49551
+ const summary = useMemo45(() => buildTweenSummary(animation), [animation]);
48538
49552
  const setKeys = Object.keys(animation.properties);
48539
49553
  if (animation.method === "set" && // `every` is vacuously true on an empty bag — require at least one key so a
48540
49554
  // property-less set doesn't masquerade as a position row.
@@ -49682,7 +50696,7 @@ function PropertyPanel3dTransform({
49682
50696
  }
49683
50697
 
49684
50698
  // src/components/editor/PropertyPanelFlat.tsx
49685
- import { useEffect as useEffect84, useMemo as useMemo51, useRef as useRef98, useState as useState89 } from "react";
50699
+ import { useEffect as useEffect84, useMemo as useMemo52, useRef as useRef98, useState as useState89 } from "react";
49686
50700
  import { useShallow as useShallow2 } from "zustand/react/shallow";
49687
50701
 
49688
50702
  // src/components/editor/audioFxSummary.ts
@@ -51752,7 +52766,7 @@ function motionSectionLabel(args) {
51752
52766
  }
51753
52767
 
51754
52768
  // src/components/editor/propertyPanelAudioFxGroup.tsx
51755
- import { useMemo as useMemo47, useState as useState77 } from "react";
52769
+ import { useMemo as useMemo48, useState as useState77 } from "react";
51756
52770
  import {
51757
52771
  HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR6,
51758
52772
  HF_AUDIO_FX_DATA_KEY as HF_AUDIO_FX_DATA_KEY2,
@@ -51811,7 +52825,7 @@ function automationAttrValue(automation) {
51811
52825
  }
51812
52826
 
51813
52827
  // src/components/editor/propertyPanelFxSection.tsx
51814
- import { useCallback as useCallback76, useEffect as useEffect71, useMemo as useMemo46, useRef as useRef86, useState as useState75 } from "react";
52828
+ import { useCallback as useCallback76, useEffect as useEffect71, useMemo as useMemo47, useRef as useRef86, useState as useState75 } from "react";
51815
52829
 
51816
52830
  // src/components/editor/audioFxRevealTarget.ts
51817
52831
  import { parseAutomationTarget as parseAutomationTarget2 } from "@hyperframes/core/audio-automation";
@@ -52757,7 +53771,7 @@ function FxEqModule({
52757
53771
  import { getAudioFxPreset as getAudioFxPreset3 } from "@hyperframes/core/audio-fx-presets";
52758
53772
 
52759
53773
  // src/components/editor/propertyPanelFxNodeRow.tsx
52760
- import { useMemo as useMemo45, useState as useState74 } from "react";
53774
+ import { useMemo as useMemo46, useState as useState74 } from "react";
52761
53775
  import {
52762
53776
  defaultAudioFxParams as defaultAudioFxParams3,
52763
53777
  getAudioFxDef as getAudioFxDef3
@@ -53183,11 +54197,11 @@ function FxNodeRow({
53183
54197
  trackKind
53184
54198
  }) {
53185
54199
  const registryDef = getAudioFxDef3(node.type);
53186
- const def = useMemo45(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
54200
+ const def = useMemo46(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
53187
54201
  const primary = registryDef ? primaryParamOf(registryDef) : null;
53188
54202
  const profile = getAudioFxProfile2(node.type);
53189
- const derived = useMemo45(() => profileParam(node.type), [node.type]);
53190
- const onlyPrimary = useMemo45(
54203
+ const derived = useMemo46(() => profileParam(node.type), [node.type]);
54204
+ const onlyPrimary = useMemo46(
53191
54205
  () => def && primary ? { ...def, params: def.params.filter((p) => p.key === primary) } : def,
53192
54206
  [def, primary]
53193
54207
  );
@@ -53833,12 +54847,12 @@ function FxSection({
53833
54847
  [chain.nodes, mutate, onRemoveNodeAutomation, trackKind]
53834
54848
  );
53835
54849
  const [carveOpen, setCarveOpen] = useState75(true);
53836
- const carveNodes = useMemo46(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
53837
- const handBuilt = useMemo46(
54850
+ const carveNodes = useMemo47(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
54851
+ const handBuilt = useMemo47(
53838
54852
  () => chain.nodes.map((node, i) => ({ node, i })).filter(({ node }) => !node.fromCarve && !node.fromEq),
53839
54853
  [chain.nodes]
53840
54854
  );
53841
- const runs = useMemo46(() => {
54855
+ const runs = useMemo47(() => {
53842
54856
  const out = [];
53843
54857
  for (const item of handBuilt) {
53844
54858
  const preset = item.node.fromPreset;
@@ -53849,8 +54863,8 @@ function FxSection({
53849
54863
  return out;
53850
54864
  }, [handBuilt]);
53851
54865
  const [collapsedRuns, setCollapsedRuns] = useState75(/* @__PURE__ */ new Set());
53852
- const eqIds = useMemo46(() => audioEqIds(chain), [chain]);
53853
- const positions = useMemo46(() => {
54866
+ const eqIds = useMemo47(() => audioEqIds(chain), [chain]);
54867
+ const positions = useMemo47(() => {
53854
54868
  const map = /* @__PURE__ */ new Map();
53855
54869
  let at = (showCarve ? 1 : 0) + eqIds.length;
53856
54870
  for (const { i } of handBuilt) map.set(i, ++at);
@@ -54586,7 +55600,7 @@ function AudioFxGroup({
54586
55600
  const removeNodeAutomation = (nodeId) => removeNodesAutomation([nodeId]);
54587
55601
  const [analysing, setAnalysing] = useState77(false);
54588
55602
  const storeElements = usePlayerStore((s) => s.elements);
54589
- const signalPath = useMemo47(() => {
55603
+ const signalPath = useMemo48(() => {
54590
55604
  const doc = element.element?.ownerDocument;
54591
55605
  return audioFxSignalPath(
54592
55606
  element.tagName?.toLowerCase(),
@@ -55063,7 +56077,7 @@ function createGsapLivePreview(iframeRef) {
55063
56077
  }
55064
56078
 
55065
56079
  // src/components/editor/propertyPanelFlatColorGradingSection.tsx
55066
- import { useEffect as useEffect81, useMemo as useMemo49, useRef as useRef97, useState as useState85 } from "react";
56080
+ import { useEffect as useEffect81, useMemo as useMemo50, useRef as useRef97, useState as useState85 } from "react";
55067
56081
  import {
55068
56082
  HF_COLOR_GRADING_GRADE_PRESETS,
55069
56083
  normalizeHfColorGrading as normalizeHfColorGrading5,
@@ -55098,7 +56112,7 @@ function presetPreviewHandlers({
55098
56112
  import { useState as useState81 } from "react";
55099
56113
 
55100
56114
  // src/components/editor/propertyPanelColorCurveGraph.tsx
55101
- import { useMemo as useMemo48, useRef as useRef90 } from "react";
56115
+ import { useMemo as useMemo49, useRef as useRef90 } from "react";
55102
56116
  import {
55103
56117
  compileHfColorCurve,
55104
56118
  compileHfHueCurve,
@@ -55286,8 +56300,8 @@ function CurveGraph({
55286
56300
  onCancel
55287
56301
  }) {
55288
56302
  const pointerRef = useRef90(null);
55289
- const samples = useMemo48(() => samplesFor(points, tab), [points, tab]);
55290
- const path = useMemo48(() => curvePath(samples, tab), [samples, tab]);
56303
+ const samples = useMemo49(() => samplesFor(points, tab), [points, tab]);
56304
+ const path = useMemo49(() => curvePath(samples, tab), [samples, tab]);
55291
56305
  const selectRelativePoint = (offset) => {
55292
56306
  if (points.length === 0) return;
55293
56307
  const current = selectedIndex ?? (offset > 0 ? -1 : 0);
@@ -57084,14 +58098,14 @@ function FlatColorGradingSection({
57084
58098
  const lutInputRef = useRef97(null);
57085
58099
  const [lutOpen, setLutOpen] = useState85(false);
57086
58100
  const [detailSettingsOpen, setDetailSettingsOpen] = useState85(null);
57087
- const lutAssets = useMemo49(
58101
+ const lutAssets = useMemo50(
57088
58102
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
57089
58103
  [assets]
57090
58104
  );
57091
58105
  const lut = grading.lut;
57092
58106
  const selectedLutName = lut?.src ? lut.src.split("/").pop() ?? lut.src : null;
57093
- const resolvedGrading = useMemo49(() => resolveColorGrading(grading), [grading]);
57094
- const secondaryInputGrading = useMemo49(
58107
+ const resolvedGrading = useMemo50(() => resolveColorGrading(grading), [grading]);
58108
+ const secondaryInputGrading = useMemo50(
57095
58109
  () => resolveColorGrading({
57096
58110
  intensity: 1,
57097
58111
  adjust: resolvedGrading.adjust,
@@ -57109,7 +58123,7 @@ function FlatColorGradingSection({
57109
58123
  ]
57110
58124
  );
57111
58125
  const actions = createColorGradingActions(grading, onCommitColorGrading);
57112
- const scopesRefreshKey = useMemo49(() => serializeHfColorGrading2(grading), [grading]);
58126
+ const scopesRefreshKey = useMemo50(() => serializeHfColorGrading2(grading), [grading]);
57113
58127
  useEffect81(() => {
57114
58128
  if (presetPreviews.status === "idle") onRequestPresetPreviews();
57115
58129
  }, [onRequestPresetPreviews, presetPreviews.status]);
@@ -58239,7 +59253,7 @@ function FlatEffectsSection({
58239
59253
  import { useState as useState88 } from "react";
58240
59254
 
58241
59255
  // src/hooks/useBlockCatalog.ts
58242
- import { useState as useState87, useEffect as useEffect83, useMemo as useMemo50 } from "react";
59256
+ import { useState as useState87, useEffect as useEffect83, useMemo as useMemo51 } from "react";
58243
59257
 
58244
59258
  // src/utils/blockCategories.ts
58245
59259
  import {
@@ -58306,7 +59320,7 @@ function useBlockCatalog() {
58306
59320
  cancelled = true;
58307
59321
  };
58308
59322
  }, []);
58309
- const filteredBlocks = useMemo50(() => {
59323
+ const filteredBlocks = useMemo51(() => {
58310
59324
  let result = blocks;
58311
59325
  if (category) {
58312
59326
  result = result.filter((b) => b.category === category);
@@ -58618,7 +59632,7 @@ function PropertyPanelFlat({
58618
59632
  const showMotionEffects = gsapEffectHandlers !== null && !audioSelection;
58619
59633
  const showMotionGroup = showMotionTiming || showMotionEffects;
58620
59634
  const volumeAutomation = useVolumeAutomation(element, onSetAttributeQuiet ?? onSetAttributeLive);
58621
- const audioGroupLabel = useMemo51(() => {
59635
+ const audioGroupLabel = useMemo52(() => {
58622
59636
  const doc = element.element?.ownerDocument;
58623
59637
  const id = element.id;
58624
59638
  if (!doc || !id) return void 0;
@@ -59179,12 +60193,12 @@ var PropertyPanel = memo32(function PropertyPanel2(props) {
59179
60193
  const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
59180
60194
  const iframeRef = previewIframeRef ?? { current: null };
59181
60195
  const gsapAnimIdForMemo = element ? gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null : null;
59182
- const gsapRuntimeValues = useMemo52(
60196
+ const gsapRuntimeValues = useMemo53(
59183
60197
  () => element ? readGsapRuntimeValuesForPanel(gsapAnimIdForMemo, gsapAnimations, element, iframeRef) : null,
59184
60198
  // eslint-disable-next-line react-hooks/exhaustive-deps -- iframeRef is stable; currentTime drives re-reads during playback
59185
60199
  [gsapAnimIdForMemo, gsapAnimations, element, currentTime]
59186
60200
  );
59187
- const gsapBorderRadius = useMemo52(
60201
+ const gsapBorderRadius = useMemo53(
59188
60202
  () => element ? readGsapBorderRadiusForPanel(gsapRuntimeValues, gsapAnimations, element, iframeRef) : null,
59189
60203
  // eslint-disable-next-line react-hooks/exhaustive-deps
59190
60204
  [gsapRuntimeValues, gsapAnimations, element, currentTime]
@@ -59574,11 +60588,11 @@ var PropertyPanel = memo32(function PropertyPanel2(props) {
59574
60588
  });
59575
60589
 
59576
60590
  // src/components/editor/FileTree.tsx
59577
- import { memo as memo34, useState as useState92, useCallback as useCallback78, useMemo as useMemo54, useRef as useRef101 } from "react";
60591
+ import { memo as memo34, useState as useState92, useCallback as useCallback78, useMemo as useMemo55, useRef as useRef101 } from "react";
59578
60592
  import { Plus as Plus2, FolderSimplePlus as FolderSimplePlus2 } from "@phosphor-icons/react";
59579
60593
 
59580
60594
  // src/components/editor/FileTreeNodes.tsx
59581
- import { memo as memo33, useState as useState91, useCallback as useCallback77, useMemo as useMemo53, useRef as useRef100, useEffect as useEffect85 } from "react";
60595
+ import { memo as memo33, useState as useState91, useCallback as useCallback77, useMemo as useMemo54, useRef as useRef100, useEffect as useEffect85 } from "react";
59582
60596
  import {
59583
60597
  PencilSimple,
59584
60598
  Copy,
@@ -60061,7 +61075,7 @@ var TreeFolder = memo33(function TreeFolder2({
60061
61075
  }) {
60062
61076
  const [isOpen, setIsOpen] = useState91(defaultOpen);
60063
61077
  const toggle = useCallback77(() => setIsOpen((v) => !v), []);
60064
- const children = useMemo53(() => sortChildren(node.children), [node.children]);
61078
+ const children = useMemo54(() => sortChildren(node.children), [node.children]);
60065
61079
  const Chevron = isOpen ? ChevronDown : ChevronRight;
60066
61080
  const isDragOver = dragOverFolder === node.fullPath;
60067
61081
  const isRenaming = inlineInput?.mode === "rename" && inlineInput.originalPath === node.fullPath;
@@ -60192,8 +61206,8 @@ var FileTree = memo34(function FileTree2({
60192
61206
  onImportFiles,
60193
61207
  lintFindingsByFile
60194
61208
  }) {
60195
- const tree = useMemo54(() => buildTree(files), [files]);
60196
- const children = useMemo54(() => sortChildren(tree.children), [tree]);
61209
+ const tree = useMemo55(() => buildTree(files), [files]);
61210
+ const children = useMemo55(() => sortChildren(tree.children), [tree]);
60197
61211
  const [contextMenu, setContextMenu] = useState92(null);
60198
61212
  const [inlineInput, setInlineInput] = useState92(null);
60199
61213
  const [deleteTarget, setDeleteTarget] = useState92(null);
@@ -60460,10 +61474,10 @@ var FileTree = memo34(function FileTree2({
60460
61474
  });
60461
61475
 
60462
61476
  // src/App.tsx
60463
- import { useState as useState155, useCallback as useCallback176, useRef as useRef170, useMemo as useMemo76, useLayoutEffect as useLayoutEffect10 } from "react";
61477
+ import { useState as useState155, useCallback as useCallback176, useRef as useRef170, useMemo as useMemo77, useLayoutEffect as useLayoutEffect10 } from "react";
60464
61478
 
60465
61479
  // src/components/renders/useRenderQueue.ts
60466
- import { useState as useState94, useEffect as useEffect87, useCallback as useCallback80, useRef as useRef103, useMemo as useMemo55 } from "react";
61480
+ import { useState as useState94, useEffect as useEffect87, useCallback as useCallback80, useRef as useRef103, useMemo as useMemo56 } from "react";
60467
61481
 
60468
61482
  // src/components/renders/serverError.ts
60469
61483
  async function readServerError(res) {
@@ -60987,7 +62001,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
60987
62001
  };
60988
62002
  }, [projectId]);
60989
62003
  const isRendering = jobs.some((j) => j.status === "rendering");
60990
- return useMemo55(
62004
+ return useMemo56(
60991
62005
  () => ({
60992
62006
  jobs,
60993
62007
  isRendering,
@@ -61044,8 +62058,8 @@ function useDialogBehavior({
61044
62058
  const onCloseRef = useRef104(onClose);
61045
62059
  onCloseRef.current = onClose;
61046
62060
  const requestClose = useCallback81(() => {
61047
- const guard = canCloseRef.current;
61048
- if (guard && !guard()) return;
62061
+ const guard3 = canCloseRef.current;
62062
+ if (guard3 && !guard3()) return;
61049
62063
  onCloseRef.current();
61050
62064
  }, []);
61051
62065
  useEffect88(() => {
@@ -62254,7 +63268,7 @@ function useCaptionSync(projectId) {
62254
63268
  }
62255
63269
 
62256
63270
  // src/hooks/usePersistentEditHistory.ts
62257
- import { useCallback as useCallback84, useEffect as useEffect90, useMemo as useMemo56, useRef as useRef110, useState as useState99 } from "react";
63271
+ import { useCallback as useCallback84, useEffect as useEffect90, useMemo as useMemo57, useRef as useRef110, useState as useState99 } from "react";
62258
63272
 
62259
63273
  // src/utils/editHistory.ts
62260
63274
  var DEFAULT_MAX_ENTRIES = 100;
@@ -62606,7 +63620,7 @@ function createPersistentEditHistoryStore({
62606
63620
  };
62607
63621
  }
62608
63622
  function usePersistentEditHistory(options) {
62609
- const storage = useMemo56(
63623
+ const storage = useMemo57(
62610
63624
  () => options.storage ?? createIndexedDbEditHistoryStorage(),
62611
63625
  [options.storage]
62612
63626
  );
@@ -62875,10 +63889,10 @@ function usePanelLayout(initialState2) {
62875
63889
  }
62876
63890
 
62877
63891
  // src/hooks/useFileManager.ts
62878
- import { useState as useState102, useCallback as useCallback88, useMemo as useMemo58, useRef as useRef113 } from "react";
63892
+ import { useState as useState102, useCallback as useCallback88, useMemo as useMemo59, useRef as useRef113 } from "react";
62879
63893
 
62880
63894
  // src/hooks/useFileTree.ts
62881
- import { useState as useState101, useCallback as useCallback86, useEffect as useEffect92, useMemo as useMemo57 } from "react";
63895
+ import { useState as useState101, useCallback as useCallback86, useEffect as useEffect92, useMemo as useMemo58 } from "react";
62882
63896
  function useFileTree({ projectId, projectIdRef }) {
62883
63897
  const [projectDir, setProjectDir] = useState101(null);
62884
63898
  const [fileTree, setFileTree] = useState101([]);
@@ -62914,11 +63928,11 @@ function useFileTree({ projectId, projectIdRef }) {
62914
63928
  if (data.files) setFileTree(data.files);
62915
63929
  }, [projectIdRef]);
62916
63930
  const compositions = compositionPaths;
62917
- const assets = useMemo57(
63931
+ const assets = useMemo58(
62918
63932
  () => fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
62919
63933
  [fileTree]
62920
63934
  );
62921
- const fontAssets = useMemo57(
63935
+ const fontAssets = useMemo58(
62922
63936
  () => assets.filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
62923
63937
  family: fontFamilyFromAssetPath(asset),
62924
63938
  path: asset,
@@ -63081,7 +64095,7 @@ function useFileManager({
63081
64095
  const projectIdRef = useRef113(projectId);
63082
64096
  projectIdRef.current = projectId;
63083
64097
  const importedFontAssetsRef = useRef113([]);
63084
- const fileVersionScope = useMemo58(
64098
+ const fileVersionScope = useMemo59(
63085
64099
  () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
63086
64100
  [projectId]
63087
64101
  );
@@ -63881,52 +64895,6 @@ import { useCallback as useCallback100, useRef as useRef118 } from "react";
63881
64895
  // src/hooks/useRazorSplit.ts
63882
64896
  import { useCallback as useCallback91, useRef as useRef116 } from "react";
63883
64897
 
63884
- // src/utils/projectRouting.ts
63885
- var PROJECT_HASH_PREFIX = "#project/";
63886
- function decodeHashProjectId(value) {
63887
- try {
63888
- return decodeURIComponent(value);
63889
- } catch {
63890
- return value;
63891
- }
63892
- }
63893
- function normalizeHashParams(params) {
63894
- if (!params) return new URLSearchParams();
63895
- if (params instanceof URLSearchParams) return params;
63896
- const next = new URLSearchParams();
63897
- for (const [key2, value] of Object.entries(params)) {
63898
- if (!key2 || value == null || value === "") continue;
63899
- next.set(key2, value);
63900
- }
63901
- return next;
63902
- }
63903
- function encodeProjectId(projectId) {
63904
- return encodeURIComponent(projectId);
63905
- }
63906
- function buildProjectHash(projectId, params) {
63907
- const search = normalizeHashParams(params).toString();
63908
- return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : ""}`;
63909
- }
63910
- function parseProjectHashRoute(hash) {
63911
- if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;
63912
- const route = hash.slice(PROJECT_HASH_PREFIX.length);
63913
- const queryIndex = route.indexOf("?");
63914
- const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;
63915
- if (!encodedProjectId || encodedProjectId.includes("/")) return null;
63916
- const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : "";
63917
- return {
63918
- projectId: decodeHashProjectId(encodedProjectId),
63919
- params: new URLSearchParams(rawParams)
63920
- };
63921
- }
63922
- function parseProjectIdFromHash(hash) {
63923
- return parseProjectHashRoute(hash)?.projectId ?? null;
63924
- }
63925
- function buildProjectApiPath(projectId, suffix = "") {
63926
- const normalizedSuffix = suffix && !suffix.startsWith("/") ? `/${suffix}` : suffix;
63927
- return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;
63928
- }
63929
-
63930
64898
  // src/utils/razorSplitTransaction.ts
63931
64899
  function targetIdentity(path, target) {
63932
64900
  if (target.hfId) return `${path}|hf:${target.hfId}`;
@@ -68631,7 +69599,7 @@ function useGroupCommits(params) {
68631
69599
  }
68632
69600
 
68633
69601
  // src/hooks/useGsapScriptCommits.ts
68634
- import { useCallback as useCallback115, useMemo as useMemo59, useRef as useRef126 } from "react";
69602
+ import { useCallback as useCallback115, useMemo as useMemo60, useRef as useRef126 } from "react";
68635
69603
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
68636
69604
 
68637
69605
  // src/hooks/gsapRuntimePatch.ts
@@ -69946,7 +70914,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
69946
70914
  const { instantPatch: _instantPatch, ...batchOptions } = options;
69947
70915
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, instantPatches.length > 0 ? { ...batchOptions, instantPatches } : batchOptions);
69948
70916
  }, [showToast, finalizeSuccessfulMutation]);
69949
- const commitMutation = useMemo59(() => {
70917
+ const commitMutation = useMemo60(() => {
69950
70918
  const serializeFile = (file, task) => {
69951
70919
  if (writeProjectFile) {
69952
70920
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -70011,7 +70979,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
70011
70979
  },
70012
70980
  [activeProjectId]
70013
70981
  );
70014
- const sdkDeps = useMemo59(
70982
+ const sdkDeps = useMemo60(
70015
70983
  () => writeProjectFile ? {
70016
70984
  editHistory: { recordEdit: editHistory.recordEdit },
70017
70985
  writeProjectFile,
@@ -73006,7 +73974,7 @@ function useStudioExternalFileChanges({
73006
73974
  }
73007
73975
 
73008
73976
  // src/hooks/useBlockHandlers.ts
73009
- import { useCallback as useCallback127, useMemo as useMemo60, useRef as useRef133, useState as useState109 } from "react";
73977
+ import { useCallback as useCallback127, useMemo as useMemo61, useRef as useRef133, useState as useState109 } from "react";
73010
73978
 
73011
73979
  // src/utils/blockInstaller.ts
73012
73980
  function getMaxZIndexFromIframe(iframe) {
@@ -73192,7 +74160,7 @@ function useBlockHandlers({
73192
74160
  setRightPanelTab
73193
74161
  }) {
73194
74162
  const [activeBlockParams, setActiveBlockParams] = useState109(null);
73195
- const blockCtx = useMemo60(
74163
+ const blockCtx = useMemo61(
73196
74164
  () => ({
73197
74165
  activeCompPath: blockCtxDeps.activeCompPath,
73198
74166
  timelineElements: blockCtxDeps.timelineElements,
@@ -74277,7 +75245,7 @@ import { useCallback as useCallback132 } from "react";
74277
75245
  import { createElement as createElement3 } from "react";
74278
75246
 
74279
75247
  // src/player/components/AudioWaveform.tsx
74280
- import { memo as memo36, useCallback as useCallback130, useMemo as useMemo61, useRef as useRef137 } from "react";
75248
+ import { memo as memo36, useCallback as useCallback130, useMemo as useMemo62, useRef as useRef137 } from "react";
74281
75249
  import { jsx as jsx167, jsxs as jsxs139 } from "react/jsx-runtime";
74282
75250
  var BAR_WIDTH = 2;
74283
75251
  var BAR_STEP = 3;
@@ -74337,7 +75305,7 @@ var AudioWaveform = memo36(function AudioWaveform2({
74337
75305
  const canvasRef = useRef137(null);
74338
75306
  const observerRef = useRef137(null);
74339
75307
  const cacheKey = waveformUrl ?? audioUrl;
74340
- const request = useMemo61(
75308
+ const request = useMemo62(
74341
75309
  () => ({
74342
75310
  key: createThumbnailKey({ kind: "waveform", source: cacheKey }),
74343
75311
  projectId,
@@ -74446,7 +75414,7 @@ var AudioWaveform = memo36(function AudioWaveform2({
74446
75414
  });
74447
75415
 
74448
75416
  // src/player/components/ImageThumbnail.tsx
74449
- import { memo as memo37, useCallback as useCallback131, useMemo as useMemo62, useRef as useRef138, useState as useState110 } from "react";
75417
+ import { memo as memo37, useCallback as useCallback131, useMemo as useMemo63, useRef as useRef138, useState as useState110 } from "react";
74450
75418
  import { jsx as jsx168, jsxs as jsxs140 } from "react/jsx-runtime";
74451
75419
  var ImageThumbnail = memo37(function ImageThumbnail2({
74452
75420
  imageSrc,
@@ -74459,7 +75427,7 @@ var ImageThumbnail = memo37(function ImageThumbnail2({
74459
75427
  }) {
74460
75428
  const [containerWidth, setContainerWidth] = useState110(0);
74461
75429
  const observerRef = useRef138(null);
74462
- const request = useMemo62(
75430
+ const request = useMemo63(
74463
75431
  () => ({
74464
75432
  key: createThumbnailKey({ kind: "image", source: imageSrc, rich: Number(rich) }),
74465
75433
  projectId,
@@ -74776,36 +75744,6 @@ function useConsoleErrorCapture(previewIframe) {
74776
75744
 
74777
75745
  // src/hooks/useFrameCapture.ts
74778
75746
  import { useState as useState112, useCallback as useCallback134, useRef as useRef140 } from "react";
74779
-
74780
- // src/utils/frameCapture.ts
74781
- function normalizeCompositionPath(compositionPath) {
74782
- return compositionPath && compositionPath !== "master" ? compositionPath : "index.html";
74783
- }
74784
- function buildFrameCaptureUrl({
74785
- projectId,
74786
- compositionPath,
74787
- currentTime,
74788
- origin = window.location.origin
74789
- }) {
74790
- const compPath = normalizeCompositionPath(compositionPath);
74791
- const url = new URL(
74792
- buildProjectApiPath(projectId, `/thumbnail/${encodeURIComponent(compPath)}`),
74793
- origin
74794
- );
74795
- url.searchParams.set("t", Math.max(0, currentTime).toFixed(3));
74796
- url.searchParams.set("format", "png");
74797
- url.searchParams.set("output", "source");
74798
- url.searchParams.set("v", String(Date.now()));
74799
- return url.toString();
74800
- }
74801
- function buildFrameCaptureFilename(compositionPath, currentTime) {
74802
- const compPath = normalizeCompositionPath(compositionPath);
74803
- const base2 = compPath.split("/").pop()?.replace(/\.html$/i, "") || "frame";
74804
- const frameTime = Math.max(0, currentTime).toFixed(3).replace(".", "-");
74805
- return `${base2}-${frameTime}s.png`;
74806
- }
74807
-
74808
- // src/hooks/useFrameCapture.ts
74809
75747
  function useFrameCapture({
74810
75748
  projectId,
74811
75749
  activeCompPath,
@@ -74899,7 +75837,7 @@ function useFrameCapture({
74899
75837
  }
74900
75838
 
74901
75839
  // src/hooks/useLintModal.ts
74902
- import { useState as useState113, useCallback as useCallback135, useEffect as useEffect108, useRef as useRef141, useMemo as useMemo63 } from "react";
75840
+ import { useState as useState113, useCallback as useCallback135, useEffect as useEffect108, useRef as useRef141, useMemo as useMemo64 } from "react";
74903
75841
  function parseFinding(f) {
74904
75842
  return {
74905
75843
  severity: f.severity === "error" ? "error" : "warning",
@@ -74971,8 +75909,8 @@ function useLintModal(projectId, refreshKey) {
74971
75909
  },
74972
75910
  [backgroundFindings]
74973
75911
  );
74974
- const findingsByElement = useMemo63(() => groupFindings((f) => f.elementId), [groupFindings]);
74975
- const findingsByFile = useMemo63(() => groupFindings((f) => f.file), [groupFindings]);
75912
+ const findingsByElement = useMemo64(() => groupFindings((f) => f.elementId), [groupFindings]);
75913
+ const findingsByFile = useMemo64(() => groupFindings((f) => f.file), [groupFindings]);
74976
75914
  useEffect108(() => {
74977
75915
  usePlayerStore.getState().setLintFindingsByElement(findingsByElement);
74978
75916
  }, [findingsByElement]);
@@ -75454,16 +76392,16 @@ function useStudioUrlState({
75454
76392
  }
75455
76393
 
75456
76394
  // src/hooks/useEffectiveTimelineDuration.ts
75457
- import { useMemo as useMemo64 } from "react";
76395
+ import { useMemo as useMemo65 } from "react";
75458
76396
  function useEffectiveTimelineDuration(timelineDuration2, timelineElements) {
75459
- return useMemo64(
76397
+ return useMemo65(
75460
76398
  () => getEffectiveTimelineDuration(timelineDuration2, timelineElements),
75461
76399
  [timelineDuration2, timelineElements]
75462
76400
  );
75463
76401
  }
75464
76402
 
75465
76403
  // src/hooks/useStudioContextValue.ts
75466
- import { useCallback as useCallback139, useMemo as useMemo65, useRef as useRef144, useState as useState116 } from "react";
76404
+ import { useCallback as useCallback139, useMemo as useMemo66, useRef as useRef144, useState as useState116 } from "react";
75467
76405
  function buildStudioContextValue(input) {
75468
76406
  return {
75469
76407
  projectId: input.projectId,
@@ -75478,6 +76416,9 @@ function buildStudioContextValue(input) {
75478
76416
  timelineElements: input.timelineElements,
75479
76417
  isPlaying: input.isPlaying,
75480
76418
  editHistory: input.editHistory,
76419
+ // Conflict first: when both are true the conflict is the one the user has
76420
+ // been asked to decide, and resolving it is what unblocks the queue.
76421
+ writeBlockedReason: input.externalFileConflict ? "an external change to this file is waiting to be resolved" : input.domEditSaveQueuePaused,
75481
76422
  handleUndo: input.handleUndo,
75482
76423
  handleRedo: input.handleRedo,
75483
76424
  renderQueue: input.renderQueue,
@@ -75488,7 +76429,7 @@ function buildStudioContextValue(input) {
75488
76429
  };
75489
76430
  }
75490
76431
  function useInspectorState(rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, domEditSelection, isGestureRecording) {
75491
- return useMemo65(() => {
76432
+ return useMemo66(() => {
75492
76433
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
75493
76434
  const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
75494
76435
  const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
@@ -75561,7 +76502,7 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
75561
76502
  import { useRef as useRef146 } from "react";
75562
76503
 
75563
76504
  // src/contexts/PanelLayoutContext.tsx
75564
- import { useContext as useContext8, useMemo as useMemo66 } from "react";
76505
+ import { useContext as useContext8, useMemo as useMemo67 } from "react";
75565
76506
  import { jsx as jsx169 } from "react/jsx-runtime";
75566
76507
  var PanelLayoutContext = createStableContext("PanelLayoutContext", null);
75567
76508
  function usePanelLayoutContext() {
@@ -75591,7 +76532,7 @@ function PanelLayoutProvider({
75591
76532
  },
75592
76533
  children
75593
76534
  }) {
75594
- const stable = useMemo66(
76535
+ const stable = useMemo67(
75595
76536
  () => ({
75596
76537
  leftWidth,
75597
76538
  rightWidth,
@@ -75639,7 +76580,7 @@ import {
75639
76580
  useCallback as useCallback140,
75640
76581
  useContext as useContext9,
75641
76582
  useEffect as useEffect110,
75642
- useMemo as useMemo67,
76583
+ useMemo as useMemo68,
75643
76584
  useRef as useRef145,
75644
76585
  useState as useState117
75645
76586
  } from "react";
@@ -75671,8 +76612,8 @@ function useViewModeState() {
75671
76612
  const guardsRef = useRef145(/* @__PURE__ */ new Set());
75672
76613
  const acceptedLocationRef = useRef145(readHistoryLocation());
75673
76614
  const canSetViewMode = useCallback140((mode) => {
75674
- for (const guard of guardsRef.current) {
75675
- if (!guard(mode)) return false;
76615
+ for (const guard3 of guardsRef.current) {
76616
+ if (!guard3(mode)) return false;
75676
76617
  }
75677
76618
  return true;
75678
76619
  }, []);
@@ -75702,13 +76643,13 @@ function useViewModeState() {
75702
76643
  },
75703
76644
  [canSetViewMode]
75704
76645
  );
75705
- const registerViewModeGuard = useCallback140((guard) => {
75706
- guardsRef.current.add(guard);
76646
+ const registerViewModeGuard = useCallback140((guard3) => {
76647
+ guardsRef.current.add(guard3);
75707
76648
  return () => {
75708
- guardsRef.current.delete(guard);
76649
+ guardsRef.current.delete(guard3);
75709
76650
  };
75710
76651
  }, []);
75711
- return useMemo67(
76652
+ return useMemo68(
75712
76653
  () => ({ viewMode, setViewMode, registerViewModeGuard }),
75713
76654
  [viewMode, setViewMode, registerViewModeGuard]
75714
76655
  );
@@ -76898,7 +77839,7 @@ function useGestureCommit({
76898
77839
  }
76899
77840
 
76900
77841
  // src/components/editor/GestureTrailOverlay.tsx
76901
- import { memo as memo38, useMemo as useMemo68 } from "react";
77842
+ import { memo as memo38, useMemo as useMemo69 } from "react";
76902
77843
  import { Fragment as Fragment49, jsx as jsx172, jsxs as jsxs142 } from "react/jsx-runtime";
76903
77844
  var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76904
77845
  samples,
@@ -76910,7 +77851,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76910
77851
  mode,
76911
77852
  accentColor = "#3CE6AC"
76912
77853
  }) {
76913
- const trailPoints = useMemo68(() => {
77854
+ const trailPoints = useMemo69(() => {
76914
77855
  if (!canvasRect) return "";
76915
77856
  if (trail2 && trail2.length > 1) {
76916
77857
  return trail2.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -76918,7 +77859,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76918
77859
  if (samples.length === 0) return "";
76919
77860
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
76920
77861
  }, [samples, trail2, sampleCount, canvasRect?.left, canvasRect?.top]);
76921
- const simplifiedPath = useMemo68(() => {
77862
+ const simplifiedPath = useMemo69(() => {
76922
77863
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
76923
77864
  const pts = [];
76924
77865
  for (const [pct, props] of simplifiedPoints) {
@@ -76930,7 +77871,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76930
77871
  if (pts.length === 0) return "";
76931
77872
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
76932
77873
  }, [simplifiedPoints]);
76933
- const diamondPositions = useMemo68(() => {
77874
+ const diamondPositions = useMemo69(() => {
76934
77875
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
76935
77876
  const pts = [];
76936
77877
  for (const [pct, props] of simplifiedPoints) {
@@ -77329,7 +78270,7 @@ var CompositionsTab = memo39(function CompositionsTab2({
77329
78270
  });
77330
78271
 
77331
78272
  // src/components/sidebar/AssetsTab.tsx
77332
- import { memo as memo40, useState as useState126, useCallback as useCallback147, useRef as useRef153, useMemo as useMemo70, useEffect as useEffect119 } from "react";
78273
+ import { memo as memo40, useState as useState126, useCallback as useCallback147, useRef as useRef153, useMemo as useMemo71, useEffect as useEffect119 } from "react";
77333
78274
 
77334
78275
  // src/components/ui/SearchInput.tsx
77335
78276
  import { jsx as jsx174, jsxs as jsxs144 } from "react/jsx-runtime";
@@ -77941,7 +78882,7 @@ function AudioRow({
77941
78882
  }
77942
78883
 
77943
78884
  // src/components/sidebar/GlobalAssetsView.tsx
77944
- import { useEffect as useEffect116, useMemo as useMemo69, useState as useState123 } from "react";
78885
+ import { useEffect as useEffect116, useMemo as useMemo70, useState as useState123 } from "react";
77945
78886
  import { jsx as jsx177, jsxs as jsxs147 } from "react/jsx-runtime";
77946
78887
  function globalAssetRows(records, query = "") {
77947
78888
  const q = query.trim().toLowerCase();
@@ -77968,7 +78909,7 @@ function GlobalAssetsView({ searchQuery }) {
77968
78909
  cancelled = true;
77969
78910
  };
77970
78911
  }, []);
77971
- const rows = useMemo69(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
78912
+ const rows = useMemo70(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
77972
78913
  if (records === null) {
77973
78914
  return /* @__PURE__ */ jsx177("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
77974
78915
  }
@@ -78527,12 +79468,12 @@ var AssetsTab = memo40(function AssetsTab2({
78527
79468
  setTimeout(() => setCopyFeedback(null), copied ? 1500 : 3e3);
78528
79469
  }, []);
78529
79470
  const elements = usePlayerStore((s) => s.elements);
78530
- const usedPaths = useMemo70(() => deriveUsedPaths(elements), [elements]);
78531
- const allMediaAssets = useMemo70(
79471
+ const usedPaths = useMemo71(() => deriveUsedPaths(elements), [elements]);
79472
+ const allMediaAssets = useMemo71(
78532
79473
  () => assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
78533
79474
  [assets]
78534
79475
  );
78535
- const mediaAssets = useMemo70(() => {
79476
+ const mediaAssets = useMemo71(() => {
78536
79477
  const all = filterByUsage(allMediaAssets, usedPaths, usageFilter);
78537
79478
  if (!searchQuery) return all;
78538
79479
  const q = searchQuery.toLowerCase();
@@ -78542,7 +79483,7 @@ var AssetsTab = memo40(function AssetsTab2({
78542
79483
  return rec?.description?.toLowerCase().includes(q);
78543
79484
  });
78544
79485
  }, [allMediaAssets, searchQuery, manifest, usageFilter, usedPaths]);
78545
- const categorized = useMemo70(() => {
79486
+ const categorized = useMemo71(() => {
78546
79487
  const groups = { audio: [], images: [], video: [], fonts: [] };
78547
79488
  for (const a of mediaAssets) {
78548
79489
  const cat = getCategory(a);
@@ -78557,12 +79498,12 @@ var AssetsTab = memo40(function AssetsTab2({
78557
79498
  }
78558
79499
  return groups;
78559
79500
  }, [mediaAssets, usedPaths]);
78560
- const counts = useMemo70(() => {
79501
+ const counts = useMemo71(() => {
78561
79502
  const c = { all: mediaAssets.length };
78562
79503
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
78563
79504
  return c;
78564
79505
  }, [mediaAssets, categorized]);
78565
- const usageCounts = useMemo70(
79506
+ const usageCounts = useMemo71(
78566
79507
  () => countUsage(
78567
79508
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
78568
79509
  usedPaths
@@ -82925,7 +83866,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
82925
83866
  }
82926
83867
 
82927
83868
  // src/components/panels/VariablesPanel.tsx
82928
- import { memo as memo51, useCallback as useCallback163, useEffect as useEffect129, useMemo as useMemo71, useState as useState144 } from "react";
83869
+ import { memo as memo51, useCallback as useCallback163, useEffect as useEffect129, useMemo as useMemo72, useState as useState144 } from "react";
82929
83870
 
82930
83871
  // src/hooks/useVariablesPersist.ts
82931
83872
  import { useCallback as useCallback160 } from "react";
@@ -83629,22 +84570,22 @@ var VariablesPanel = memo51(function VariablesPanel2({
83629
84570
  domEditSaveTimestampRef,
83630
84571
  publishSdkSession
83631
84572
  });
83632
- const declarations = useMemo71(
84573
+ const declarations = useMemo72(
83633
84574
  () => sdkSession?.getVariableDeclarations() ?? [],
83634
84575
  // eslint-disable-next-line react-hooks/exhaustive-deps
83635
84576
  [sdkSession, refreshKey, revision]
83636
84577
  );
83637
- const usage = useMemo71(
84578
+ const usage = useMemo72(
83638
84579
  () => sdkSession?.getVariableUsage() ?? null,
83639
84580
  // eslint-disable-next-line react-hooks/exhaustive-deps
83640
84581
  [sdkSession, refreshKey, revision]
83641
84582
  );
83642
- const issues = useMemo71(
84583
+ const issues = useMemo72(
83643
84584
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
83644
84585
  // eslint-disable-next-line react-hooks/exhaustive-deps
83645
84586
  [sdkSession, previewValues, refreshKey, revision]
83646
84587
  );
83647
- const effectiveValues = useMemo71(
84588
+ const effectiveValues = useMemo72(
83648
84589
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
83649
84590
  // eslint-disable-next-line react-hooks/exhaustive-deps
83650
84591
  [sdkSession, previewValues, refreshKey, revision]
@@ -83983,15 +84924,15 @@ function useSlideshowPersist({
83983
84924
  }
83984
84925
 
83985
84926
  // src/hooks/useSlideshowTabState.ts
83986
- import { useEffect as useEffect130, useMemo as useMemo72 } from "react";
84927
+ import { useEffect as useEffect130, useMemo as useMemo73 } from "react";
83987
84928
  import { SLIDESHOW_ISLAND_TYPE as SLIDESHOW_ISLAND_TYPE2, slideshowIslandRegex as slideshowIslandRegex2 } from "@hyperframes/core/slideshow";
83988
84929
  function useSlideshowTabState(params) {
83989
84930
  const { editingFileContent, previewIframeRef, refreshKey, rightPanelTab, setRightPanelTab } = params;
83990
- const isSlideshowComposition = useMemo72(() => {
84931
+ const isSlideshowComposition = useMemo73(() => {
83991
84932
  if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE2)) return false;
83992
84933
  return slideshowIslandRegex2("i").test(editingFileContent);
83993
84934
  }, [editingFileContent]);
83994
- const slideshowScenes = useMemo72(() => {
84935
+ const slideshowScenes = useMemo73(() => {
83995
84936
  try {
83996
84937
  const win = previewIframeRef.current?.contentWindow;
83997
84938
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -85663,7 +86604,7 @@ function useProjectSignaturePoll(projectId, currentSignature, onChange) {
85663
86604
  }
85664
86605
 
85665
86606
  // src/components/storyboard/StoryboardLoaded.tsx
85666
- import { useEffect as useEffect141, useMemo as useMemo75, useState as useState152 } from "react";
86607
+ import { useEffect as useEffect141, useMemo as useMemo76, useState as useState152 } from "react";
85667
86608
 
85668
86609
  // src/components/storyboard/StoryboardDirection.tsx
85669
86610
  import { jsx as jsx205, jsxs as jsxs170 } from "react/jsx-runtime";
@@ -85900,7 +86841,7 @@ function StoryboardScriptPanel({ script }) {
85900
86841
  }
85901
86842
 
85902
86843
  // src/components/storyboard/StoryboardSourceEditor.tsx
85903
- import { useCallback as useCallback172, useEffect as useEffect137, useMemo as useMemo73, useRef as useRef169, useState as useState148 } from "react";
86844
+ import { useCallback as useCallback172, useEffect as useEffect137, useMemo as useMemo74, useRef as useRef169, useState as useState148 } from "react";
85904
86845
  import { marked } from "marked";
85905
86846
  import DOMPurify from "dompurify";
85906
86847
  import { jsx as jsx210, jsxs as jsxs173 } from "react/jsx-runtime";
@@ -85959,7 +86900,7 @@ function useMarkdownPreview(source) {
85959
86900
  const id = window.setTimeout(() => setDebounced(source), 200);
85960
86901
  return () => window.clearTimeout(id);
85961
86902
  }, [source]);
85962
- return useMemo73(() => {
86903
+ return useMemo74(() => {
85963
86904
  const html2 = marked.parse(debounced, { async: false });
85964
86905
  DOMPurify.addHook("afterSanitizeAttributes", hardenLinks);
85965
86906
  try {
@@ -86631,7 +87572,7 @@ function progressLabel(summary) {
86631
87572
  }
86632
87573
 
86633
87574
  // src/components/storyboard/useFrameComments.ts
86634
- import { useCallback as useCallback174, useEffect as useEffect140, useMemo as useMemo74, useState as useState151 } from "react";
87575
+ import { useCallback as useCallback174, useEffect as useEffect140, useMemo as useMemo75, useState as useState151 } from "react";
86635
87576
 
86636
87577
  // src/components/storyboard/frameComments.ts
86637
87578
  var FRAME_COMMENTS_PATH = ".hyperframes/frame-comments.json";
@@ -86713,7 +87654,7 @@ function useFrameComments(frames) {
86713
87654
  const setDraft = useCallback174((index, text) => {
86714
87655
  setDrafts((prev) => ({ ...prev, [index]: text }));
86715
87656
  }, []);
86716
- const draftCount = useMemo74(
87657
+ const draftCount = useMemo75(
86717
87658
  () => Object.values(drafts).filter((text) => text.trim().length > 0).length,
86718
87659
  [drafts]
86719
87660
  );
@@ -86781,7 +87722,7 @@ function StoryboardLoaded({
86781
87722
  setFeedbackMessageCopied(false);
86782
87723
  }
86783
87724
  };
86784
- const sourceFiles = useMemo75(() => {
87725
+ const sourceFiles = useMemo76(() => {
86785
87726
  const files = [{ path: data.path, label: data.path }];
86786
87727
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
86787
87728
  return files;
@@ -87267,7 +88208,7 @@ function StudioApp() {
87267
88208
  domEditSaveTimestampRef,
87268
88209
  setRefreshKey
87269
88210
  });
87270
- const masterCompPath = useMemo76(
88211
+ const masterCompPath = useMemo77(
87271
88212
  () => resolveMasterCompositionPath(fileManager.fileTree),
87272
88213
  [fileManager.fileTree]
87273
88214
  );
@@ -87587,11 +88528,13 @@ function StudioApp() {
87587
88528
  handleRedo: appHotkeys.handleRedo,
87588
88529
  renderQueue,
87589
88530
  compositionDimensions,
88531
+ domEditSaveQueuePaused: previewPersistence.domEditSaveQueuePaused,
88532
+ externalFileConflict: externalFileChanges.blocked !== null,
87590
88533
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
87591
88534
  handlePreviewIframeRef,
87592
88535
  refreshPreviewDocumentVersion
87593
88536
  });
87594
- const timelineToolbar = useMemo76(
88537
+ const timelineToolbar = useMemo77(
87595
88538
  () => /* @__PURE__ */ jsx217(
87596
88539
  TimelineToolbar,
87597
88540
  {