@hyperframes/studio 0.8.19 → 0.8.21

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 (45) hide show
  1. package/dist/assets/hyperframes-player-CIXNA_bj.js +460 -0
  2. package/dist/assets/{index-Dd0fsIIL.js → index-44gfDvkh.js} +1 -1
  3. package/dist/assets/{index-DxccOrkZ.js → index-9VfE7bZe.js} +1 -1
  4. package/dist/assets/{index-Bjd2hioj.js → index-CgGY8dAv.js} +164 -164
  5. package/dist/index.d.ts +28 -0
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +1206 -234
  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/useEditorSave.test.tsx +81 -3
  13. package/src/hooks/useEditorSave.ts +31 -6
  14. package/src/hooks/useGsapInteractionFailureTelemetry.test.tsx +48 -16
  15. package/src/hooks/useGsapInteractionFailureTelemetry.ts +5 -2
  16. package/src/hooks/useStudioContextValue.ts +9 -0
  17. package/src/player/hooks/timelineSyncHydration.test.ts +96 -0
  18. package/src/player/hooks/timelineSyncHydration.ts +47 -1
  19. package/src/player/hooks/useExpandedTimelineElements.test.ts +43 -0
  20. package/src/player/hooks/useExpandedTimelineElements.ts +52 -29
  21. package/src/player/hooks/useTimelineSyncCallbacks.ts +4 -0
  22. package/src/player/store/playerStore.ts +17 -2
  23. package/src/player/store/timelineElement.ts +21 -0
  24. package/src/utils/studioFileVersion.test.ts +37 -1
  25. package/src/utils/studioFileVersion.ts +12 -2
  26. package/src/utils/studioSaveDiagnostics.test.ts +29 -0
  27. package/src/utils/studioSaveDiagnostics.ts +4 -0
  28. package/src/webmcp/StudioAgentTools.tsx +105 -10
  29. package/src/webmcp/toolResult.ts +1 -1
  30. package/src/webmcp/tools/animationTools.test.ts +244 -0
  31. package/src/webmcp/tools/animationTools.ts +276 -0
  32. package/src/webmcp/tools/contentTools.test.ts +275 -0
  33. package/src/webmcp/tools/contentTools.ts +217 -0
  34. package/src/webmcp/tools/frameTools.test.ts +136 -0
  35. package/src/webmcp/tools/frameTools.ts +133 -0
  36. package/src/webmcp/tools/inspectTools.test.ts +197 -0
  37. package/src/webmcp/tools/inspectTools.ts +208 -0
  38. package/src/webmcp/tools/selectionTools.test.ts +164 -0
  39. package/src/webmcp/tools/selectionTools.ts +154 -0
  40. package/src/webmcp/tools/transformTools.test.ts +179 -0
  41. package/src/webmcp/tools/transformTools.ts +205 -0
  42. package/src/webmcp/useStudioAgentTools.test.tsx +71 -22
  43. package/src/webmcp/useStudioAgentTools.ts +195 -1
  44. package/src/webmcp/webmcpTestUtils.ts +91 -0
  45. package/dist/assets/hyperframes-player-BA-QO9CQ.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
@@ -16306,6 +16313,9 @@ function buildStudioSaveFailureProperties(input) {
16306
16313
  function trackStudioSaveFailure(input) {
16307
16314
  trackStudioEvent("save_failure", buildStudioSaveFailureProperties(input));
16308
16315
  }
16316
+ function trackStudioEditBlocked(input) {
16317
+ trackStudioEvent("edit_blocked", buildStudioSaveFailureProperties(input));
16318
+ }
16309
16319
  async function createStudioSaveHttpError(response, fallbackMessage, options = {}) {
16310
16320
  let body = "";
16311
16321
  try {
@@ -16693,7 +16703,16 @@ async function studioExpectedFileVersion(versions, path, expectedContent) {
16693
16703
  return versions.get(path);
16694
16704
  }
16695
16705
  function createStudioWriteToken() {
16696
- return globalThis.crypto.randomUUID();
16706
+ const webCrypto = globalThis.crypto;
16707
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
16708
+ if (typeof webCrypto?.getRandomValues !== "function") {
16709
+ throw new Error("Web Crypto getRandomValues is required for Studio write identity");
16710
+ }
16711
+ const bytes = webCrypto.getRandomValues(new Uint8Array(16));
16712
+ bytes[6] = bytes[6] & 15 | 64;
16713
+ bytes[8] = bytes[8] & 63 | 128;
16714
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
16715
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
16697
16716
  }
16698
16717
  function studioWriteHeaders() {
16699
16718
  const token = createStudioWriteToken();
@@ -21192,6 +21211,7 @@ function StudioShellProvider({
21192
21211
  showToast,
21193
21212
  previewIframeRef,
21194
21213
  editHistory,
21214
+ writeBlockedReason,
21195
21215
  handleUndo,
21196
21216
  handleRedo,
21197
21217
  renderQueue,
@@ -21207,6 +21227,7 @@ function StudioShellProvider({
21207
21227
  showToast,
21208
21228
  previewIframeRef,
21209
21229
  editHistory,
21230
+ writeBlockedReason,
21210
21231
  handleUndo,
21211
21232
  handleRedo,
21212
21233
  renderQueue,
@@ -21223,6 +21244,7 @@ function StudioShellProvider({
21223
21244
  setActiveCompPath,
21224
21245
  showToast,
21225
21246
  previewIframeRef,
21247
+ writeBlockedReason,
21226
21248
  handleUndo,
21227
21249
  handleRedo,
21228
21250
  waitForPendingDomEditSaves,
@@ -24831,6 +24853,32 @@ function collectSubCompositionDomChildren(iframeDoc, clips, parentMap) {
24831
24853
  }
24832
24854
  return out;
24833
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
+ }
24834
24882
  function safeContentDocument(iframe) {
24835
24883
  try {
24836
24884
  return iframe?.contentDocument ?? null;
@@ -24988,6 +25036,7 @@ function useTimelineSyncCallbacks({
24988
25036
  const domClipChildren = collectSubCompositionDomChildren(iframeDoc, data.clips, parentMap);
24989
25037
  usePlayerStore.getState().setClipParentMap(parentMap);
24990
25038
  usePlayerStore.getState().setDomClipChildren(domClipChildren);
25039
+ usePlayerStore.getState().setSubCompositionHostState(collectSubCompositionHostState(iframeDoc, data.clips));
24991
25040
  } catch {
24992
25041
  }
24993
25042
  const els = buildTimelineElementsFromClips(filtered, iframeDoc);
@@ -40572,7 +40621,7 @@ function useTimelineSelectionPreviewSync({
40572
40621
  }
40573
40622
 
40574
40623
  // src/webmcp/StudioAgentTools.tsx
40575
- import { useCallback as useCallback60 } from "react";
40624
+ import { useCallback as useCallback60, useMemo as useMemo37 } from "react";
40576
40625
 
40577
40626
  // src/webmcp/useStudioAgentTools.ts
40578
40627
  import { useEffect as useEffect49, useRef as useRef68 } from "react";
@@ -40719,6 +40768,43 @@ function patchTargetAddress(target) {
40719
40768
  selectorIndex: target.selectorIndex
40720
40769
  };
40721
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
+ }
40722
40808
 
40723
40809
  // src/webmcp/tools/lookTools.ts
40724
40810
  var DEFAULT_LIMIT = 200;
@@ -40804,6 +40890,744 @@ var STUDIO_LOOK_DESCRIPTION = [
40804
40890
  "you did not do, a human pressed undo and your earlier edits are gone."
40805
40891
  ].join(" ");
40806
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
+
40807
41631
  // src/webmcp/useStudioAgentTools.ts
40808
41632
  var log2 = makeStudioDebugLogger("webmcp");
40809
41633
  function reportRegistration(report, native) {
@@ -40832,9 +41656,117 @@ function buildStudioTools(depsRef) {
40832
41656
  "studio_look",
40833
41657
  async () => buildStudioLook(depsRef.current.getSnapshot(), input)
40834
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))
40835
41759
  }
40836
41760
  ];
40837
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
+ }
40838
41770
  function useStudioAgentTools(deps) {
40839
41771
  const depsRef = useRef68(deps);
40840
41772
  depsRef.current = deps;
@@ -40866,8 +41798,27 @@ function useStudioAgentTools(deps) {
40866
41798
 
40867
41799
  // src/webmcp/StudioAgentTools.tsx
40868
41800
  function StudioAgentTools() {
40869
- const { projectId, activeCompPath, editHistory } = useStudioShellContext();
40870
- 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();
40871
41822
  const getSnapshot = useCallback60(() => {
40872
41823
  const player = usePlayerStore.getState();
40873
41824
  return {
@@ -40887,7 +41838,82 @@ function StudioAgentTools() {
40887
41838
  }
40888
41839
  };
40889
41840
  }, [projectId, activeCompPath, domEditSelection, selectedGsapAnimations, editHistory]);
40890
- 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);
40891
41917
  return null;
40892
41918
  }
40893
41919
 
@@ -41215,13 +42241,13 @@ var SourceEditor = memo28(function SourceEditor2({
41215
42241
  });
41216
42242
 
41217
42243
  // src/components/editor/PropertyPanel.tsx
41218
- 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";
41219
42245
 
41220
42246
  // src/components/editor/InspectorHeaderActions.tsx
41221
42247
  import { Eye as Eye3, EyeSlash as EyeSlash2 } from "@phosphor-icons/react";
41222
42248
 
41223
42249
  // src/contexts/DesignPanelInputContext.tsx
41224
- 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";
41225
42251
 
41226
42252
  // src/utils/designInputTracking.ts
41227
42253
  var COALESCE_WINDOW_MS = 600;
@@ -41264,7 +42290,7 @@ function DesignPanelInputProvider({
41264
42290
  children
41265
42291
  }) {
41266
42292
  const parent = useContext6(DesignPanelInputContext);
41267
- const value = useMemo37(
42293
+ const value = useMemo38(
41268
42294
  () => ({ ui: ui ?? parent.ui, section: section ?? parent.section }),
41269
42295
  [ui, section, parent.ui, parent.section]
41270
42296
  );
@@ -42879,7 +43905,7 @@ function MediaSection({
42879
43905
  import { isHfColorGradingActive as isHfColorGradingActive2 } from "@hyperframes/core/color-grading";
42880
43906
 
42881
43907
  // src/components/editor/propertyPanelColorGradingControls.tsx
42882
- 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";
42883
43909
  import {
42884
43910
  HF_COLOR_GRADING_PRESETS,
42885
43911
  normalizeHfColorGrading
@@ -43323,7 +44349,7 @@ function ColorGradingControls({
43323
44349
  const [detailSettings, setDetailSettings] = useState53(null);
43324
44350
  const [lutImporting, setLutImporting] = useState53(false);
43325
44351
  const [lutImportError, setLutImportError] = useState53(null);
43326
- const lutAssets = useMemo38(
44352
+ const lutAssets = useMemo39(
43327
44353
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
43328
44354
  [assets]
43329
44355
  );
@@ -43603,7 +44629,7 @@ function ColorGradingControls({
43603
44629
  }
43604
44630
 
43605
44631
  // src/components/editor/useColorGradingController.ts
43606
- 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";
43607
44633
  import {
43608
44634
  HF_COLOR_GRADING_ATTR,
43609
44635
  hasHfColorGradingAuthoredValues,
@@ -44015,7 +45041,7 @@ function useColorGradingController({
44015
45041
  state: "pending",
44016
45042
  message: "Waiting for runtime"
44017
45043
  }));
44018
- const selectedAssetPath = useMemo39(
45044
+ const selectedAssetPath = useMemo40(
44019
45045
  () => selectedMediaAssetPath(element, projectId),
44020
45046
  [element, projectId]
44021
45047
  );
@@ -44062,7 +45088,7 @@ function useColorGradingController({
44062
45088
  trackStudioPendingEdit(onSetAttributeLive(COLOR_GRADING_DATA_KEY, value));
44063
45089
  };
44064
45090
  }, [identityKey]);
44065
- const target = useMemo39(
45091
+ const target = useMemo40(
44066
45092
  () => ({
44067
45093
  id: element.id ?? null,
44068
45094
  hfId: element.hfId ?? null,
@@ -45040,7 +46066,7 @@ function ColorField({
45040
46066
  }
45041
46067
 
45042
46068
  // src/components/editor/propertyPanelFont.tsx
45043
- 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";
45044
46070
 
45045
46071
  // src/components/editor/fontAssets.ts
45046
46072
  var FONT_EXT_RE = /\.(eot|otf|ttc|ttf|woff2?)$/i;
@@ -45262,13 +46288,13 @@ function FontFamilyField({
45262
46288
  setImportingFonts(false);
45263
46289
  }
45264
46290
  };
45265
- const projectFontAssets = useMemo40(
46291
+ const projectFontAssets = useMemo41(
45266
46292
  () => uniqueFontOptions(
45267
46293
  importedFonts.map((f) => ({ family: f.family, source: "Imported" }))
45268
46294
  ),
45269
46295
  [importedFonts]
45270
46296
  );
45271
- const options = useMemo40(() => {
46297
+ const options = useMemo41(() => {
45272
46298
  const documentFonts = collectDocumentFontFamilies();
45273
46299
  const googleSet = new Set(googleFonts.map((f) => f.toLowerCase()));
45274
46300
  const taggedLocal = localFonts.map(
@@ -45288,7 +46314,7 @@ function FontFamilyField({
45288
46314
  ])
45289
46315
  );
45290
46316
  }, [currentFamily, googleFonts, localFonts, projectFontAssets]);
45291
- const filteredOptions = useMemo40(() => {
46317
+ const filteredOptions = useMemo41(() => {
45292
46318
  const matches = options.filter((o) => fontMatchesQuery(o.family, query));
45293
46319
  if (query.trim()) return matches.slice(0, 200);
45294
46320
  const bySource = /* @__PURE__ */ new Map();
@@ -45524,7 +46550,7 @@ function FontFamilyField({
45524
46550
  import { useEffect as useEffect61 } from "react";
45525
46551
 
45526
46552
  // src/components/panels/VariablesBindElement.tsx
45527
- import { useMemo as useMemo41, useState as useState59 } from "react";
46553
+ import { useMemo as useMemo42, useState as useState59 } from "react";
45528
46554
 
45529
46555
  // src/components/panels/VariablesValueControls.tsx
45530
46556
  import { useState as useState58 } from "react";
@@ -45806,7 +46832,7 @@ function VariablesBindElement({
45806
46832
  }) {
45807
46833
  const [activeKey, setActiveKey] = useState59(null);
45808
46834
  const [idDraft, setIdDraft] = useState59("");
45809
- const actions = useMemo41(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
46835
+ const actions = useMemo42(() => buildBindActions(selection, sdkSession), [selection, sdkSession]);
45810
46836
  if (actions.length === 0) return null;
45811
46837
  const active = actions.find((a) => a.key === activeKey) ?? null;
45812
46838
  const trimmedId = sanitizeId(idDraft);
@@ -45917,7 +46943,7 @@ function uniqueId(base2, existing) {
45917
46943
  }
45918
46944
 
45919
46945
  // src/contexts/VariablePromoteContext.tsx
45920
- 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";
45921
46947
  import { jsx as jsx100 } from "react/jsx-runtime";
45922
46948
  var VariablePromoteContext = createStableContext(
45923
46949
  "VariablePromoteContext",
@@ -45940,15 +46966,15 @@ function VariablePromoteProvider({
45940
46966
  if (!session) return;
45941
46967
  return session.on("change", () => setRevision((r) => r + 1));
45942
46968
  }, [session]);
45943
- const actions = useMemo42(() => {
46969
+ const actions = useMemo43(() => {
45944
46970
  if (!session || !selection) return [];
45945
46971
  return buildBindActions(selection, session);
45946
46972
  }, [session, selection, revision]);
45947
- const declarations = useMemo42(() => {
46973
+ const declarations = useMemo43(() => {
45948
46974
  if (!session) return [];
45949
46975
  return session.getVariableDeclarations();
45950
46976
  }, [session, revision]);
45951
- const value = useMemo42(
46977
+ const value = useMemo43(
45952
46978
  () => ({ session, selection, actions, declarations, persist: persist2, onPersistError }),
45953
46979
  [session, selection, actions, declarations, persist2, onPersistError]
45954
46980
  );
@@ -45957,7 +46983,7 @@ function VariablePromoteProvider({
45957
46983
  function useVariablePromoteChannel(channel) {
45958
46984
  const ctx3 = useContext7(VariablePromoteContext);
45959
46985
  const key2 = channel.kind === "style" ? `style:${channel.prop}` : channel.kind;
45960
- return useMemo42(() => {
46986
+ return useMemo43(() => {
45961
46987
  if (!ctx3 || !ctx3.session || !ctx3.selection?.hfId) return null;
45962
46988
  const { session, selection, actions, declarations, persist: persist2, onPersistError } = ctx3;
45963
46989
  const hfId = selection.hfId;
@@ -46392,7 +47418,7 @@ function parseColorString(value) {
46392
47418
  }
46393
47419
 
46394
47420
  // src/components/editor/propertyPanelFill.tsx
46395
- 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";
46396
47422
  import { jsx as jsx102, jsxs as jsxs79 } from "react/jsx-runtime";
46397
47423
  function normalizeProjectPath(value) {
46398
47424
  const trimmed = value.trim();
@@ -46437,8 +47463,8 @@ function ImageFillField({
46437
47463
  const fileInputRef = useRef79(null);
46438
47464
  const [uploading, setUploading] = useState61(false);
46439
47465
  const [uploadError, setUploadError] = useState61(null);
46440
- const imageAssets = useMemo43(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
46441
- const selectedAsset = useMemo43(
47466
+ const imageAssets = useMemo44(() => assets.filter((a) => IMAGE_EXT.test(a)), [assets]);
47467
+ const selectedAsset = useMemo44(
46442
47468
  () => resolveSelectedAsset(value, sourceFile, imageAssets),
46443
47469
  [imageAssets, sourceFile, value]
46444
47470
  );
@@ -47920,7 +48946,7 @@ import { useCallback as useCallback73, useState as useState66 } from "react";
47920
48946
  import { useShallow } from "zustand/react/shallow";
47921
48947
 
47922
48948
  // src/components/editor/AnimationCard.tsx
47923
- 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";
47924
48950
  import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
47925
48951
 
47926
48952
  // src/components/editor/gsapAnimationHelpers.ts
@@ -48461,21 +49487,21 @@ var AnimationCard = memo30(function AnimationCard2({
48461
49487
  segment?.scrollIntoView({ block: "nearest", behavior: "smooth" });
48462
49488
  pendingAutoScrollRef.current = false;
48463
49489
  }, [expandedKfPct]);
48464
- const usedProps = useMemo44(
49490
+ const usedProps = useMemo45(
48465
49491
  () => new Set(Object.keys(animation.properties)),
48466
49492
  [animation.properties]
48467
49493
  );
48468
- const availableProps = useMemo44(
49494
+ const availableProps = useMemo45(
48469
49495
  () => SUPPORTED_PROPS.filter(
48470
49496
  (p) => !usedProps.has(p) && (animation.method === "set" || !BOOLEAN_PROPS.has(p))
48471
49497
  ),
48472
49498
  [usedProps, animation.method]
48473
49499
  );
48474
- const usedFromProps = useMemo44(
49500
+ const usedFromProps = useMemo45(
48475
49501
  () => new Set(Object.keys(animation.fromProperties ?? {})),
48476
49502
  [animation.fromProperties]
48477
49503
  );
48478
- const availableFromProps = useMemo44(
49504
+ const availableFromProps = useMemo45(
48479
49505
  () => SUPPORTED_PROPS.filter((p) => !usedFromProps.has(p) && !BOOLEAN_PROPS.has(p)),
48480
49506
  [usedFromProps]
48481
49507
  );
@@ -48522,7 +49548,7 @@ var AnimationCard = memo30(function AnimationCard2({
48522
49548
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
48523
49549
  const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
48524
49550
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
48525
- const summary = useMemo44(() => buildTweenSummary(animation), [animation]);
49551
+ const summary = useMemo45(() => buildTweenSummary(animation), [animation]);
48526
49552
  const setKeys = Object.keys(animation.properties);
48527
49553
  if (animation.method === "set" && // `every` is vacuously true on an empty bag — require at least one key so a
48528
49554
  // property-less set doesn't masquerade as a position row.
@@ -49670,7 +50696,7 @@ function PropertyPanel3dTransform({
49670
50696
  }
49671
50697
 
49672
50698
  // src/components/editor/PropertyPanelFlat.tsx
49673
- 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";
49674
50700
  import { useShallow as useShallow2 } from "zustand/react/shallow";
49675
50701
 
49676
50702
  // src/components/editor/audioFxSummary.ts
@@ -51740,7 +52766,7 @@ function motionSectionLabel(args) {
51740
52766
  }
51741
52767
 
51742
52768
  // src/components/editor/propertyPanelAudioFxGroup.tsx
51743
- import { useMemo as useMemo47, useState as useState77 } from "react";
52769
+ import { useMemo as useMemo48, useState as useState77 } from "react";
51744
52770
  import {
51745
52771
  HF_AUDIO_FX_ATTR as HF_AUDIO_FX_ATTR6,
51746
52772
  HF_AUDIO_FX_DATA_KEY as HF_AUDIO_FX_DATA_KEY2,
@@ -51799,7 +52825,7 @@ function automationAttrValue(automation) {
51799
52825
  }
51800
52826
 
51801
52827
  // src/components/editor/propertyPanelFxSection.tsx
51802
- 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";
51803
52829
 
51804
52830
  // src/components/editor/audioFxRevealTarget.ts
51805
52831
  import { parseAutomationTarget as parseAutomationTarget2 } from "@hyperframes/core/audio-automation";
@@ -52745,7 +53771,7 @@ function FxEqModule({
52745
53771
  import { getAudioFxPreset as getAudioFxPreset3 } from "@hyperframes/core/audio-fx-presets";
52746
53772
 
52747
53773
  // src/components/editor/propertyPanelFxNodeRow.tsx
52748
- import { useMemo as useMemo45, useState as useState74 } from "react";
53774
+ import { useMemo as useMemo46, useState as useState74 } from "react";
52749
53775
  import {
52750
53776
  defaultAudioFxParams as defaultAudioFxParams3,
52751
53777
  getAudioFxDef as getAudioFxDef3
@@ -53171,11 +54197,11 @@ function FxNodeRow({
53171
54197
  trackKind
53172
54198
  }) {
53173
54199
  const registryDef = getAudioFxDef3(node.type);
53174
- const def = useMemo45(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
54200
+ const def = useMemo46(() => registryDef ? plainDef(registryDef) : null, [registryDef]);
53175
54201
  const primary = registryDef ? primaryParamOf(registryDef) : null;
53176
54202
  const profile = getAudioFxProfile2(node.type);
53177
- const derived = useMemo45(() => profileParam(node.type), [node.type]);
53178
- const onlyPrimary = useMemo45(
54203
+ const derived = useMemo46(() => profileParam(node.type), [node.type]);
54204
+ const onlyPrimary = useMemo46(
53179
54205
  () => def && primary ? { ...def, params: def.params.filter((p) => p.key === primary) } : def,
53180
54206
  [def, primary]
53181
54207
  );
@@ -53821,12 +54847,12 @@ function FxSection({
53821
54847
  [chain.nodes, mutate, onRemoveNodeAutomation, trackKind]
53822
54848
  );
53823
54849
  const [carveOpen, setCarveOpen] = useState75(true);
53824
- const carveNodes = useMemo46(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
53825
- const handBuilt = useMemo46(
54850
+ const carveNodes = useMemo47(() => chain.nodes.filter((n) => n.fromCarve), [chain.nodes]);
54851
+ const handBuilt = useMemo47(
53826
54852
  () => chain.nodes.map((node, i) => ({ node, i })).filter(({ node }) => !node.fromCarve && !node.fromEq),
53827
54853
  [chain.nodes]
53828
54854
  );
53829
- const runs = useMemo46(() => {
54855
+ const runs = useMemo47(() => {
53830
54856
  const out = [];
53831
54857
  for (const item of handBuilt) {
53832
54858
  const preset = item.node.fromPreset;
@@ -53837,8 +54863,8 @@ function FxSection({
53837
54863
  return out;
53838
54864
  }, [handBuilt]);
53839
54865
  const [collapsedRuns, setCollapsedRuns] = useState75(/* @__PURE__ */ new Set());
53840
- const eqIds = useMemo46(() => audioEqIds(chain), [chain]);
53841
- const positions = useMemo46(() => {
54866
+ const eqIds = useMemo47(() => audioEqIds(chain), [chain]);
54867
+ const positions = useMemo47(() => {
53842
54868
  const map = /* @__PURE__ */ new Map();
53843
54869
  let at = (showCarve ? 1 : 0) + eqIds.length;
53844
54870
  for (const { i } of handBuilt) map.set(i, ++at);
@@ -54574,7 +55600,7 @@ function AudioFxGroup({
54574
55600
  const removeNodeAutomation = (nodeId) => removeNodesAutomation([nodeId]);
54575
55601
  const [analysing, setAnalysing] = useState77(false);
54576
55602
  const storeElements = usePlayerStore((s) => s.elements);
54577
- const signalPath = useMemo47(() => {
55603
+ const signalPath = useMemo48(() => {
54578
55604
  const doc = element.element?.ownerDocument;
54579
55605
  return audioFxSignalPath(
54580
55606
  element.tagName?.toLowerCase(),
@@ -55051,7 +56077,7 @@ function createGsapLivePreview(iframeRef) {
55051
56077
  }
55052
56078
 
55053
56079
  // src/components/editor/propertyPanelFlatColorGradingSection.tsx
55054
- 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";
55055
56081
  import {
55056
56082
  HF_COLOR_GRADING_GRADE_PRESETS,
55057
56083
  normalizeHfColorGrading as normalizeHfColorGrading5,
@@ -55086,7 +56112,7 @@ function presetPreviewHandlers({
55086
56112
  import { useState as useState81 } from "react";
55087
56113
 
55088
56114
  // src/components/editor/propertyPanelColorCurveGraph.tsx
55089
- import { useMemo as useMemo48, useRef as useRef90 } from "react";
56115
+ import { useMemo as useMemo49, useRef as useRef90 } from "react";
55090
56116
  import {
55091
56117
  compileHfColorCurve,
55092
56118
  compileHfHueCurve,
@@ -55274,8 +56300,8 @@ function CurveGraph({
55274
56300
  onCancel
55275
56301
  }) {
55276
56302
  const pointerRef = useRef90(null);
55277
- const samples = useMemo48(() => samplesFor(points, tab), [points, tab]);
55278
- 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]);
55279
56305
  const selectRelativePoint = (offset) => {
55280
56306
  if (points.length === 0) return;
55281
56307
  const current = selectedIndex ?? (offset > 0 ? -1 : 0);
@@ -57072,14 +58098,14 @@ function FlatColorGradingSection({
57072
58098
  const lutInputRef = useRef97(null);
57073
58099
  const [lutOpen, setLutOpen] = useState85(false);
57074
58100
  const [detailSettingsOpen, setDetailSettingsOpen] = useState85(null);
57075
- const lutAssets = useMemo49(
58101
+ const lutAssets = useMemo50(
57076
58102
  () => assets.filter((asset) => LUT_EXT.test(asset)).sort((a, b) => a.localeCompare(b)),
57077
58103
  [assets]
57078
58104
  );
57079
58105
  const lut = grading.lut;
57080
58106
  const selectedLutName = lut?.src ? lut.src.split("/").pop() ?? lut.src : null;
57081
- const resolvedGrading = useMemo49(() => resolveColorGrading(grading), [grading]);
57082
- const secondaryInputGrading = useMemo49(
58107
+ const resolvedGrading = useMemo50(() => resolveColorGrading(grading), [grading]);
58108
+ const secondaryInputGrading = useMemo50(
57083
58109
  () => resolveColorGrading({
57084
58110
  intensity: 1,
57085
58111
  adjust: resolvedGrading.adjust,
@@ -57097,7 +58123,7 @@ function FlatColorGradingSection({
57097
58123
  ]
57098
58124
  );
57099
58125
  const actions = createColorGradingActions(grading, onCommitColorGrading);
57100
- const scopesRefreshKey = useMemo49(() => serializeHfColorGrading2(grading), [grading]);
58126
+ const scopesRefreshKey = useMemo50(() => serializeHfColorGrading2(grading), [grading]);
57101
58127
  useEffect81(() => {
57102
58128
  if (presetPreviews.status === "idle") onRequestPresetPreviews();
57103
58129
  }, [onRequestPresetPreviews, presetPreviews.status]);
@@ -58227,7 +59253,7 @@ function FlatEffectsSection({
58227
59253
  import { useState as useState88 } from "react";
58228
59254
 
58229
59255
  // src/hooks/useBlockCatalog.ts
58230
- 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";
58231
59257
 
58232
59258
  // src/utils/blockCategories.ts
58233
59259
  import {
@@ -58294,7 +59320,7 @@ function useBlockCatalog() {
58294
59320
  cancelled = true;
58295
59321
  };
58296
59322
  }, []);
58297
- const filteredBlocks = useMemo50(() => {
59323
+ const filteredBlocks = useMemo51(() => {
58298
59324
  let result = blocks;
58299
59325
  if (category) {
58300
59326
  result = result.filter((b) => b.category === category);
@@ -58606,7 +59632,7 @@ function PropertyPanelFlat({
58606
59632
  const showMotionEffects = gsapEffectHandlers !== null && !audioSelection;
58607
59633
  const showMotionGroup = showMotionTiming || showMotionEffects;
58608
59634
  const volumeAutomation = useVolumeAutomation(element, onSetAttributeQuiet ?? onSetAttributeLive);
58609
- const audioGroupLabel = useMemo51(() => {
59635
+ const audioGroupLabel = useMemo52(() => {
58610
59636
  const doc = element.element?.ownerDocument;
58611
59637
  const id = element.id;
58612
59638
  if (!doc || !id) return void 0;
@@ -59167,12 +60193,12 @@ var PropertyPanel = memo32(function PropertyPanel2(props) {
59167
60193
  const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
59168
60194
  const iframeRef = previewIframeRef ?? { current: null };
59169
60195
  const gsapAnimIdForMemo = element ? gsapAnimations?.find((a) => a.keyframes)?.id ?? gsapAnimations?.[0]?.id ?? null : null;
59170
- const gsapRuntimeValues = useMemo52(
60196
+ const gsapRuntimeValues = useMemo53(
59171
60197
  () => element ? readGsapRuntimeValuesForPanel(gsapAnimIdForMemo, gsapAnimations, element, iframeRef) : null,
59172
60198
  // eslint-disable-next-line react-hooks/exhaustive-deps -- iframeRef is stable; currentTime drives re-reads during playback
59173
60199
  [gsapAnimIdForMemo, gsapAnimations, element, currentTime]
59174
60200
  );
59175
- const gsapBorderRadius = useMemo52(
60201
+ const gsapBorderRadius = useMemo53(
59176
60202
  () => element ? readGsapBorderRadiusForPanel(gsapRuntimeValues, gsapAnimations, element, iframeRef) : null,
59177
60203
  // eslint-disable-next-line react-hooks/exhaustive-deps
59178
60204
  [gsapRuntimeValues, gsapAnimations, element, currentTime]
@@ -59562,11 +60588,11 @@ var PropertyPanel = memo32(function PropertyPanel2(props) {
59562
60588
  });
59563
60589
 
59564
60590
  // src/components/editor/FileTree.tsx
59565
- 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";
59566
60592
  import { Plus as Plus2, FolderSimplePlus as FolderSimplePlus2 } from "@phosphor-icons/react";
59567
60593
 
59568
60594
  // src/components/editor/FileTreeNodes.tsx
59569
- 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";
59570
60596
  import {
59571
60597
  PencilSimple,
59572
60598
  Copy,
@@ -60049,7 +61075,7 @@ var TreeFolder = memo33(function TreeFolder2({
60049
61075
  }) {
60050
61076
  const [isOpen, setIsOpen] = useState91(defaultOpen);
60051
61077
  const toggle = useCallback77(() => setIsOpen((v) => !v), []);
60052
- const children = useMemo53(() => sortChildren(node.children), [node.children]);
61078
+ const children = useMemo54(() => sortChildren(node.children), [node.children]);
60053
61079
  const Chevron = isOpen ? ChevronDown : ChevronRight;
60054
61080
  const isDragOver = dragOverFolder === node.fullPath;
60055
61081
  const isRenaming = inlineInput?.mode === "rename" && inlineInput.originalPath === node.fullPath;
@@ -60180,8 +61206,8 @@ var FileTree = memo34(function FileTree2({
60180
61206
  onImportFiles,
60181
61207
  lintFindingsByFile
60182
61208
  }) {
60183
- const tree = useMemo54(() => buildTree(files), [files]);
60184
- const children = useMemo54(() => sortChildren(tree.children), [tree]);
61209
+ const tree = useMemo55(() => buildTree(files), [files]);
61210
+ const children = useMemo55(() => sortChildren(tree.children), [tree]);
60185
61211
  const [contextMenu, setContextMenu] = useState92(null);
60186
61212
  const [inlineInput, setInlineInput] = useState92(null);
60187
61213
  const [deleteTarget, setDeleteTarget] = useState92(null);
@@ -60448,10 +61474,10 @@ var FileTree = memo34(function FileTree2({
60448
61474
  });
60449
61475
 
60450
61476
  // src/App.tsx
60451
- 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";
60452
61478
 
60453
61479
  // src/components/renders/useRenderQueue.ts
60454
- 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";
60455
61481
 
60456
61482
  // src/components/renders/serverError.ts
60457
61483
  async function readServerError(res) {
@@ -60975,7 +62001,7 @@ function useRenderQueue(projectId, activeCompPathRef) {
60975
62001
  };
60976
62002
  }, [projectId]);
60977
62003
  const isRendering = jobs.some((j) => j.status === "rendering");
60978
- return useMemo55(
62004
+ return useMemo56(
60979
62005
  () => ({
60980
62006
  jobs,
60981
62007
  isRendering,
@@ -61032,8 +62058,8 @@ function useDialogBehavior({
61032
62058
  const onCloseRef = useRef104(onClose);
61033
62059
  onCloseRef.current = onClose;
61034
62060
  const requestClose = useCallback81(() => {
61035
- const guard = canCloseRef.current;
61036
- if (guard && !guard()) return;
62061
+ const guard3 = canCloseRef.current;
62062
+ if (guard3 && !guard3()) return;
61037
62063
  onCloseRef.current();
61038
62064
  }, []);
61039
62065
  useEffect88(() => {
@@ -62242,7 +63268,7 @@ function useCaptionSync(projectId) {
62242
63268
  }
62243
63269
 
62244
63270
  // src/hooks/usePersistentEditHistory.ts
62245
- 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";
62246
63272
 
62247
63273
  // src/utils/editHistory.ts
62248
63274
  var DEFAULT_MAX_ENTRIES = 100;
@@ -62594,7 +63620,7 @@ function createPersistentEditHistoryStore({
62594
63620
  };
62595
63621
  }
62596
63622
  function usePersistentEditHistory(options) {
62597
- const storage = useMemo56(
63623
+ const storage = useMemo57(
62598
63624
  () => options.storage ?? createIndexedDbEditHistoryStorage(),
62599
63625
  [options.storage]
62600
63626
  );
@@ -62863,10 +63889,10 @@ function usePanelLayout(initialState2) {
62863
63889
  }
62864
63890
 
62865
63891
  // src/hooks/useFileManager.ts
62866
- 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";
62867
63893
 
62868
63894
  // src/hooks/useFileTree.ts
62869
- 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";
62870
63896
  function useFileTree({ projectId, projectIdRef }) {
62871
63897
  const [projectDir, setProjectDir] = useState101(null);
62872
63898
  const [fileTree, setFileTree] = useState101([]);
@@ -62902,11 +63928,11 @@ function useFileTree({ projectId, projectIdRef }) {
62902
63928
  if (data.files) setFileTree(data.files);
62903
63929
  }, [projectIdRef]);
62904
63930
  const compositions = compositionPaths;
62905
- const assets = useMemo57(
63931
+ const assets = useMemo58(
62906
63932
  () => fileTree.filter((f) => !f.endsWith(".html") && !f.endsWith(".md") && !f.endsWith(".json")),
62907
63933
  [fileTree]
62908
63934
  );
62909
- const fontAssets = useMemo57(
63935
+ const fontAssets = useMemo58(
62910
63936
  () => assets.filter((asset) => FONT_EXT.test(asset)).map((asset) => ({
62911
63937
  family: fontFamilyFromAssetPath(asset),
62912
63938
  path: asset,
@@ -62928,6 +63954,7 @@ function useFileTree({ projectId, projectIdRef }) {
62928
63954
 
62929
63955
  // src/hooks/useEditorSave.ts
62930
63956
  import { useCallback as useCallback87, useRef as useRef112 } from "react";
63957
+ var FAILURE_BURST_MS = 5e3;
62931
63958
  function useEditorSave({
62932
63959
  editingPathRef,
62933
63960
  projectIdRef,
@@ -62940,18 +63967,32 @@ function useEditorSave({
62940
63967
  }) {
62941
63968
  const saveRafRef = useRef112(null);
62942
63969
  const refreshRafRef = useRef112(null);
62943
- const lastFailureToastAtRef = useRef112(0);
63970
+ const lastFailureToastAtRef = useRef112(null);
63971
+ const lastFailureReportRef = useRef112(null);
62944
63972
  const pendingCandidateRef = useRef112(null);
62945
63973
  const inFlightRef = useRef112(null);
62946
63974
  const inFlightCandidateRef = useRef112(null);
62947
63975
  const reportFailure = useCallback87(
62948
63976
  (path, error) => {
62949
- trackStudioEvent("save_failure", {
63977
+ const now2 = Date.now();
63978
+ const properties = buildStudioSaveFailureProperties({
62950
63979
  source: "code_editor",
62951
- error_message: error instanceof Error ? error.message : "unknown"
63980
+ error,
63981
+ filePath: path
62952
63982
  });
62953
- const now2 = Date.now();
62954
- if (now2 - lastFailureToastAtRef.current > 5e3) {
63983
+ const errorName = error instanceof Error ? error.name : typeof error;
63984
+ const fingerprint = JSON.stringify([
63985
+ path,
63986
+ errorName,
63987
+ properties.error_message,
63988
+ properties.status_code
63989
+ ]);
63990
+ const previous = lastFailureReportRef.current;
63991
+ if (previous === null || previous.fingerprint !== fingerprint || now2 - previous.emittedAt >= FAILURE_BURST_MS) {
63992
+ trackStudioSaveFailure({ source: "code_editor", error, filePath: path });
63993
+ lastFailureReportRef.current = { fingerprint, emittedAt: now2 };
63994
+ }
63995
+ if (lastFailureToastAtRef.current === null || now2 - lastFailureToastAtRef.current >= FAILURE_BURST_MS) {
62955
63996
  lastFailureToastAtRef.current = now2;
62956
63997
  showToast(
62957
63998
  `Couldn't save ${path} \u2014 your latest edits are NOT persisted. Check the preview server; editing again retries the save.`,
@@ -62974,6 +64015,7 @@ function useEditorSave({
62974
64015
  recordEdit
62975
64016
  }).then(() => {
62976
64017
  if (pendingCandidateRef.current === candidate) pendingCandidateRef.current = null;
64018
+ lastFailureReportRef.current = null;
62977
64019
  if (refreshRafRef.current != null) cancelAnimationFrame(refreshRafRef.current);
62978
64020
  refreshRafRef.current = requestAnimationFrame(() => setRefreshKey((k) => k + 1));
62979
64021
  return { status: "clean" };
@@ -63053,7 +64095,7 @@ function useFileManager({
63053
64095
  const projectIdRef = useRef113(projectId);
63054
64096
  projectIdRef.current = projectId;
63055
64097
  const importedFontAssetsRef = useRef113([]);
63056
- const fileVersionScope = useMemo58(
64098
+ const fileVersionScope = useMemo59(
63057
64099
  () => ({ projectId, versions: /* @__PURE__ */ new Map() }),
63058
64100
  [projectId]
63059
64101
  );
@@ -63853,52 +64895,6 @@ import { useCallback as useCallback100, useRef as useRef118 } from "react";
63853
64895
  // src/hooks/useRazorSplit.ts
63854
64896
  import { useCallback as useCallback91, useRef as useRef116 } from "react";
63855
64897
 
63856
- // src/utils/projectRouting.ts
63857
- var PROJECT_HASH_PREFIX = "#project/";
63858
- function decodeHashProjectId(value) {
63859
- try {
63860
- return decodeURIComponent(value);
63861
- } catch {
63862
- return value;
63863
- }
63864
- }
63865
- function normalizeHashParams(params) {
63866
- if (!params) return new URLSearchParams();
63867
- if (params instanceof URLSearchParams) return params;
63868
- const next = new URLSearchParams();
63869
- for (const [key2, value] of Object.entries(params)) {
63870
- if (!key2 || value == null || value === "") continue;
63871
- next.set(key2, value);
63872
- }
63873
- return next;
63874
- }
63875
- function encodeProjectId(projectId) {
63876
- return encodeURIComponent(projectId);
63877
- }
63878
- function buildProjectHash(projectId, params) {
63879
- const search = normalizeHashParams(params).toString();
63880
- return `${PROJECT_HASH_PREFIX}${encodeProjectId(projectId)}${search ? `?${search}` : ""}`;
63881
- }
63882
- function parseProjectHashRoute(hash) {
63883
- if (!hash.startsWith(PROJECT_HASH_PREFIX)) return null;
63884
- const route = hash.slice(PROJECT_HASH_PREFIX.length);
63885
- const queryIndex = route.indexOf("?");
63886
- const encodedProjectId = queryIndex >= 0 ? route.slice(0, queryIndex) : route;
63887
- if (!encodedProjectId || encodedProjectId.includes("/")) return null;
63888
- const rawParams = queryIndex >= 0 ? route.slice(queryIndex + 1) : "";
63889
- return {
63890
- projectId: decodeHashProjectId(encodedProjectId),
63891
- params: new URLSearchParams(rawParams)
63892
- };
63893
- }
63894
- function parseProjectIdFromHash(hash) {
63895
- return parseProjectHashRoute(hash)?.projectId ?? null;
63896
- }
63897
- function buildProjectApiPath(projectId, suffix = "") {
63898
- const normalizedSuffix = suffix && !suffix.startsWith("/") ? `/${suffix}` : suffix;
63899
- return `/api/projects/${encodeProjectId(projectId)}${normalizedSuffix}`;
63900
- }
63901
-
63902
64898
  // src/utils/razorSplitTransaction.ts
63903
64899
  function targetIdentity(path, target) {
63904
64900
  if (target.hfId) return `${path}|hf:${target.hfId}`;
@@ -68603,7 +69599,7 @@ function useGroupCommits(params) {
68603
69599
  }
68604
69600
 
68605
69601
  // src/hooks/useGsapScriptCommits.ts
68606
- 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";
68607
69603
  import { findUnsafeMutationValues } from "@hyperframes/core/studio-api/finite-mutation";
68608
69604
 
68609
69605
  // src/hooks/gsapRuntimePatch.ts
@@ -69918,7 +70914,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
69918
70914
  const { instantPatch: _instantPatch, ...batchOptions } = options;
69919
70915
  await finalizeSuccessfulMutation(pid, compositionPath, last.selection, last.mutation, targetPath, result, instantPatches.length > 0 ? { ...batchOptions, instantPatches } : batchOptions);
69920
70916
  }, [showToast, finalizeSuccessfulMutation]);
69921
- const commitMutation = useMemo59(() => {
70917
+ const commitMutation = useMemo60(() => {
69922
70918
  const serializeFile = (file, task) => {
69923
70919
  if (writeProjectFile) {
69924
70920
  return serializeStudioFileMutation(writeProjectFile, file, task);
@@ -69983,7 +70979,7 @@ function useGsapScriptCommits({ projectIdRef, activeCompPath, previewIframeRef,
69983
70979
  },
69984
70980
  [activeProjectId]
69985
70981
  );
69986
- const sdkDeps = useMemo59(
70982
+ const sdkDeps = useMemo60(
69987
70983
  () => writeProjectFile ? {
69988
70984
  editHistory: { recordEdit: editHistory.recordEdit },
69989
70985
  writeProjectFile,
@@ -70189,7 +71185,8 @@ import { useCallback as useCallback117 } from "react";
70189
71185
  function useGsapInteractionFailureTelemetry(activeCompPath, showToast) {
70190
71186
  return useCallback117(
70191
71187
  (error, selection, mutationType, label) => {
70192
- trackStudioSaveFailure({
71188
+ const report = isGsapEditBlockedError(error) ? trackStudioEditBlocked : trackStudioSaveFailure;
71189
+ report({
70193
71190
  source: "gsap_commit",
70194
71191
  error,
70195
71192
  filePath: selection?.sourceFile ?? activeCompPath ?? "index.html",
@@ -72977,7 +73974,7 @@ function useStudioExternalFileChanges({
72977
73974
  }
72978
73975
 
72979
73976
  // src/hooks/useBlockHandlers.ts
72980
- 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";
72981
73978
 
72982
73979
  // src/utils/blockInstaller.ts
72983
73980
  function getMaxZIndexFromIframe(iframe) {
@@ -73163,7 +74160,7 @@ function useBlockHandlers({
73163
74160
  setRightPanelTab
73164
74161
  }) {
73165
74162
  const [activeBlockParams, setActiveBlockParams] = useState109(null);
73166
- const blockCtx = useMemo60(
74163
+ const blockCtx = useMemo61(
73167
74164
  () => ({
73168
74165
  activeCompPath: blockCtxDeps.activeCompPath,
73169
74166
  timelineElements: blockCtxDeps.timelineElements,
@@ -74248,7 +75245,7 @@ import { useCallback as useCallback132 } from "react";
74248
75245
  import { createElement as createElement3 } from "react";
74249
75246
 
74250
75247
  // src/player/components/AudioWaveform.tsx
74251
- 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";
74252
75249
  import { jsx as jsx167, jsxs as jsxs139 } from "react/jsx-runtime";
74253
75250
  var BAR_WIDTH = 2;
74254
75251
  var BAR_STEP = 3;
@@ -74308,7 +75305,7 @@ var AudioWaveform = memo36(function AudioWaveform2({
74308
75305
  const canvasRef = useRef137(null);
74309
75306
  const observerRef = useRef137(null);
74310
75307
  const cacheKey = waveformUrl ?? audioUrl;
74311
- const request = useMemo61(
75308
+ const request = useMemo62(
74312
75309
  () => ({
74313
75310
  key: createThumbnailKey({ kind: "waveform", source: cacheKey }),
74314
75311
  projectId,
@@ -74417,7 +75414,7 @@ var AudioWaveform = memo36(function AudioWaveform2({
74417
75414
  });
74418
75415
 
74419
75416
  // src/player/components/ImageThumbnail.tsx
74420
- 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";
74421
75418
  import { jsx as jsx168, jsxs as jsxs140 } from "react/jsx-runtime";
74422
75419
  var ImageThumbnail = memo37(function ImageThumbnail2({
74423
75420
  imageSrc,
@@ -74430,7 +75427,7 @@ var ImageThumbnail = memo37(function ImageThumbnail2({
74430
75427
  }) {
74431
75428
  const [containerWidth, setContainerWidth] = useState110(0);
74432
75429
  const observerRef = useRef138(null);
74433
- const request = useMemo62(
75430
+ const request = useMemo63(
74434
75431
  () => ({
74435
75432
  key: createThumbnailKey({ kind: "image", source: imageSrc, rich: Number(rich) }),
74436
75433
  projectId,
@@ -74747,36 +75744,6 @@ function useConsoleErrorCapture(previewIframe) {
74747
75744
 
74748
75745
  // src/hooks/useFrameCapture.ts
74749
75746
  import { useState as useState112, useCallback as useCallback134, useRef as useRef140 } from "react";
74750
-
74751
- // src/utils/frameCapture.ts
74752
- function normalizeCompositionPath(compositionPath) {
74753
- return compositionPath && compositionPath !== "master" ? compositionPath : "index.html";
74754
- }
74755
- function buildFrameCaptureUrl({
74756
- projectId,
74757
- compositionPath,
74758
- currentTime,
74759
- origin = window.location.origin
74760
- }) {
74761
- const compPath = normalizeCompositionPath(compositionPath);
74762
- const url = new URL(
74763
- buildProjectApiPath(projectId, `/thumbnail/${encodeURIComponent(compPath)}`),
74764
- origin
74765
- );
74766
- url.searchParams.set("t", Math.max(0, currentTime).toFixed(3));
74767
- url.searchParams.set("format", "png");
74768
- url.searchParams.set("output", "source");
74769
- url.searchParams.set("v", String(Date.now()));
74770
- return url.toString();
74771
- }
74772
- function buildFrameCaptureFilename(compositionPath, currentTime) {
74773
- const compPath = normalizeCompositionPath(compositionPath);
74774
- const base2 = compPath.split("/").pop()?.replace(/\.html$/i, "") || "frame";
74775
- const frameTime = Math.max(0, currentTime).toFixed(3).replace(".", "-");
74776
- return `${base2}-${frameTime}s.png`;
74777
- }
74778
-
74779
- // src/hooks/useFrameCapture.ts
74780
75747
  function useFrameCapture({
74781
75748
  projectId,
74782
75749
  activeCompPath,
@@ -74870,7 +75837,7 @@ function useFrameCapture({
74870
75837
  }
74871
75838
 
74872
75839
  // src/hooks/useLintModal.ts
74873
- 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";
74874
75841
  function parseFinding(f) {
74875
75842
  return {
74876
75843
  severity: f.severity === "error" ? "error" : "warning",
@@ -74942,8 +75909,8 @@ function useLintModal(projectId, refreshKey) {
74942
75909
  },
74943
75910
  [backgroundFindings]
74944
75911
  );
74945
- const findingsByElement = useMemo63(() => groupFindings((f) => f.elementId), [groupFindings]);
74946
- 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]);
74947
75914
  useEffect108(() => {
74948
75915
  usePlayerStore.getState().setLintFindingsByElement(findingsByElement);
74949
75916
  }, [findingsByElement]);
@@ -75425,16 +76392,16 @@ function useStudioUrlState({
75425
76392
  }
75426
76393
 
75427
76394
  // src/hooks/useEffectiveTimelineDuration.ts
75428
- import { useMemo as useMemo64 } from "react";
76395
+ import { useMemo as useMemo65 } from "react";
75429
76396
  function useEffectiveTimelineDuration(timelineDuration2, timelineElements) {
75430
- return useMemo64(
76397
+ return useMemo65(
75431
76398
  () => getEffectiveTimelineDuration(timelineDuration2, timelineElements),
75432
76399
  [timelineDuration2, timelineElements]
75433
76400
  );
75434
76401
  }
75435
76402
 
75436
76403
  // src/hooks/useStudioContextValue.ts
75437
- 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";
75438
76405
  function buildStudioContextValue(input) {
75439
76406
  return {
75440
76407
  projectId: input.projectId,
@@ -75449,6 +76416,9 @@ function buildStudioContextValue(input) {
75449
76416
  timelineElements: input.timelineElements,
75450
76417
  isPlaying: input.isPlaying,
75451
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,
75452
76422
  handleUndo: input.handleUndo,
75453
76423
  handleRedo: input.handleRedo,
75454
76424
  renderQueue: input.renderQueue,
@@ -75459,7 +76429,7 @@ function buildStudioContextValue(input) {
75459
76429
  };
75460
76430
  }
75461
76431
  function useInspectorState(rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, domEditSelection, isGestureRecording) {
75462
- return useMemo65(() => {
76432
+ return useMemo66(() => {
75463
76433
  const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";
75464
76434
  const layersPanelActive = inspectorTabActive && rightInspectorPanes.layers;
75465
76435
  const designPanelActive = inspectorTabActive && rightInspectorPanes.design;
@@ -75532,7 +76502,7 @@ function useGlobalFileDrop(handleTimelineFileDrop) {
75532
76502
  import { useRef as useRef146 } from "react";
75533
76503
 
75534
76504
  // src/contexts/PanelLayoutContext.tsx
75535
- import { useContext as useContext8, useMemo as useMemo66 } from "react";
76505
+ import { useContext as useContext8, useMemo as useMemo67 } from "react";
75536
76506
  import { jsx as jsx169 } from "react/jsx-runtime";
75537
76507
  var PanelLayoutContext = createStableContext("PanelLayoutContext", null);
75538
76508
  function usePanelLayoutContext() {
@@ -75562,7 +76532,7 @@ function PanelLayoutProvider({
75562
76532
  },
75563
76533
  children
75564
76534
  }) {
75565
- const stable = useMemo66(
76535
+ const stable = useMemo67(
75566
76536
  () => ({
75567
76537
  leftWidth,
75568
76538
  rightWidth,
@@ -75610,7 +76580,7 @@ import {
75610
76580
  useCallback as useCallback140,
75611
76581
  useContext as useContext9,
75612
76582
  useEffect as useEffect110,
75613
- useMemo as useMemo67,
76583
+ useMemo as useMemo68,
75614
76584
  useRef as useRef145,
75615
76585
  useState as useState117
75616
76586
  } from "react";
@@ -75642,8 +76612,8 @@ function useViewModeState() {
75642
76612
  const guardsRef = useRef145(/* @__PURE__ */ new Set());
75643
76613
  const acceptedLocationRef = useRef145(readHistoryLocation());
75644
76614
  const canSetViewMode = useCallback140((mode) => {
75645
- for (const guard of guardsRef.current) {
75646
- if (!guard(mode)) return false;
76615
+ for (const guard3 of guardsRef.current) {
76616
+ if (!guard3(mode)) return false;
75647
76617
  }
75648
76618
  return true;
75649
76619
  }, []);
@@ -75673,13 +76643,13 @@ function useViewModeState() {
75673
76643
  },
75674
76644
  [canSetViewMode]
75675
76645
  );
75676
- const registerViewModeGuard = useCallback140((guard) => {
75677
- guardsRef.current.add(guard);
76646
+ const registerViewModeGuard = useCallback140((guard3) => {
76647
+ guardsRef.current.add(guard3);
75678
76648
  return () => {
75679
- guardsRef.current.delete(guard);
76649
+ guardsRef.current.delete(guard3);
75680
76650
  };
75681
76651
  }, []);
75682
- return useMemo67(
76652
+ return useMemo68(
75683
76653
  () => ({ viewMode, setViewMode, registerViewModeGuard }),
75684
76654
  [viewMode, setViewMode, registerViewModeGuard]
75685
76655
  );
@@ -76869,7 +77839,7 @@ function useGestureCommit({
76869
77839
  }
76870
77840
 
76871
77841
  // src/components/editor/GestureTrailOverlay.tsx
76872
- import { memo as memo38, useMemo as useMemo68 } from "react";
77842
+ import { memo as memo38, useMemo as useMemo69 } from "react";
76873
77843
  import { Fragment as Fragment49, jsx as jsx172, jsxs as jsxs142 } from "react/jsx-runtime";
76874
77844
  var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76875
77845
  samples,
@@ -76881,7 +77851,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76881
77851
  mode,
76882
77852
  accentColor = "#3CE6AC"
76883
77853
  }) {
76884
- const trailPoints = useMemo68(() => {
77854
+ const trailPoints = useMemo69(() => {
76885
77855
  if (!canvasRect) return "";
76886
77856
  if (trail2 && trail2.length > 1) {
76887
77857
  return trail2.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
@@ -76889,7 +77859,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76889
77859
  if (samples.length === 0) return "";
76890
77860
  return samples.filter((s) => s.properties.x != null && s.properties.y != null).map((s) => `${s.properties.x},${s.properties.y}`).join(" ");
76891
77861
  }, [samples, trail2, sampleCount, canvasRect?.left, canvasRect?.top]);
76892
- const simplifiedPath = useMemo68(() => {
77862
+ const simplifiedPath = useMemo69(() => {
76893
77863
  if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
76894
77864
  const pts = [];
76895
77865
  for (const [pct, props] of simplifiedPoints) {
@@ -76901,7 +77871,7 @@ var GestureTrailOverlay = memo38(function GestureTrailOverlay2({
76901
77871
  if (pts.length === 0) return "";
76902
77872
  return pts.map((p) => `${p.x},${p.y}`).join(" ");
76903
77873
  }, [simplifiedPoints]);
76904
- const diamondPositions = useMemo68(() => {
77874
+ const diamondPositions = useMemo69(() => {
76905
77875
  if (!simplifiedPoints || simplifiedPoints.size === 0) return [];
76906
77876
  const pts = [];
76907
77877
  for (const [pct, props] of simplifiedPoints) {
@@ -77300,7 +78270,7 @@ var CompositionsTab = memo39(function CompositionsTab2({
77300
78270
  });
77301
78271
 
77302
78272
  // src/components/sidebar/AssetsTab.tsx
77303
- 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";
77304
78274
 
77305
78275
  // src/components/ui/SearchInput.tsx
77306
78276
  import { jsx as jsx174, jsxs as jsxs144 } from "react/jsx-runtime";
@@ -77912,7 +78882,7 @@ function AudioRow({
77912
78882
  }
77913
78883
 
77914
78884
  // src/components/sidebar/GlobalAssetsView.tsx
77915
- 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";
77916
78886
  import { jsx as jsx177, jsxs as jsxs147 } from "react/jsx-runtime";
77917
78887
  function globalAssetRows(records, query = "") {
77918
78888
  const q = query.trim().toLowerCase();
@@ -77939,7 +78909,7 @@ function GlobalAssetsView({ searchQuery }) {
77939
78909
  cancelled = true;
77940
78910
  };
77941
78911
  }, []);
77942
- const rows = useMemo69(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
78912
+ const rows = useMemo70(() => globalAssetRows(records ?? [], searchQuery), [records, searchQuery]);
77943
78913
  if (records === null) {
77944
78914
  return /* @__PURE__ */ jsx177("p", { className: "px-4 py-3 text-[11px] text-panel-text-5", children: "Loading global assets\u2026" });
77945
78915
  }
@@ -78498,12 +79468,12 @@ var AssetsTab = memo40(function AssetsTab2({
78498
79468
  setTimeout(() => setCopyFeedback(null), copied ? 1500 : 3e3);
78499
79469
  }, []);
78500
79470
  const elements = usePlayerStore((s) => s.elements);
78501
- const usedPaths = useMemo70(() => deriveUsedPaths(elements), [elements]);
78502
- const allMediaAssets = useMemo70(
79471
+ const usedPaths = useMemo71(() => deriveUsedPaths(elements), [elements]);
79472
+ const allMediaAssets = useMemo71(
78503
79473
  () => assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
78504
79474
  [assets]
78505
79475
  );
78506
- const mediaAssets = useMemo70(() => {
79476
+ const mediaAssets = useMemo71(() => {
78507
79477
  const all = filterByUsage(allMediaAssets, usedPaths, usageFilter);
78508
79478
  if (!searchQuery) return all;
78509
79479
  const q = searchQuery.toLowerCase();
@@ -78513,7 +79483,7 @@ var AssetsTab = memo40(function AssetsTab2({
78513
79483
  return rec?.description?.toLowerCase().includes(q);
78514
79484
  });
78515
79485
  }, [allMediaAssets, searchQuery, manifest, usageFilter, usedPaths]);
78516
- const categorized = useMemo70(() => {
79486
+ const categorized = useMemo71(() => {
78517
79487
  const groups = { audio: [], images: [], video: [], fonts: [] };
78518
79488
  for (const a of mediaAssets) {
78519
79489
  const cat = getCategory(a);
@@ -78528,12 +79498,12 @@ var AssetsTab = memo40(function AssetsTab2({
78528
79498
  }
78529
79499
  return groups;
78530
79500
  }, [mediaAssets, usedPaths]);
78531
- const counts = useMemo70(() => {
79501
+ const counts = useMemo71(() => {
78532
79502
  const c = { all: mediaAssets.length };
78533
79503
  for (const cat of FILTER_ORDER) c[cat] = categorized[cat].length;
78534
79504
  return c;
78535
79505
  }, [mediaAssets, categorized]);
78536
- const usageCounts = useMemo70(
79506
+ const usageCounts = useMemo71(
78537
79507
  () => countUsage(
78538
79508
  assets.filter((a) => MEDIA_EXT.test(a) || FONT_EXT.test(a)),
78539
79509
  usedPaths
@@ -82896,7 +83866,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
82896
83866
  }
82897
83867
 
82898
83868
  // src/components/panels/VariablesPanel.tsx
82899
- 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";
82900
83870
 
82901
83871
  // src/hooks/useVariablesPersist.ts
82902
83872
  import { useCallback as useCallback160 } from "react";
@@ -83600,22 +84570,22 @@ var VariablesPanel = memo51(function VariablesPanel2({
83600
84570
  domEditSaveTimestampRef,
83601
84571
  publishSdkSession
83602
84572
  });
83603
- const declarations = useMemo71(
84573
+ const declarations = useMemo72(
83604
84574
  () => sdkSession?.getVariableDeclarations() ?? [],
83605
84575
  // eslint-disable-next-line react-hooks/exhaustive-deps
83606
84576
  [sdkSession, refreshKey, revision]
83607
84577
  );
83608
- const usage = useMemo71(
84578
+ const usage = useMemo72(
83609
84579
  () => sdkSession?.getVariableUsage() ?? null,
83610
84580
  // eslint-disable-next-line react-hooks/exhaustive-deps
83611
84581
  [sdkSession, refreshKey, revision]
83612
84582
  );
83613
- const issues = useMemo71(
84583
+ const issues = useMemo72(
83614
84584
  () => previewValues && sdkSession ? sdkSession.validateVariableValues(previewValues) : [],
83615
84585
  // eslint-disable-next-line react-hooks/exhaustive-deps
83616
84586
  [sdkSession, previewValues, refreshKey, revision]
83617
84587
  );
83618
- const effectiveValues = useMemo71(
84588
+ const effectiveValues = useMemo72(
83619
84589
  () => sdkSession?.getVariableValues(previewValues ?? void 0) ?? {},
83620
84590
  // eslint-disable-next-line react-hooks/exhaustive-deps
83621
84591
  [sdkSession, previewValues, refreshKey, revision]
@@ -83954,15 +84924,15 @@ function useSlideshowPersist({
83954
84924
  }
83955
84925
 
83956
84926
  // src/hooks/useSlideshowTabState.ts
83957
- import { useEffect as useEffect130, useMemo as useMemo72 } from "react";
84927
+ import { useEffect as useEffect130, useMemo as useMemo73 } from "react";
83958
84928
  import { SLIDESHOW_ISLAND_TYPE as SLIDESHOW_ISLAND_TYPE2, slideshowIslandRegex as slideshowIslandRegex2 } from "@hyperframes/core/slideshow";
83959
84929
  function useSlideshowTabState(params) {
83960
84930
  const { editingFileContent, previewIframeRef, refreshKey, rightPanelTab, setRightPanelTab } = params;
83961
- const isSlideshowComposition = useMemo72(() => {
84931
+ const isSlideshowComposition = useMemo73(() => {
83962
84932
  if (!editingFileContent || !editingFileContent.includes(SLIDESHOW_ISLAND_TYPE2)) return false;
83963
84933
  return slideshowIslandRegex2("i").test(editingFileContent);
83964
84934
  }, [editingFileContent]);
83965
- const slideshowScenes = useMemo72(() => {
84935
+ const slideshowScenes = useMemo73(() => {
83966
84936
  try {
83967
84937
  const win = previewIframeRef.current?.contentWindow;
83968
84938
  return (win?.__clipManifest?.scenes ?? []).map((s) => ({
@@ -85634,7 +86604,7 @@ function useProjectSignaturePoll(projectId, currentSignature, onChange) {
85634
86604
  }
85635
86605
 
85636
86606
  // src/components/storyboard/StoryboardLoaded.tsx
85637
- 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";
85638
86608
 
85639
86609
  // src/components/storyboard/StoryboardDirection.tsx
85640
86610
  import { jsx as jsx205, jsxs as jsxs170 } from "react/jsx-runtime";
@@ -85871,7 +86841,7 @@ function StoryboardScriptPanel({ script }) {
85871
86841
  }
85872
86842
 
85873
86843
  // src/components/storyboard/StoryboardSourceEditor.tsx
85874
- 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";
85875
86845
  import { marked } from "marked";
85876
86846
  import DOMPurify from "dompurify";
85877
86847
  import { jsx as jsx210, jsxs as jsxs173 } from "react/jsx-runtime";
@@ -85930,7 +86900,7 @@ function useMarkdownPreview(source) {
85930
86900
  const id = window.setTimeout(() => setDebounced(source), 200);
85931
86901
  return () => window.clearTimeout(id);
85932
86902
  }, [source]);
85933
- return useMemo73(() => {
86903
+ return useMemo74(() => {
85934
86904
  const html2 = marked.parse(debounced, { async: false });
85935
86905
  DOMPurify.addHook("afterSanitizeAttributes", hardenLinks);
85936
86906
  try {
@@ -86602,7 +87572,7 @@ function progressLabel(summary) {
86602
87572
  }
86603
87573
 
86604
87574
  // src/components/storyboard/useFrameComments.ts
86605
- 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";
86606
87576
 
86607
87577
  // src/components/storyboard/frameComments.ts
86608
87578
  var FRAME_COMMENTS_PATH = ".hyperframes/frame-comments.json";
@@ -86684,7 +87654,7 @@ function useFrameComments(frames) {
86684
87654
  const setDraft = useCallback174((index, text) => {
86685
87655
  setDrafts((prev) => ({ ...prev, [index]: text }));
86686
87656
  }, []);
86687
- const draftCount = useMemo74(
87657
+ const draftCount = useMemo75(
86688
87658
  () => Object.values(drafts).filter((text) => text.trim().length > 0).length,
86689
87659
  [drafts]
86690
87660
  );
@@ -86752,7 +87722,7 @@ function StoryboardLoaded({
86752
87722
  setFeedbackMessageCopied(false);
86753
87723
  }
86754
87724
  };
86755
- const sourceFiles = useMemo75(() => {
87725
+ const sourceFiles = useMemo76(() => {
86756
87726
  const files = [{ path: data.path, label: data.path }];
86757
87727
  if (data.script?.exists) files.push({ path: data.script.path, label: data.script.path });
86758
87728
  return files;
@@ -87238,7 +88208,7 @@ function StudioApp() {
87238
88208
  domEditSaveTimestampRef,
87239
88209
  setRefreshKey
87240
88210
  });
87241
- const masterCompPath = useMemo76(
88211
+ const masterCompPath = useMemo77(
87242
88212
  () => resolveMasterCompositionPath(fileManager.fileTree),
87243
88213
  [fileManager.fileTree]
87244
88214
  );
@@ -87558,11 +88528,13 @@ function StudioApp() {
87558
88528
  handleRedo: appHotkeys.handleRedo,
87559
88529
  renderQueue,
87560
88530
  compositionDimensions,
88531
+ domEditSaveQueuePaused: previewPersistence.domEditSaveQueuePaused,
88532
+ externalFileConflict: externalFileChanges.blocked !== null,
87561
88533
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
87562
88534
  handlePreviewIframeRef,
87563
88535
  refreshPreviewDocumentVersion
87564
88536
  });
87565
- const timelineToolbar = useMemo76(
88537
+ const timelineToolbar = useMemo77(
87566
88538
  () => /* @__PURE__ */ jsx217(
87567
88539
  TimelineToolbar,
87568
88540
  {