@hyperframes/studio 0.8.133 → 0.8.135

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 (72) hide show
  1. package/dist/assets/{hyperframes-player-Cjcwveg0.js → hyperframes-player-DFv9heE6.js} +1 -1
  2. package/dist/assets/{index-CwgWmfgx.js → index-Buy06mZy.js} +1 -1
  3. package/dist/assets/{index-ME-Z5jh6.js → index-CTp4loy9.js} +1 -1
  4. package/dist/assets/{index-Dj21zXWA.js → index-N_4Ij4X6.js} +184 -183
  5. package/dist/assets/{index-D11atkBt.css → index-syMK0_9C.css} +1 -1
  6. package/dist/{chunk-5VSXRGIX.js → chunk-NM7BXNZ7.js} +1 -1
  7. package/dist/chunk-NM7BXNZ7.js.map +1 -0
  8. package/dist/{chunk-FOEID72M.js → chunk-QRLKWENQ.js} +2 -2
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +911 -691
  12. package/dist/index.js.map +1 -1
  13. package/dist/player.js +2 -2
  14. package/dist/styles.css +1 -1
  15. package/dist/ui.d.ts +3 -2
  16. package/dist/ui.js +1 -1
  17. package/package.json +8 -8
  18. package/src/components/editor/AnimationCard.test.tsx +60 -0
  19. package/src/components/editor/AnimationCard.tsx +16 -6
  20. package/src/components/editor/KeyframeEaseList.tsx +4 -1
  21. package/src/components/editor/LayersPanel.tsx +39 -47
  22. package/src/components/editor/domEditInlineText.ts +1 -1
  23. package/src/components/editor/gsapAnimationHelpers.test.ts +29 -0
  24. package/src/components/editor/gsapAnimationHelpers.ts +34 -0
  25. package/src/components/editor/layerKind.test.ts +38 -0
  26. package/src/components/editor/layerKind.ts +19 -0
  27. package/src/components/editor/manualEditsDom.ts +5 -1
  28. package/src/components/editor/plainTranslate.test.ts +21 -2
  29. package/src/components/editor/plainTranslate.ts +7 -0
  30. package/src/components/editor/useDomEditNudge.test.tsx +48 -27
  31. package/src/components/ui/cn.ts +2 -1
  32. package/src/contexts/DomEditContext.tsx +3 -1
  33. package/src/hooks/draggedGsapPosition.test.ts +34 -0
  34. package/src/hooks/draggedGsapPosition.ts +40 -15
  35. package/src/hooks/gsapDragCommit.ts +17 -10
  36. package/src/hooks/gsapDragPositionCommit.ts +8 -9
  37. package/src/hooks/gsapPlaybackTestHarness.ts +28 -0
  38. package/src/hooks/gsapResizeIntercept.test.ts +72 -1
  39. package/src/hooks/gsapResizeIntercept.ts +84 -17
  40. package/src/hooks/gsapRuntimeBridge.ts +4 -1
  41. package/src/hooks/gsapRuntimePatch.ts +4 -0
  42. package/src/hooks/gsapShared.test.ts +11 -0
  43. package/src/hooks/gsapShared.ts +1 -1
  44. package/src/hooks/gsapTrailingKeyframes.realGsap.test.ts +44 -0
  45. package/src/hooks/keyframeEase.realGsap.test.ts +37 -0
  46. package/src/hooks/useAnimatedPropertyCommit.test.tsx +33 -0
  47. package/src/hooks/useAnimatedPropertyCommit.ts +3 -1
  48. package/src/hooks/useDomGeometryCommits.test.tsx +21 -0
  49. package/src/hooks/useDomGeometryCommits.ts +6 -1
  50. package/src/hooks/useEditHistoryActions.test.tsx +20 -0
  51. package/src/hooks/useEditHistoryActions.ts +3 -0
  52. package/src/hooks/useEnableKeyframes.ts +32 -37
  53. package/src/hooks/useGestureCommit.test.tsx +89 -0
  54. package/src/hooks/useGestureCommit.ts +26 -9
  55. package/src/hooks/useGsapAwareEditing.test.tsx +1 -0
  56. package/src/hooks/useGsapAwareEditing.ts +10 -2
  57. package/src/icons/SystemIcons.tsx +6 -0
  58. package/src/layersPanelExport.test.tsx +45 -4
  59. package/src/utils/elementGsap.ts +6 -5
  60. package/src/utils/gsapKeyframeEases.ts +2 -0
  61. package/src/utils/gsapLiveRetime.cache.test.ts +39 -0
  62. package/src/utils/gsapLiveRetime.realGsap.test.ts +39 -15
  63. package/src/utils/gsapLiveRetime.ts +39 -31
  64. package/src/utils/gsapSoftReload.test.ts +98 -14
  65. package/src/utils/gsapSoftReload.ts +14 -2
  66. package/src/utils/gsapUndoRestore.test.ts +43 -2
  67. package/src/utils/gsapUndoRestore.ts +4 -2
  68. package/src/utils/softReloadTargets.ts +56 -0
  69. package/src/utils/studioPendingEdits.test.ts +123 -0
  70. package/src/utils/studioPendingEdits.ts +39 -3
  71. package/dist/chunk-5VSXRGIX.js.map +0 -1
  72. /package/dist/{chunk-FOEID72M.js.map → chunk-QRLKWENQ.js.map} +0 -0
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  usePreviewVariablesStore,
40
40
  whenPreviewBooted,
41
41
  writeStudioUiPreferences
42
- } from "./chunk-FOEID72M.js";
42
+ } from "./chunk-QRLKWENQ.js";
43
43
  import {
44
44
  Button,
45
45
  ContextMenu,
@@ -71,7 +71,7 @@ import {
71
71
  fieldBase,
72
72
  fieldText,
73
73
  popupSurface
74
- } from "./chunk-5VSXRGIX.js";
74
+ } from "./chunk-NM7BXNZ7.js";
75
75
  import {
76
76
  DOM_EDIT_LAYER_OBSERVER_INIT,
77
77
  STUDIO_FLAT_INSPECTOR_ENABLED,
@@ -4504,6 +4504,7 @@ function timingEase(runEase) {
4504
4504
  }
4505
4505
  return timingEases.get(runEase) ?? null;
4506
4506
  }
4507
+ var warpsTime = (runEase) => timingEase(runEase) !== null;
4507
4508
  function progressAtTime(runEase, timePercentage) {
4508
4509
  const run = timingEase(runEase);
4509
4510
  return run ? run(timePercentage / 100) * 100 : timePercentage;
@@ -4592,7 +4593,7 @@ var PROPERTY_DEFAULTS = {
4592
4593
  };
4593
4594
  function keyframeEases(anim) {
4594
4595
  if (!anim.keyframes) return { easeEach: anim.ease };
4595
- return { ease: anim.ease, easeEach: anim.keyframes.easeEach };
4596
+ return { ease: runEaseOf(anim), easeEach: anim.keyframes.easeEach };
4596
4597
  }
4597
4598
  function isInstantHold(animation) {
4598
4599
  return animation.method === "set" || (animation.method === "to" || animation.method === "fromTo") && resolveTweenDuration(animation) === 0;
@@ -9465,15 +9466,24 @@ function focusedField() {
9465
9466
  function hasStudioPendingEdits() {
9466
9467
  return pendingEdits.size > 0 || focusedField() !== null;
9467
9468
  }
9469
+ var shownRestores = /* @__PURE__ */ new Set();
9470
+ function beginStudioShownRestore() {
9471
+ let end = () => {
9472
+ };
9473
+ const landed = new Promise((resolve) => end = resolve);
9474
+ shownRestores.add(landed);
9475
+ void landed.then(() => shownRestores.delete(landed));
9476
+ return end;
9477
+ }
9468
9478
  function isStudioEditSaving() {
9469
- return pendingEdits.size > 0;
9479
+ return pendingEdits.size > 0 || shownRestores.size > 0;
9470
9480
  }
9471
9481
  function afterStudioPendingEdits(run) {
9472
9482
  let waiting = true;
9473
9483
  const check = () => {
9474
9484
  if (!waiting) return;
9475
9485
  if (isStudioEditSaving()) {
9476
- void Promise.allSettled([...pendingEdits.keys()]).then(check);
9486
+ void Promise.allSettled([...pendingEdits.keys(), ...shownRestores]).then(check);
9477
9487
  return;
9478
9488
  }
9479
9489
  waiting = false;
@@ -9501,14 +9511,35 @@ function trackStudioPendingEdit(result) {
9501
9511
  );
9502
9512
  return promise;
9503
9513
  }
9504
- function trackedStudioEdit(edit) {
9514
+ var flushedSavesStillWriting = null;
9515
+ function commitOlderDebouncedEdits() {
9516
+ if (typeof window === "undefined" || adopting) return;
9517
+ const detail = { promises: [] };
9518
+ window.dispatchEvent(
9519
+ new CustomEvent(STUDIO_FLUSH_PENDING_EDITS_EVENT, { detail })
9520
+ );
9521
+ if (!detail.promises.length) return;
9522
+ const saves = Promise.allSettled([flushedSavesStillWriting, ...detail.promises]);
9523
+ flushedSavesStillWriting = saves;
9524
+ void saves.then(() => {
9525
+ if (flushedSavesStillWriting === saves) flushedSavesStillWriting = null;
9526
+ });
9527
+ }
9528
+ function afterOlderSaves(run) {
9529
+ return flushedSavesStillWriting && flushedSavesStillWriting.then(run);
9530
+ }
9531
+ function trackedStudioEdit(edit, { afterOlderSaves: waits = false } = {}) {
9505
9532
  return (...args) => {
9533
+ commitOlderDebouncedEdits();
9534
+ const deferred = waits && !adopting ? afterOlderSaves(() => edit(...args)) : null;
9535
+ if (deferred) return trackStudioPendingEdit(deferred);
9506
9536
  const result = edit(...args);
9507
9537
  if (result instanceof Promise) trackStudioPendingEdit(result);
9508
9538
  return result;
9509
9539
  };
9510
9540
  }
9511
9541
  function beginStudioPendingEdit(revert) {
9542
+ commitOlderDebouncedEdits();
9512
9543
  let settle;
9513
9544
  const promise = trackStudioPendingEdit(new Promise((resolve) => settle = resolve));
9514
9545
  const entry2 = pendingEdits.get(promise);
@@ -9548,7 +9579,8 @@ function beginStudioPendingEdit(revert) {
9548
9579
  // Only what `start` registers synchronously is adopted; a later write joins only through `within`.
9549
9580
  adopt(start) {
9550
9581
  try {
9551
- const committed = inFlight.within(start);
9582
+ const deferred = afterOlderSaves(() => inFlight.within(start));
9583
+ const committed = deferred ?? inFlight.within(start);
9552
9584
  landed = Promise.resolve(committed).then(
9553
9585
  () => true,
9554
9586
  () => saved2
@@ -12133,7 +12165,9 @@ function trackEditCommits(actions) {
12133
12165
  const tracked = { ...actions };
12134
12166
  for (const key2 of Object.keys(SAVES_AN_EDIT)) {
12135
12167
  if (!SAVES_AN_EDIT[key2]) continue;
12136
- tracked[key2] = trackedStudioEdit(actions[key2]);
12168
+ tracked[key2] = trackedStudioEdit(actions[key2], {
12169
+ afterOlderSaves: true
12170
+ });
12137
12171
  }
12138
12172
  return tracked;
12139
12173
  }
@@ -17106,6 +17140,9 @@ function applyAuthoredInlineOpacity(style, authored) {
17106
17140
  else style.setProperty("opacity", authored);
17107
17141
  }
17108
17142
 
17143
+ // src/utils/softReloadTargets.ts
17144
+ import { parseGsapScript } from "@hyperframes/parsers/gsap-parser";
17145
+
17109
17146
  // src/utils/elementGsap.ts
17110
17147
  function gsapOf(element) {
17111
17148
  return element.ownerDocument.defaultView?.gsap;
@@ -17122,6 +17159,12 @@ function setElementGsapScale(element, x, y) {
17122
17159
  gsap3.set(element, { scaleX: x, scaleY: y });
17123
17160
  return true;
17124
17161
  }
17162
+ function setElementGsapSize(element, width, height) {
17163
+ const gsap3 = gsapOf(element);
17164
+ if (!gsap3?.set) return false;
17165
+ gsap3.set(element, { width, height });
17166
+ return true;
17167
+ }
17125
17168
  function elementTargets(tween) {
17126
17169
  return (tween.targets?.() ?? []).filter(
17127
17170
  (t2) => Boolean(t2?.style && t2.nodeType)
@@ -17154,7 +17197,7 @@ function addTweenTargets(tween, targets) {
17154
17197
  targets.set(el, seen);
17155
17198
  }
17156
17199
  }
17157
- function collectResetTargets(win, doc, targetKeys) {
17200
+ function collectResetTargets(win, doc, targetKeys, outgoingScripts = []) {
17158
17201
  const targets = /* @__PURE__ */ new Map();
17159
17202
  const timelines = win.__timelines ?? {};
17160
17203
  const others = new Set(
@@ -17167,6 +17210,8 @@ function collectResetTargets(win, doc, targetKeys) {
17167
17210
  }
17168
17211
  }
17169
17212
  sweepHeldElements(doc, targetKeys, others, targets);
17213
+ addLiveSetTargets(doc, targetKeys, targets);
17214
+ for (const script of outgoingScripts) addStandaloneSetTargets(doc, script, targets);
17170
17215
  return targets;
17171
17216
  }
17172
17217
  function tweenTargetsIn(timelines) {
@@ -17200,6 +17245,43 @@ function sweepHeldElements(doc, targetKeys, others, targets) {
17200
17245
  }
17201
17246
  }
17202
17247
  }
17248
+ var liveSets = /* @__PURE__ */ new WeakMap();
17249
+ function recordLiveSet(el, vars) {
17250
+ const byElement = liveSets.get(el.ownerDocument) ?? /* @__PURE__ */ new Map();
17251
+ liveSets.set(el.ownerDocument, byElement);
17252
+ const props = byElement.get(el) ?? /* @__PURE__ */ new Set();
17253
+ for (const prop of tweenedProps(vars)) props.add(prop);
17254
+ byElement.set(el, props);
17255
+ }
17256
+ function addLiveSetTargets(doc, targetKeys, targets) {
17257
+ const roots = new Set(targetKeys.map((k) => compositionRoot(doc, k)));
17258
+ for (const [el, props] of liveSets.get(doc) ?? []) {
17259
+ if (!roots.has(el) && !roots.has(el.parentElement?.closest("[data-composition-id]"))) continue;
17260
+ const seen = targets.get(el) ?? /* @__PURE__ */ new Set();
17261
+ for (const prop of props) seen.add(prop);
17262
+ targets.set(el, seen);
17263
+ }
17264
+ }
17265
+ function forgetLiveSets(reset) {
17266
+ for (const el of reset.keys()) liveSets.get(el.ownerDocument)?.delete(el);
17267
+ }
17268
+ function addStandaloneSetTargets(doc, outgoingScript, targets) {
17269
+ if (!outgoingScript.includes("gsap.set(")) return;
17270
+ for (const set of parseGsapScript(outgoingScript).animations) {
17271
+ if (set.method !== "set" || !set.global) continue;
17272
+ let els;
17273
+ try {
17274
+ els = [...doc.querySelectorAll(set.targetSelector)];
17275
+ } catch {
17276
+ continue;
17277
+ }
17278
+ for (const el of els) {
17279
+ const seen = targets.get(el) ?? /* @__PURE__ */ new Set();
17280
+ for (const prop of tweenedProps(set.properties)) seen.add(prop);
17281
+ targets.set(el, seen);
17282
+ }
17283
+ }
17284
+ }
17203
17285
  function fileDocs(ownFile, written, nestedFiles) {
17204
17286
  const docs = /* @__PURE__ */ new Map();
17205
17287
  return (file) => {
@@ -17363,7 +17445,12 @@ function applySoftReload(iframe, scriptText, options = {}) {
17363
17445
  const readAuthoredOpacity = (el) => findAuthoredStyle(el)?.opacity ?? readStampedAuthoredOpacity(el);
17364
17446
  const doReload = () => {
17365
17447
  const timelines = win.__timelines;
17366
- const targets = collectResetTargets(win, doc, targetKeys);
17448
+ const targets = collectResetTargets(
17449
+ win,
17450
+ doc,
17451
+ targetKeys,
17452
+ staleScripts.map((script) => script.textContent ?? "")
17453
+ );
17367
17454
  if (timelines) {
17368
17455
  for (const key2 of targetKeys) {
17369
17456
  const tl = timelines[key2];
@@ -17394,6 +17481,7 @@ function applySoftReload(iframe, scriptText, options = {}) {
17394
17481
  }
17395
17482
  }
17396
17483
  }
17484
+ forgetLiveSets(targets);
17397
17485
  for (const script of staleScripts) script.remove();
17398
17486
  const executeScript = () => {
17399
17487
  if (win.MotionPathPlugin && win.gsap?.registerPlugin) {
@@ -17621,25 +17709,33 @@ import { findTimelineScript } from "@hyperframes/core/gsap-parser-acorn";
17621
17709
  import { walkCompositionDescendants } from "@hyperframes/parsers/hf-ids";
17622
17710
 
17623
17711
  // src/utils/gsapLiveRetime.ts
17624
- import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
17712
+ import { parseGsapScriptAcorn, scriptShape } from "@hyperframes/parsers/gsap-parser-acorn";
17625
17713
  import { hasExplicitTime } from "@hyperframes/parsers/gsap-writer-acorn";
17626
17714
  import { RUNTIME_FILLER } from "@hyperframes/core/runtime/protocol";
17627
17715
  var RERUN = { kind: "rerun" };
17628
- var withoutLayout = (code) => code.replace(/\s+/g, " ").replace(/ ?([^\w$ ]) ?/g, "$1").trim();
17629
17716
  function untimedShape(animation) {
17630
17717
  const { id: _id, position: _p, resolvedStart: _s, duration: _d, ...rest } = animation;
17631
17718
  const provenance = rest.provenance && { ...rest.provenance, sourceRange: void 0 };
17632
17719
  return JSON.stringify(
17633
17720
  { ...rest, provenance },
17634
- (_key, value) => typeof value === "string" && value.startsWith("__raw:") ? withoutLayout(value) : value
17721
+ (_key, value) => typeof value === "string" && value.startsWith("__raw:") ? scriptShape(`(${value.slice(6)}
17722
+ )`) ?? value : value
17635
17723
  );
17636
17724
  }
17637
17725
  var onTimeline = (animation) => !animation.global;
17638
17726
  var placedAlone = (animation) => hasExplicitTime(animation) && typeof animation.resolvedStart === "number";
17639
17727
  var lengthOf = (animation) => animation.method === "set" ? 0 : animation.durationUnresolved ? void 0 : animation.duration;
17640
- var withoutNumbers = (script) => withoutLayout(script.replace(/(?<![\w$#.])\d*\.?\d+/g, "0"));
17728
+ var shapes = /* @__PURE__ */ new Map();
17729
+ function shapeOf(code) {
17730
+ const shape = shapes.has(code) ? shapes.get(code) : scriptShape(code, true);
17731
+ shapes.delete(code);
17732
+ shapes.set(code, shape);
17733
+ if (shapes.size > 2) shapes.delete(shapes.keys().next().value);
17734
+ return shape;
17735
+ }
17641
17736
  function planLiveRetime(before, after) {
17642
- if (withoutNumbers(before) !== withoutNumbers(after)) return RERUN;
17737
+ const shape = shapeOf(before);
17738
+ if (shape === null || shape !== shapeOf(after)) return RERUN;
17643
17739
  const keys = timelineKeys(after);
17644
17740
  if (keys.length !== 1 || timelineKeys(before).join() !== keys.join()) return RERUN;
17645
17741
  const was = parseGsapScriptAcorn(before).animations.filter(onTimeline);
@@ -17705,15 +17801,10 @@ function sharesOneStretch(tweens) {
17705
17801
  );
17706
17802
  return stretch > 0 && fits;
17707
17803
  }
17708
- function inOrHostsSubComposition(target, ownId) {
17709
- const element = target;
17710
- const owner = element.closest?.("[data-composition-id]");
17711
- return owner != null && owner.getAttribute("data-composition-id") !== ownId || element.querySelector?.("[data-composition-id]") != null;
17712
- }
17713
17804
  var keepsOrder = (tweens) => tweens.every(
17714
17805
  (t2, k) => k === 0 || (tweens[k - 1].start - t2.start || tweens[k - 1].source - t2.source) < 0
17715
17806
  );
17716
- function movesKeepEachElementsHistory(timeline, plan, children) {
17807
+ function movesKeepEachElementsHistory(timeline, plan, children, registry3) {
17717
17808
  const byElement = /* @__PURE__ */ new Map();
17718
17809
  children.forEach((child, i) => {
17719
17810
  for (const target of child.targets?.() ?? []) {
@@ -17725,16 +17816,25 @@ function movesKeepEachElementsHistory(timeline, plan, children) {
17725
17816
  const moved = /* @__PURE__ */ new Set();
17726
17817
  for (const [target, indexes] of byElement) {
17727
17818
  if (!indexes.some((i) => plan.tweens[i].moved)) continue;
17728
- if (inOrHostsSubComposition(target, plan.key)) return false;
17729
17819
  const tweens = indexes.map((i) => plan.tweens[i]);
17730
17820
  const shifted = tweens.every((t2) => t2.duration === t2.wasDuration);
17731
17821
  const undelayed = indexes.every((i) => !children[i].delay() && !children[i].repeatDelay?.());
17732
17822
  if (!keepsOrder(tweens) || !sharesOneStretch(tweens) || !(shifted || undelayed)) return false;
17733
17823
  moved.add(target);
17734
17824
  }
17735
- return !timeline.getChildren(false, false, true).some(
17736
- (sub) => sub.getChildren(true, true, false).some((tween) => tween.targets?.().some((t2) => moved.has(t2)))
17825
+ return !animatedElsewhere(timeline, registry3, moved);
17826
+ }
17827
+ function animatedElsewhere(own, registry3, targets) {
17828
+ const seen = /* @__PURE__ */ new Set([own]);
17829
+ const visit = (timeline) => {
17830
+ if (seen.has(timeline)) return false;
17831
+ seen.add(timeline);
17832
+ return timeline.getChildren(false, true, false).some((tween) => tween.targets?.().some((target) => targets.has(target))) || timeline.getChildren(false, false, true).some(visit);
17833
+ };
17834
+ const timelines = registry3.filter(
17835
+ (entry2) => typeof entry2?.getChildren === "function"
17737
17836
  );
17837
+ return own.getChildren(false, false, true).some(visit) || timelines.some(visit);
17738
17838
  }
17739
17839
  function applyLiveRetime(iframe, plan) {
17740
17840
  const win = iframe?.contentWindow;
@@ -17743,7 +17843,8 @@ function applyLiveRetime(iframe, plan) {
17743
17843
  const children = pairedLiveTweens(timeline, plan);
17744
17844
  if (!children) return false;
17745
17845
  if (plan.tweens.some((t2) => t2.moved)) {
17746
- if (!movesKeepEachElementsHistory(timeline, plan, children)) return false;
17846
+ const registry3 = Object.values(win?.__timelines ?? {});
17847
+ if (!movesKeepEachElementsHistory(timeline, plan, children, registry3)) return false;
17747
17848
  const bySource = plan.tweens.map((t2, i) => ({ ...t2, tween: children[i] })).sort((a, b) => a.source - b.source);
17748
17849
  for (const { tween } of bySource) timeline.remove(tween);
17749
17850
  for (const { tween, start, duration } of bySource) {
@@ -21246,7 +21347,10 @@ import {
21246
21347
  Scissors as PhScissors,
21247
21348
  Link as PhLink,
21248
21349
  Eyedropper as PhEyedropper,
21249
- Trash as PhTrash
21350
+ Trash as PhTrash,
21351
+ Image as PhImage,
21352
+ BezierCurve,
21353
+ FolderSimple
21250
21354
  } from "@phosphor-icons/react";
21251
21355
  import { jsx as jsx40 } from "react/jsx-runtime";
21252
21356
  var makeIcon = (Icon) => {
@@ -21291,6 +21395,9 @@ var Scissors = makeIcon(PhScissors);
21291
21395
  var Link = makeIcon(PhLink);
21292
21396
  var Eyedropper = makeIcon(PhEyedropper);
21293
21397
  var Trash = makeIcon(PhTrash);
21398
+ var ImageIcon = makeIcon(PhImage);
21399
+ var Vector = makeIcon(BezierCurve);
21400
+ var Folder = makeIcon(FolderSimple);
21294
21401
 
21295
21402
  // src/player/components/TrackClipCount.tsx
21296
21403
  import { jsx as jsx41 } from "react/jsx-runtime";
@@ -30342,9 +30449,12 @@ var gestureSaves = /* @__PURE__ */ new WeakMap();
30342
30449
  function studioManualEditSavesIn(doc) {
30343
30450
  return gestureSaves.get(doc) ?? 0;
30344
30451
  }
30452
+ function countStudioPreviewChange(doc) {
30453
+ gestureSaves.set(doc, studioManualEditSavesIn(doc) + 1);
30454
+ }
30345
30455
  function countStudioManualEditSave(element, save) {
30346
30456
  const doc = element.ownerDocument;
30347
- const count = () => void gestureSaves.set(doc, studioManualEditSavesIn(doc) + 1);
30457
+ const count = () => countStudioPreviewChange(doc);
30348
30458
  count();
30349
30459
  const result = save();
30350
30460
  void Promise.resolve(result).then(count, count);
@@ -37412,22 +37522,31 @@ function readCssRotation(element, withRotate = true) {
37412
37522
  return angle(transform);
37413
37523
  }
37414
37524
  }
37415
- function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
37525
+ function readDragStamp(element) {
37526
+ const read2 = (name) => Number.parseFloat(element.getAttribute(name) ?? "");
37527
+ return {
37528
+ origX: read2("data-hf-drag-initial-offset-x") || 0,
37529
+ origY: read2("data-hf-drag-initial-offset-y") || 0,
37530
+ baseX: read2("data-hf-drag-gsap-base-x"),
37531
+ baseY: read2("data-hf-drag-gsap-base-y")
37532
+ };
37533
+ }
37534
+ var freezeDragStamp = (element) => ({
37535
+ ...readDragStamp(element),
37536
+ frozen: true
37537
+ });
37538
+ function computeDraggedGsapPosition(element, studioOffset, fallbackBase, stamp = readDragStamp(element)) {
37416
37539
  const rotStyle = element.style.getPropertyValue("--hf-studio-rotation");
37417
37540
  const rotDeg = Number.parseFloat(rotStyle) || 0;
37418
37541
  const rad = -rotDeg * Math.PI / 180;
37419
37542
  const cos = Math.cos(rad);
37420
37543
  const sin = Math.sin(rad);
37421
- const origX = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
37422
- const origY = Number.parseFloat(element.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
37423
- const deltaX = studioOffset.x - origX;
37424
- const deltaY = studioOffset.y - origY;
37544
+ const deltaX = studioOffset.x - stamp.origX;
37545
+ const deltaY = studioOffset.y - stamp.origY;
37425
37546
  const adjX = deltaX * cos - deltaY * sin;
37426
37547
  const adjY = deltaX * sin + deltaY * cos;
37427
- const parsedBaseX = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-x") ?? "");
37428
- const parsedBaseY = Number.parseFloat(element.getAttribute("data-hf-drag-gsap-base-y") ?? "");
37429
- const baseGsapX = Number.isFinite(parsedBaseX) ? parsedBaseX : fallbackBase.x;
37430
- const baseGsapY = Number.isFinite(parsedBaseY) ? parsedBaseY : fallbackBase.y;
37548
+ const baseGsapX = Number.isFinite(stamp.baseX) ? stamp.baseX : fallbackBase.x;
37549
+ const baseGsapY = Number.isFinite(stamp.baseY) ? stamp.baseY : fallbackBase.y;
37431
37550
  return {
37432
37551
  newX: roundTo3(baseGsapX + adjX),
37433
37552
  newY: roundTo3(baseGsapY + adjY),
@@ -37435,10 +37554,10 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
37435
37554
  baseGsapY
37436
37555
  };
37437
37556
  }
37438
- function restoreDragOffset(element) {
37439
- const origin2 = (axis) => Number.parseFloat(element.getAttribute(`data-hf-drag-initial-offset-${axis}`) ?? "") || 0;
37440
- element.style.setProperty("--hf-studio-offset-x", `${origin2("x")}px`);
37441
- element.style.setProperty("--hf-studio-offset-y", `${origin2("y")}px`);
37557
+ function restoreDragOffset(element, stamp = readDragStamp(element)) {
37558
+ if (stamp.frozen) return;
37559
+ element.style.setProperty("--hf-studio-offset-x", `${stamp.origX}px`);
37560
+ element.style.setProperty("--hf-studio-offset-y", `${stamp.origY}px`);
37442
37561
  element.removeAttribute("data-hf-drag-initial-offset-x");
37443
37562
  element.removeAttribute("data-hf-drag-initial-offset-y");
37444
37563
  }
@@ -37512,6 +37631,12 @@ function readTranslatePx(el) {
37512
37631
  y: evaluateLength(y, side("height", "top", "bottom"))
37513
37632
  };
37514
37633
  }
37634
+ function readTranslatePxLeavingPercent(el) {
37635
+ const value = el.ownerDocument.defaultView?.getComputedStyle(el).translate || "none";
37636
+ const [x = "0px", y = "0px"] = value === "none" ? [] : splitTopLevelWhitespace(value);
37637
+ const px = (part) => roundTo3(evaluateLength(part, 0)) || 0;
37638
+ return { x: px(x), y: px(y) };
37639
+ }
37515
37640
  var UNREADABLE_TRANSLATE = "Studio can't read this layer's translate. Move it in the Code tab.";
37516
37641
  function formatTranslatePx(p) {
37517
37642
  return `${roundTo3(p.x)}px ${roundTo3(p.y)}px`;
@@ -45740,7 +45865,12 @@ function refuseStepListRewrite(anim) {
45740
45865
  if (step) throw new GsapEditBlockedError("keyframes-uneditable", step);
45741
45866
  }
45742
45867
  async function commitStaticGsapPosition(selection, studioOffset, gsapPos, selector, existingSet, callbacks) {
45743
- const { newX, newY } = computeDraggedGsapPosition(selection.element, studioOffset, gsapPos);
45868
+ const { newX, newY } = computeDraggedGsapPosition(
45869
+ selection.element,
45870
+ studioOffset,
45871
+ gsapPos,
45872
+ callbacks.stamp
45873
+ );
45744
45874
  if (existingSet) {
45745
45875
  if (existingSet.keyframes) {
45746
45876
  await replaceKeyframedPositionHold(
@@ -45859,21 +45989,16 @@ async function commitStaticGsapSize(selection, size, selector, existingSet, call
45859
45989
  }
45860
45990
  async function commitWholePathOffset(selection, anim, studioOffset, gsapPos, iframe, selector, callbacks) {
45861
45991
  const el = selection.element;
45992
+ const stamp = callbacks.stamp ?? readDragStamp(el);
45862
45993
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
45863
45994
  el,
45864
45995
  studioOffset,
45865
- gsapPos
45996
+ gsapPos,
45997
+ stamp
45866
45998
  );
45867
45999
  const deltaX = newX - baseGsapX;
45868
46000
  const deltaY = newY - baseGsapY;
45869
- const origX = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
45870
- const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
45871
- const restoreOffset = () => {
45872
- el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
45873
- el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
45874
- el.removeAttribute("data-hf-drag-initial-offset-x");
45875
- el.removeAttribute("data-hf-drag-initial-offset-y");
45876
- };
46001
+ const restoreOffset = () => restoreDragOffset(el, stamp);
45877
46002
  let effectiveAnim = anim;
45878
46003
  if (anim.keyframes) {
45879
46004
  refuseStepListRewrite(anim);
@@ -47811,19 +47936,14 @@ function gsapPositionFromDragOutcome(selection, anim, studioOffset, gsapPos, ifr
47811
47936
  }
47812
47937
  async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos, iframe, callbacks) {
47813
47938
  const el = selection.element;
47939
+ const stamp = callbacks.stamp ?? readDragStamp(el);
47814
47940
  const { newX, newY, baseGsapX, baseGsapY } = computeDraggedGsapPosition(
47815
47941
  el,
47816
47942
  studioOffset,
47817
- gsapPos
47943
+ gsapPos,
47944
+ stamp
47818
47945
  );
47819
- const origX = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-x") ?? "") || 0;
47820
- const origY = Number.parseFloat(el.getAttribute("data-hf-drag-initial-offset-y") ?? "") || 0;
47821
- const restoreOffset = () => {
47822
- el.style.setProperty("--hf-studio-offset-x", `${origX}px`);
47823
- el.style.setProperty("--hf-studio-offset-y", `${origY}px`);
47824
- el.removeAttribute("data-hf-drag-initial-offset-x");
47825
- el.removeAttribute("data-hf-drag-initial-offset-y");
47826
- };
47946
+ const restoreOffset = () => restoreDragOffset(el, stamp);
47827
47947
  if (anim.arcPath?.enabled) {
47828
47948
  const { activeKeyframePct, currentTime, setActiveKeyframePct } = usePlayerStore.getState();
47829
47949
  const tweenStart = resolveTweenStart(anim);
@@ -48086,7 +48206,8 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
48086
48206
  const existingSet = posAnim && isInstantHold(posAnim) && posAnim.targetSelector === selector ? posAnim : findExistingPositionWrite(resolvedAnimations, selector, selection.element);
48087
48207
  await commitStaticGsapPosition(selection, offset, gsapPos, selector, existingSet, {
48088
48208
  commitMutation,
48089
- fetchAnimations: fetchFallbackAnimations
48209
+ fetchAnimations: fetchFallbackAnimations,
48210
+ stamp: options?.stamp
48090
48211
  });
48091
48212
  return { status: "persisted" };
48092
48213
  }
@@ -48104,7 +48225,7 @@ async function tryGsapDragIntercept(selection, offset, allAnimations, iframe, ge
48104
48225
  posAnim = freshMatch;
48105
48226
  }
48106
48227
  }
48107
- const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations };
48228
+ const cbs = { commitMutation, fetchAnimations: fetchFallbackAnimations, stamp: options?.stamp };
48108
48229
  if (route === "whole-path") {
48109
48230
  await commitWholePathOffset(selection, posAnim, offset, gsapPos, iframe, selector, cbs);
48110
48231
  } else {
@@ -50783,7 +50904,7 @@ function revealedScroll(shape, scrollLeft) {
50783
50904
  if (shape.left < scrollLeft) return shape.left;
50784
50905
  return Math.max(scrollLeft, shape.left + shape.size + 1 - shape.width);
50785
50906
  }
50786
- function measureFill(list, shapes) {
50907
+ function measureFill(list, shapes2) {
50787
50908
  const fill = fillOf(list);
50788
50909
  const tab = list.querySelector(":scope > .dv-active-tab");
50789
50910
  const shape = {
@@ -50792,8 +50913,8 @@ function measureFill(list, shapes) {
50792
50913
  left: tab?.offsetLeft ?? 0,
50793
50914
  size: tab?.offsetWidth ?? 0
50794
50915
  };
50795
- const reveal2 = tab !== null && reshaped(shapes.get(list), shape);
50796
- if (shape.width > 0) shapes.set(list, shape);
50916
+ const reveal2 = tab !== null && reshaped(shapes2.get(list), shape);
50917
+ if (shape.width > 0) shapes2.set(list, shape);
50797
50918
  const scrollLeft = list.scrollLeft;
50798
50919
  const target = reveal2 ? revealedScroll(shape, scrollLeft) : scrollLeft;
50799
50920
  return () => {
@@ -50828,10 +50949,10 @@ function installTabFill(api, root) {
50828
50949
  observed.add(element);
50829
50950
  }
50830
50951
  }
50831
- const shapes = /* @__PURE__ */ new WeakMap();
50952
+ const shapes2 = /* @__PURE__ */ new WeakMap();
50832
50953
  function placeAll() {
50833
50954
  const strips2 = [...lists()];
50834
- const writes = strips2.map((list) => measureFill(list, shapes));
50955
+ const writes = strips2.map((list) => measureFill(list, shapes2));
50835
50956
  for (const write of writes) write();
50836
50957
  for (const list of strips2) markClippedEdges(list);
50837
50958
  observe(strips2.flatMap((list) => [list, ...list.querySelectorAll(":scope > .dv-tab")]));
@@ -58874,6 +58995,25 @@ function formatPropValue(prop, v) {
58874
58995
  if (PERCENT_PROPS.has(prop)) return `${Math.round(Number(v) * 100)}${unit}`;
58875
58996
  return `${v}${unit}`;
58876
58997
  }
58998
+ var easeLabel = (ease) => ease.startsWith("custom(") ? "custom" : EASE_LABELS[ease] ?? ease;
58999
+ function uniformSegmentEase(animation) {
59000
+ const eases = keyframedTweenEases(animation);
59001
+ const steps = animation.keyframes?.keyframes ?? [];
59002
+ const segmentEases = new Set(steps.slice(1).map((kf) => eases.segment(kf)));
59003
+ if (segmentEases.size === 0) return eases.segment({});
59004
+ return segmentEases.size === 1 ? [...segmentEases][0] : null;
59005
+ }
59006
+ function keyframedTweenSummary(animation, pos, dur) {
59007
+ const steps = animation.keyframes?.keyframes ?? [];
59008
+ const props = [...new Set(steps.flatMap((kf) => Object.keys(kf.properties)))];
59009
+ const propText = props.map((p) => (PROP_LABELS[p] ?? p).toLowerCase()).join(", ");
59010
+ const segmentEase = uniformSegmentEase(animation);
59011
+ const segmentText = steps.length < 2 ? "" : segmentEase ? `, each segment eased ${easeLabel(segmentEase)}` : ", with per-keyframe easing";
59012
+ const run = keyframedTweenEases(animation).run;
59013
+ const runText = run && run !== "none" ? `, across a ${easeLabel(run)} run` : "";
59014
+ const count = `${steps.length} keyframe${steps.length === 1 ? "" : "s"}`;
59015
+ return `Starting at ${pos}s, over ${dur}s, animate ${animation.targetSelector}'s ${propText || "no properties yet"} through ${count}${segmentText}${runText}.`;
59016
+ }
58877
59017
  function buildTweenSummary(animation) {
58878
59018
  const easeName = animation.ease ?? "none";
58879
59019
  const ease = EASE_LABELS[easeName] ?? easeName;
@@ -58882,6 +59022,7 @@ function buildTweenSummary(animation) {
58882
59022
  const dur = animation.duration ?? 0;
58883
59023
  const rawPos = animation.position;
58884
59024
  const pos = typeof rawPos === "number" ? parseFloat(rawPos.toFixed(3)) : rawPos;
59025
+ if (animation.keyframes) return keyframedTweenSummary(animation, pos, dur);
58885
59026
  const propDescs = props.map(([p, v]) => {
58886
59027
  const label = (PROP_LABELS[p] ?? p).toLowerCase();
58887
59028
  return `${label} to ${formatPropValue(p, v)}`;
@@ -59096,6 +59237,7 @@ var APPLY_ALL_EASES = [
59096
59237
  "bounce.out",
59097
59238
  "bounce.inOut"
59098
59239
  ];
59240
+ var percentLabel = (pct) => pct > 0 && pct < 1e-3 ? "<0.001" : String(roundTo3(pct));
59099
59241
  function KeyframeEaseList({
59100
59242
  keyframes,
59101
59243
  globalEase,
@@ -59130,8 +59272,8 @@ function KeyframeEaseList({
59130
59272
  if (i === 0) return null;
59131
59273
  const segEase = kf.ease ?? globalEase;
59132
59274
  const isExpanded = expandedPct === kf.percentage;
59133
- const label = `${keyframes[i - 1].percentage}% \u2192 ${kf.percentage}%`;
59134
- const easeLabel = segEase.startsWith("custom(") ? "Custom" : EASE_LABELS[segEase] ?? segEase;
59275
+ const label = `${percentLabel(keyframes[i - 1].percentage)}% \u2192 ${percentLabel(kf.percentage)}%`;
59276
+ const easeLabel2 = segEase.startsWith("custom(") ? "Custom" : EASE_LABELS[segEase] ?? segEase;
59135
59277
  return /* @__PURE__ */ jsxs105(
59136
59278
  "div",
59137
59279
  {
@@ -59147,7 +59289,7 @@ function KeyframeEaseList({
59147
59289
  className: "flex w-full items-center gap-2 px-2 py-1.5 text-left active:scale-[0.99]",
59148
59290
  children: [
59149
59291
  /* @__PURE__ */ jsx140("span", { className: "text-[10px] font-medium text-neutral-400", children: label }),
59150
- /* @__PURE__ */ jsx140("span", { className: "ml-auto text-[9px] text-neutral-500", children: easeLabel }),
59292
+ /* @__PURE__ */ jsx140("span", { className: "ml-auto text-[9px] text-neutral-500", children: easeLabel2 }),
59151
59293
  /* @__PURE__ */ jsx140(
59152
59294
  "svg",
59153
59295
  {
@@ -59360,6 +59502,7 @@ function parseNumericOrString(raw) {
59360
59502
 
59361
59503
  // src/components/editor/AnimationCard.tsx
59362
59504
  import { Fragment as Fragment40, jsx as jsx142, jsxs as jsxs107 } from "react/jsx-runtime";
59505
+ var easeDisplayName = (ease) => ease.startsWith("custom(") ? "Custom curve" : EASE_LABELS[ease] ?? ease;
59363
59506
  var AnimationCard = memo37(function AnimationCard2({
59364
59507
  animation,
59365
59508
  defaultExpanded,
@@ -59465,7 +59608,9 @@ var AnimationCard = memo37(function AnimationCard2({
59465
59608
  const [copied, setCopied] = useState75(false);
59466
59609
  const methodLabel = METHOD_LABELS[animation.method] ?? animation.method;
59467
59610
  const easeName = (animation.keyframes ? animation.keyframes.easeEach : void 0) ?? animation.ease ?? "none";
59468
- const easeLabel = easeName.startsWith("custom(") ? "Custom curve" : EASE_LABELS[easeName] ?? easeName;
59611
+ const headerEase = animation.keyframes ? uniformSegmentEase(animation) : easeName;
59612
+ const easeLabel2 = headerEase === null ? "Mixed" : easeDisplayName(headerEase);
59613
+ const runEase = animation.keyframes ? keyframedTweenEases(animation).run : void 0;
59469
59614
  const endTime = typeof animation.position === "number" ? animation.position + (animation.duration ?? 0) : animation.position;
59470
59615
  const summary = useMemo57(() => buildTweenSummary(animation), [animation]);
59471
59616
  const setKeys = Object.keys(animation.properties);
@@ -59523,8 +59668,9 @@ var AnimationCard = memo37(function AnimationCard2({
59523
59668
  "span",
59524
59669
  {
59525
59670
  className: `ml-auto text-[10px] ${flat ? "text-panel-text-3" : "text-neutral-500"}`,
59526
- title: easeName,
59527
- children: easeLabel
59671
+ title: headerEase ?? "Mixed",
59672
+ "data-card-ease": true,
59673
+ children: easeLabel2
59528
59674
  }
59529
59675
  ),
59530
59676
  /* @__PURE__ */ jsx142(
@@ -59564,6 +59710,10 @@ var AnimationCard = memo37(function AnimationCard2({
59564
59710
  }
59565
59711
  ),
59566
59712
  "Keyframed \u2014 click a segment below to edit its curve"
59713
+ ] }),
59714
+ runEase && runEase !== "none" && /* @__PURE__ */ jsxs107("p", { className: "mt-1 text-[9px] text-neutral-500", "data-card-run-ease": true, children: [
59715
+ "Run ease: ",
59716
+ easeDisplayName(runEase)
59567
59717
  ] })
59568
59718
  ] }),
59569
59719
  /* @__PURE__ */ jsx142(
@@ -59607,7 +59757,7 @@ var AnimationCard = memo37(function AnimationCard2({
59607
59757
  KeyframeEaseList,
59608
59758
  {
59609
59759
  keyframes: animation.keyframes.keyframes,
59610
- globalEase: animation.keyframes.easeEach ?? animation.ease ?? "none",
59760
+ globalEase: keyframedTweenEases(animation).segment({}),
59611
59761
  expandedPct: expandedKfPct,
59612
59762
  collidingAnimationTargets: focusedCollidingAnimationTargets,
59613
59763
  onToggle: (pct) => {
@@ -70768,7 +70918,7 @@ import {
70768
70918
  Trash as Trash2,
70769
70919
  FilePlus,
70770
70920
  FolderSimplePlus,
70771
- FolderSimple
70921
+ FolderSimple as FolderSimple2
70772
70922
  } from "@phosphor-icons/react";
70773
70923
 
70774
70924
  // src/components/editor/FileTreeIcons.tsx
@@ -70789,7 +70939,7 @@ import {
70789
70939
  File as File2,
70790
70940
  Waveform,
70791
70941
  TextAa,
70792
- Image as PhImage
70942
+ Image as PhImage2
70793
70943
  } from "@phosphor-icons/react";
70794
70944
  import { jsx as jsx189 } from "react/jsx-runtime";
70795
70945
  var SZ = 14;
@@ -70812,7 +70962,7 @@ var ICONS = /* @__PURE__ */ new Map([
70812
70962
  ["txt", [FileTxt, GRAY]],
70813
70963
  ["png", [FilePng, GREEN]],
70814
70964
  ["jpg", [FileJpg, GREEN]],
70815
- ["webp", [PhImage, GREEN]],
70965
+ ["webp", [PhImage2, GREEN]],
70816
70966
  ["mp4", [FileVideo, "text-[light-dark(oklch(0.59_0.2_304),oklch(0.71_0.2_304))]"]],
70817
70967
  ["mp3", [Waveform, "text-[light-dark(oklch(0.53_0.159_165),#3CE6AC)]"]],
70818
70968
  ["woff", [TextAa, DIM]]
@@ -71110,7 +71260,7 @@ function InlineInput({
71110
71260
  className: "flex items-center gap-2 py-0.5 min-h-7",
71111
71261
  style: { paddingLeft: `${8 + depth * 12 + (isFolder ? 0 : 14)}px` },
71112
71262
  children: [
71113
- isFolder ? /* @__PURE__ */ jsx190(FolderSimple, { size: SZ_ICON, weight: "duotone", className: `shrink-0 ${DIM}` }) : /* @__PURE__ */ jsx190(FileIcon, { path: value }),
71263
+ isFolder ? /* @__PURE__ */ jsx190(FolderSimple2, { size: SZ_ICON, weight: "duotone", className: `shrink-0 ${DIM}` }) : /* @__PURE__ */ jsx190(FileIcon, { path: value }),
71114
71264
  /* @__PURE__ */ jsxs149("div", { className: "flex-1 min-w-0", children: [
71115
71265
  /* @__PURE__ */ jsx190(
71116
71266
  "input",
@@ -74787,7 +74937,8 @@ function showRestoreInPlace(iframe, activeCompPath, files, currentTime) {
74787
74937
  const doc = iframe?.contentDocument;
74788
74938
  const win = iframe?.contentWindow;
74789
74939
  const plan = doc && win ? planRestoreTargets(doc, activeCompPath ?? "index.html", files) : null;
74790
- if (!iframe || !win || !plan || plan.scripted) return null;
74940
+ if (!iframe || !doc || !win || !plan || plan.scripted) return null;
74941
+ countStudioPreviewChange(doc);
74791
74942
  const before = plan.targets.map(
74792
74943
  ({ live, restored }) => [live, live.cloneNode(false), restored]
74793
74944
  );
@@ -74821,6 +74972,7 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
74821
74972
  reloadPreview();
74822
74973
  return "full";
74823
74974
  }
74975
+ countStudioPreviewChange(doc);
74824
74976
  for (const target of plan.targets)
74825
74977
  syncElementAttributes(target.live, target.restored, target.previous);
74826
74978
  const active = files[activeDocPath];
@@ -78180,7 +78332,7 @@ function useTimelineMoveEditsHandler(handleTimelineGroupMove) {
78180
78332
  }
78181
78333
 
78182
78334
  // src/hooks/useDomEditSession.ts
78183
- import { useCallback as useCallback148 } from "react";
78335
+ import { useCallback as useCallback149 } from "react";
78184
78336
 
78185
78337
  // src/hooks/useAskAgentModal.ts
78186
78338
  import { useState as useState117, useCallback as useCallback125, useRef as useRef140, useEffect as useEffect115 } from "react";
@@ -80506,6 +80658,7 @@ function savePlainRotation({ commitPositionPatchToHtml, readOnlyPreview }, selec
80506
80658
  }
80507
80659
 
80508
80660
  // src/hooks/useDomGeometryCommits.ts
80661
+ var boxSizeCommitCounter = 0;
80509
80662
  function useDomGeometryCommits({
80510
80663
  showToast,
80511
80664
  commitPositionPatchToHtml,
@@ -80535,7 +80688,10 @@ function useDomGeometryCommits({
80535
80688
  if (offset) patches.push(...writePlainMove(element, offset));
80536
80689
  return commitPositionPatchToHtml(selection, patches, {
80537
80690
  label: "Resize layer box",
80538
- ...undoKey ? { coalesceKey: undoKey, coalesceMs: Number.POSITIVE_INFINITY, deferRender: true } : { coalesceKey: `box-size:${getDomEditTargetKey(selection)}` }
80691
+ ...undoKey ? { coalesceKey: undoKey, coalesceMs: Number.POSITIVE_INFINITY, deferRender: true } : {
80692
+ coalesceKey: `box-size:${++boxSizeCommitCounter}`,
80693
+ coalesceMs: Number.POSITIVE_INFINITY
80694
+ }
80539
80695
  }).catch((error) => {
80540
80696
  restoreStudioBoxSize(element, beforeSize);
80541
80697
  if (offset) restoreStudioPathOffset(element, beforeOffset);
@@ -80973,6 +81129,7 @@ function applyGlobalSet(iframe, selector, props) {
80973
81129
  const numeric = finiteNumericProps(props);
80974
81130
  if (!gsapLib?.set || !el || !numeric) return false;
80975
81131
  gsapLib.set(el, numeric);
81132
+ recordLiveSet(el, numeric);
80976
81133
  return true;
80977
81134
  } catch {
80978
81135
  return false;
@@ -83164,7 +83321,7 @@ function useGsapAwareGroupMove({
83164
83321
  }
83165
83322
 
83166
83323
  // src/hooks/useGsapAwareEditing.ts
83167
- import { useCallback as useCallback146 } from "react";
83324
+ import { useCallback as useCallback147 } from "react";
83168
83325
 
83169
83326
  // src/hooks/gsapResizePreflight.ts
83170
83327
  function resizeRoute(animations, fetchedAnimations) {
@@ -83196,9 +83353,339 @@ function preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnim
83196
83353
  return { status: "persisted" };
83197
83354
  }
83198
83355
 
83356
+ // src/hooks/useEnableKeyframes.ts
83357
+ import { useCallback as useCallback145 } from "react";
83358
+ var enableKeyframesTransactionCounter = 0;
83359
+ function animatedProps(anim) {
83360
+ if (!anim) return ["x", "y"];
83361
+ const own = Object.keys(anim.properties ?? {});
83362
+ if (own.length > 0) return own;
83363
+ const stops = anim.keyframes?.keyframes;
83364
+ if (stops?.length) {
83365
+ const keys = /* @__PURE__ */ new Set();
83366
+ for (const stop2 of stops) for (const k of Object.keys(stop2.properties ?? {})) keys.add(k);
83367
+ if (keys.size > 0) return [...keys];
83368
+ }
83369
+ return ["x", "y"];
83370
+ }
83371
+ function isPlayheadWithinTween(anim, currentTime) {
83372
+ const start = resolveTweenStart(anim);
83373
+ if (start === null) return true;
83374
+ return isTimeWithinTween(currentTime, start, resolveTweenDuration(anim));
83375
+ }
83376
+ async function replaceSetWithSingleKeyframe(session, sel, setAnim, t2, iframe, selector) {
83377
+ const position = readElementPosition(iframe, sel, setAnim);
83378
+ if (Object.keys(position).length === 0) {
83379
+ for (const [key2, held] of Object.entries(setAnim.properties ?? {})) {
83380
+ if (typeof held === "number") position[key2] = held;
83381
+ }
83382
+ }
83383
+ if (Object.keys(position).length === 0 || !session.commitMutation) return;
83384
+ const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t2);
83385
+ await session.commitMutation(
83386
+ {
83387
+ type: "replace-with-keyframes",
83388
+ animationId: setAnim.id,
83389
+ targetSelector: selector,
83390
+ position: roundTo3(range.start),
83391
+ duration: roundTo3(range.duration),
83392
+ keyframes: [{ percentage: 0, properties: position }],
83393
+ ease: setAnim.ease
83394
+ },
83395
+ { label: "Enable keyframes", keyframeAction: "add", softReload: true }
83396
+ );
83397
+ }
83398
+ function readElementPosition(iframe, sel, anim) {
83399
+ const result = {};
83400
+ if (!iframe?.contentWindow) return result;
83401
+ let gsap3;
83402
+ try {
83403
+ gsap3 = iframe.contentWindow.gsap;
83404
+ } catch {
83405
+ return result;
83406
+ }
83407
+ const element = sel.element;
83408
+ if (!element?.isConnected || !gsap3?.getProperty) return result;
83409
+ const props = animatedProps(anim);
83410
+ for (const prop of props) {
83411
+ const val = Number(gsap3.getProperty(element, prop));
83412
+ if (!Number.isFinite(val)) continue;
83413
+ result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
83414
+ }
83415
+ return result;
83416
+ }
83417
+ function resolveNewTweenRange(authoredStart, authoredDuration, currentTime) {
83418
+ const t2 = Math.max(0, roundTo3(currentTime));
83419
+ const start = authoredStart != null ? Number.parseFloat(authoredStart) : Number.NaN;
83420
+ const duration = authoredDuration != null ? Number.parseFloat(authoredDuration) : Number.NaN;
83421
+ if (!Number.isFinite(start) || !Number.isFinite(duration) || duration <= 0) {
83422
+ return { start: t2, duration: 1 };
83423
+ }
83424
+ const end = start + duration;
83425
+ const clampedStart = Math.min(Math.max(t2, start), end);
83426
+ return { start: clampedStart, duration: Math.max(0.5, roundTo3(end - clampedStart)) };
83427
+ }
83428
+ function singleKeyTweenMutation(sel, properties, currentTime) {
83429
+ const targetSelector = writeTargetSelector(sel);
83430
+ if (!targetSelector) return null;
83431
+ const { start, duration } = resolveNewTweenRange(
83432
+ sel.dataAttributes?.start,
83433
+ sel.dataAttributes?.duration,
83434
+ currentTime
83435
+ );
83436
+ return {
83437
+ type: "add-with-keyframes",
83438
+ targetSelector,
83439
+ position: roundTo3(start),
83440
+ duration: roundTo3(duration),
83441
+ keyframes: [{ percentage: 0, properties }]
83442
+ };
83443
+ }
83444
+ async function tryFetchAnimationsForElement(sel) {
83445
+ const projectId = window.location.hash.match(/project\/([^?/]+)/)?.[1];
83446
+ if (!projectId) return null;
83447
+ const sourceFile = sel.sourceFile || "index.html";
83448
+ const parsed = await fetchParsedAnimations(projectId, sourceFile);
83449
+ if (!parsed) return null;
83450
+ return getAnimationsForElement(parsed.animations, {
83451
+ id: sel.id,
83452
+ selector: sel.selector
83453
+ });
83454
+ }
83455
+ async function fetchAnimationsForElement(sel) {
83456
+ return await tryFetchAnimationsForElement(sel) ?? [];
83457
+ }
83458
+ async function extendKeyframedTweenToPlayhead(session, sel, anim, currentTime, duration, iframe, commitOverrides) {
83459
+ const selector = existingTweenTargetSelector(anim, sel);
83460
+ const position = readElementPosition(iframe, sel, anim);
83461
+ if (!selector || Object.keys(position).length === 0 || !session.commitMutation) return;
83462
+ const extended = buildExtendedKeyframes(anim, currentTime, position, duration);
83463
+ await session.commitMutation(
83464
+ {
83465
+ type: "replace-with-keyframes",
83466
+ animationId: anim.id,
83467
+ targetSelector: selector,
83468
+ position: extended.position,
83469
+ duration: extended.duration,
83470
+ keyframes: extended.keyframes,
83471
+ ...keyframeEases(anim)
83472
+ },
83473
+ {
83474
+ label: "Add keyframe",
83475
+ keyframeAction: "add",
83476
+ softReload: true,
83477
+ ...commitOverrides
83478
+ }
83479
+ );
83480
+ }
83481
+ async function applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe, commitOverrides, liveTween) {
83482
+ const keepAuthoredDuration = kfAnim.durationUnresolved === true;
83483
+ if (keepAuthoredDuration) {
83484
+ kfAnim = withLiveTiming(kfAnim, liveTween ?? findParsedTween(iframe, sel.element, kfAnim));
83485
+ }
83486
+ if (keepAuthoredDuration && kfAnim.duration == null) return;
83487
+ const duration = resolveTweenDuration(kfAnim);
83488
+ const start = resolveTweenStart(kfAnim);
83489
+ if (start !== null && !isTimeWithinTween(t2, start, duration)) {
83490
+ if (keepAuthoredDuration) return;
83491
+ await extendKeyframedTweenToPlayhead(
83492
+ session,
83493
+ sel,
83494
+ kfAnim,
83495
+ t2,
83496
+ duration,
83497
+ iframe,
83498
+ commitOverrides
83499
+ );
83500
+ return;
83501
+ }
83502
+ const pct = start === null ? computeElementPercentage(t2, sel) : absoluteToPercentageForAnimation(t2, kfAnim) ?? 0;
83503
+ const existing = kfAnim.keyframes?.keyframes.find((k) => playsNear(kfAnim, k.percentage, pct));
83504
+ if (existing) {
83505
+ session.handleGsapRemoveKeyframe(kfAnim.id, existing.percentage);
83506
+ return;
83507
+ }
83508
+ if (session.handleGsapAddKeyframeBatch) {
83509
+ const position = readElementPosition(iframe, sel, kfAnim);
83510
+ if (Object.keys(position).length > 0) {
83511
+ await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position, commitOverrides);
83512
+ }
83513
+ }
83514
+ }
83515
+ async function promoteSetToKeyframes(session, sel, setAnim, t2, iframe) {
83516
+ const selector = existingTweenTargetSelector(setAnim, sel);
83517
+ const setStart = resolveTweenStart(setAnim) ?? 0;
83518
+ if (!selector || !session.commitMutation) return;
83519
+ if (t2 <= setStart) {
83520
+ await replaceSetWithSingleKeyframe(session, sel, setAnim, t2, iframe, selector);
83521
+ return;
83522
+ }
83523
+ const endPosition = readElementPosition(iframe, sel, setAnim);
83524
+ if (Object.keys(endPosition).length === 0) return;
83525
+ const startPosition = {};
83526
+ for (const key2 of Object.keys(endPosition)) {
83527
+ const held = setAnim.properties?.[key2];
83528
+ if (typeof held === "number") startPosition[key2] = held;
83529
+ }
83530
+ await session.commitMutation(
83531
+ {
83532
+ type: "replace-with-keyframes",
83533
+ animationId: setAnim.id,
83534
+ targetSelector: selector,
83535
+ position: roundTo3(setStart),
83536
+ duration: roundTo3(t2 - setStart),
83537
+ keyframes: [
83538
+ {
83539
+ percentage: 0,
83540
+ properties: Object.keys(startPosition).length > 0 ? startPosition : endPosition,
83541
+ auto: true
83542
+ },
83543
+ { percentage: 100, properties: endPosition }
83544
+ ],
83545
+ ease: setAnim.ease
83546
+ },
83547
+ { label: "Add keyframe", keyframeAction: "add", softReload: true }
83548
+ );
83549
+ }
83550
+ async function applyArcKeyframeAtPlayhead(session, sel, arcAnim, t2, iframe) {
83551
+ if (!session.commitMutation) return;
83552
+ const targetSelector = existingTweenTargetSelector(arcAnim, sel);
83553
+ if (!targetSelector) return;
83554
+ const start = resolveTweenStart(arcAnim) ?? 0;
83555
+ const duration = resolveTweenDuration(arcAnim);
83556
+ if (!isTimeWithinTween(t2, start, duration)) {
83557
+ const held = readElementPosition(iframe, sel, arcAnim);
83558
+ if (t2 <= start || typeof held.x !== "number" || typeof held.y !== "number") return;
83559
+ const extended = buildExtendedKeyframes(arcAnim, t2, { x: held.x, y: held.y }, duration, "none");
83560
+ await session.commitMutation(
83561
+ {
83562
+ type: "replace-with-keyframes",
83563
+ animationId: arcAnim.id,
83564
+ targetSelector,
83565
+ position: extended.position,
83566
+ duration: extended.duration,
83567
+ keyframes: extended.keyframes,
83568
+ ease: "none"
83569
+ },
83570
+ { label: "Add keyframe", keyframeAction: "add", softReload: true }
83571
+ );
83572
+ return;
83573
+ }
83574
+ const nodes = linearArcKeyframes(arcAnim);
83575
+ const playheadPercentage = absoluteToPercentage(t2, start, duration);
83576
+ const timedNodeIndex = nodes.findIndex(
83577
+ (node) => Math.abs(node.percentage - playheadPercentage) <= KEYFRAME_PCT_MATCH
83578
+ );
83579
+ if (timedNodeIndex !== -1) {
83580
+ if (timedNodeIndex > 0 && timedNodeIndex < nodes.length - 1) {
83581
+ await session.commitMutation(
83582
+ {
83583
+ type: "replace-with-keyframes",
83584
+ animationId: arcAnim.id,
83585
+ targetSelector,
83586
+ position: roundTo3(start),
83587
+ duration: roundTo3(duration),
83588
+ keyframes: nodes.filter((_, index) => index !== timedNodeIndex),
83589
+ ease: "none"
83590
+ },
83591
+ { label: "Remove keyframe", softReload: true }
83592
+ );
83593
+ }
83594
+ return;
83595
+ }
83596
+ const live = readElementPosition(iframe, sel, arcAnim);
83597
+ if (typeof live.x !== "number" || typeof live.y !== "number") return;
83598
+ await session.commitMutation(
83599
+ {
83600
+ type: "replace-with-keyframes",
83601
+ animationId: arcAnim.id,
83602
+ targetSelector,
83603
+ position: roundTo3(start),
83604
+ duration: roundTo3(duration),
83605
+ keyframes: buildTemporalArcKeyframes(arcAnim, playheadPercentage, {
83606
+ x: live.x,
83607
+ y: live.y
83608
+ }),
83609
+ ease: "none"
83610
+ },
83611
+ { label: "Add keyframe", keyframeAction: "add", softReload: true }
83612
+ );
83613
+ }
83614
+ function useEnableKeyframes(sessionRef) {
83615
+ return useCallback145(async () => {
83616
+ const session = sessionRef.current;
83617
+ if (!session) return;
83618
+ const sel = session.domEditSelection;
83619
+ if (!sel) return;
83620
+ const t2 = usePlayerStore.getState().currentTime;
83621
+ const iframe = session.previewIframeRef?.current ?? null;
83622
+ const fetched = await tryFetchAnimationsForElement(sel);
83623
+ const anims = fetched ?? session.selectedGsapAnimations;
83624
+ const arcAnim = anims.find((a) => a.arcPath);
83625
+ const kfAnim = anims.find((a) => a.keyframes && !a.arcPath);
83626
+ const setAnim = anims.find((a) => isInstantHold(a) && !a.keyframes && !a.arcPath);
83627
+ const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && !isInstantHold(a));
83628
+ if (arcAnim) {
83629
+ await applyArcKeyframeAtPlayhead(session, sel, arcAnim, t2, iframe);
83630
+ } else if (kfAnim) {
83631
+ await applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe);
83632
+ } else if (setAnim) {
83633
+ await promoteSetToKeyframes(session, sel, setAnim, t2, iframe);
83634
+ } else if (flatAnim) {
83635
+ const flatLive = flatAnim.durationUnresolved ? findParsedTween(iframe, sel.element, flatAnim) : void 0;
83636
+ enableKeyframesTransactionCounter += 1;
83637
+ const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
83638
+ const convertCommitOverrides = {
83639
+ keyframeTelemetry: false,
83640
+ skipReload: true,
83641
+ coalesceKey,
83642
+ coalesceMs: Number.POSITIVE_INFINITY
83643
+ };
83644
+ await session.handleGsapConvertToKeyframes(
83645
+ flatAnim.id,
83646
+ void 0,
83647
+ void 0,
83648
+ convertCommitOverrides
83649
+ );
83650
+ const converted = (await fetchAnimationsForElement(sel)).find((a) => a.keyframes);
83651
+ if (converted) {
83652
+ const applyCommitOverrides = {
83653
+ softReload: true,
83654
+ coalesceKey,
83655
+ // Must match the convert phase's window: editHistory keys coalescing
83656
+ // off the incoming entry, so without Infinity here the apply (landing
83657
+ // after two POSTs + a fetch) falls back to the 300ms default and the
83658
+ // conversion splits into two undo entries under real latency.
83659
+ coalesceMs: Number.POSITIVE_INFINITY
83660
+ };
83661
+ await applyKeyframeAtPlayhead(
83662
+ session,
83663
+ sel,
83664
+ converted,
83665
+ t2,
83666
+ iframe,
83667
+ applyCommitOverrides,
83668
+ flatLive
83669
+ );
83670
+ }
83671
+ } else {
83672
+ const position = readElementPosition(iframe, sel, null);
83673
+ if (Object.keys(position).length === 0)
83674
+ Object.assign(position, readTranslatePxLeavingPercent(sel.element));
83675
+ const mutation = singleKeyTweenMutation(sel, position, t2);
83676
+ if (mutation && session.commitMutation) {
83677
+ await session.commitMutation(mutation, { label: "Enable keyframes", softReload: true });
83678
+ } else {
83679
+ session.handleGsapAddAnimation("to");
83680
+ }
83681
+ }
83682
+ }, [sessionRef]);
83683
+ }
83684
+
83199
83685
  // src/hooks/gsapResizeIntercept.ts
83200
83686
  var SIZE_PROPS = /* @__PURE__ */ new Set(["width", "height"]);
83201
83687
  var POSITION_XY = /* @__PURE__ */ new Set(["x", "y"]);
83688
+ var MOVED_OR_SIZED = /* @__PURE__ */ new Set(["x", "y", "xPercent", "yPercent", "width", "height"]);
83202
83689
  function originalBoxSize(el, measuredAttr, inlineProperty) {
83203
83690
  const measured = Number.parseFloat(el?.getAttribute(measuredAttr) ?? "");
83204
83691
  if (Number.isFinite(measured) && measured > 0) return measured;
@@ -83218,7 +83705,8 @@ async function commitSizeAtPlayhead(selection, anim, size, iframe, dragOffset, c
83218
83705
  const anchor2 = moves && animationWritesAnyProperty(anim, POSITION_XY) ? computeDraggedGsapPosition(
83219
83706
  selection.element,
83220
83707
  dragOffset,
83221
- readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 }
83708
+ readGsapPositionFromIframe(iframe, selector) ?? { x: 0, y: 0 },
83709
+ callbacks.stamp
83222
83710
  ) : null;
83223
83711
  const written = await commitValueAtPlayhead(
83224
83712
  selection,
@@ -83232,11 +83720,28 @@ async function commitSizeAtPlayhead(selection, anim, size, iframe, dragOffset, c
83232
83720
  ...preGestureBoxSize(selection.element),
83233
83721
  ...anchor2 && { x: anchor2.baseGsapX, y: anchor2.baseGsapY }
83234
83722
  },
83235
- ...anchor2 && { beforeReload: () => restoreDragOffset(selection.element) }
83723
+ ...anchor2 && { beforeReload: () => restoreDragOffset(selection.element, callbacks.stamp) }
83236
83724
  }
83237
83725
  );
83238
83726
  return written.status === "persisted" && anchor2 ? { ...written, ownsDragOffset: true } : written;
83239
83727
  }
83728
+ function handOverDraftSize(selection, written, size, draw) {
83729
+ const { width, height } = size;
83730
+ if (written.status !== "persisted" || width == null || height == null) return written;
83731
+ draw(() => {
83732
+ clearStudioBoxSize(selection.element);
83733
+ setElementGsapSize(selection.element, width, height);
83734
+ });
83735
+ return written;
83736
+ }
83737
+ function firstSizeKey(selection, size, resizeGroup, animations) {
83738
+ const { autoKeyframeEnabled, currentTime } = usePlayerStore.getState();
83739
+ const keyframed = animations.some(
83740
+ (a) => a.keyframes && animationWritesAnyProperty(a, MOVED_OR_SIZED)
83741
+ );
83742
+ if (resizeGroup !== "size" || !autoKeyframeEnabled || !keyframed) return null;
83743
+ return singleKeyTweenMutation(selection, size, currentTime);
83744
+ }
83240
83745
  function tweenUsesScaleLonghands(anim) {
83241
83746
  const isLonghand = (name) => name === "scaleX" || name === "scaleY";
83242
83747
  const inKeyframes = (anim?.keyframes?.keyframes ?? []).some(
@@ -83244,7 +83749,7 @@ function tweenUsesScaleLonghands(anim) {
83244
83749
  );
83245
83750
  return inKeyframes || Object.keys(anim?.properties ?? {}).some(isLonghand);
83246
83751
  }
83247
- async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run()) {
83752
+ async function tryGsapResizeIntercept(selection, size, animations, iframe, commitMutation, fetchFallbackAnimations, dragOffset, draw = (run) => run(), stamp) {
83248
83753
  const fetchedAnimations = fetchFallbackAnimations ? await fetchFallbackAnimations() : [];
83249
83754
  const outcome = preflightGsapResizeIntercept(selection, animations, iframe, fetchedAnimations);
83250
83755
  if (outcome.status === "blocked") return outcome;
@@ -83269,10 +83774,16 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83269
83774
  size
83270
83775
  });
83271
83776
  if (!anim || isInstantHold(anim)) {
83777
+ const sized = { width: roundToLayoutPx(size.width), height: roundToLayoutPx(size.height) };
83272
83778
  const scriptWritesSize = allKnownAnimations.some(
83273
83779
  (a) => animationWritesAnyProperty(a, SIZE_PROPS)
83274
83780
  );
83275
- if (!scriptWritesSize) return { status: "element-size" };
83781
+ if (!scriptWritesSize) {
83782
+ const firstKey = firstSizeKey(selection, sized, resizeGroup, workingAnimations);
83783
+ if (!firstKey) return { status: "element-size" };
83784
+ await commitMutation(selection, firstKey, { label: "Resize", softReload: true });
83785
+ return handOverDraftSize(selection, { status: "persisted" }, sized, draw);
83786
+ }
83276
83787
  const sel = selectorFromSelection(selection) ?? writeTargetSelector(selection);
83277
83788
  if (!sel) return { status: "blocked", reason: "no-selector" };
83278
83789
  const sizeSet = resizeGroup === "size" ? anim ?? findSizeSetAnimation(workingAnimations, sel, selection.element) : findSizeSetAnimation(workingAnimations, sel, selection.element);
@@ -83284,11 +83795,19 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83284
83795
  );
83285
83796
  if (animatedTween) {
83286
83797
  logResize("intercept-route", { route: "keyframed-size", tweenId: animatedTween.id });
83287
- const sized = { width: roundToLayoutPx(size.width), height: roundToLayoutPx(size.height) };
83288
- return commitSizeAtPlayhead(selection, animatedTween, sized, iframe, dragOffset, {
83289
- commitMutation,
83290
- fetchAnimations: fetchFallbackAnimations
83291
- });
83798
+ const written2 = await commitSizeAtPlayhead(
83799
+ selection,
83800
+ animatedTween,
83801
+ sized,
83802
+ iframe,
83803
+ dragOffset,
83804
+ {
83805
+ commitMutation,
83806
+ fetchAnimations: fetchFallbackAnimations,
83807
+ stamp
83808
+ }
83809
+ );
83810
+ return handOverDraftSize(selection, written2, sized, draw);
83292
83811
  }
83293
83812
  }
83294
83813
  logResize("intercept-route", { route: "static-size-set", hadSizeSet: !!sizeSet, resizeGroup });
@@ -83367,7 +83886,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83367
83886
  const { baseGsapX, baseGsapY } = computeDraggedGsapPosition(
83368
83887
  selection.element,
83369
83888
  { x: 0, y: 0 },
83370
- gsapPos
83889
+ gsapPos,
83890
+ stamp
83371
83891
  );
83372
83892
  const base3 = { x: baseGsapX, y: baseGsapY };
83373
83893
  setElementGsapPosition(draftEl, base3.x, base3.y);
@@ -83408,7 +83928,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83408
83928
  assertGsapEditPersisted(
83409
83929
  await commitGsapPositionFromDrag(selection, positionTween, delta, base2, iframe, {
83410
83930
  commitMutation,
83411
- fetchAnimations: fetchFallbackAnimations
83931
+ fetchAnimations: fetchFallbackAnimations,
83932
+ stamp
83412
83933
  })
83413
83934
  );
83414
83935
  return true;
@@ -83416,7 +83937,8 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83416
83937
  const existingSet = findExistingPositionWrite(currentAnimations, selector, selection.element);
83417
83938
  await commitStaticGsapPosition(selection, delta, base2, selector, existingSet, {
83418
83939
  commitMutation,
83419
- fetchAnimations: fetchFallbackAnimations
83940
+ fetchAnimations: fetchFallbackAnimations,
83941
+ stamp
83420
83942
  });
83421
83943
  return true;
83422
83944
  };
@@ -83433,9 +83955,18 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83433
83955
  );
83434
83956
  return { status: "persisted", ownsDragOffset: await finalizeScaleResizeCommit() };
83435
83957
  }
83436
- const callbacks = { commitMutation, fetchAnimations: fetchFallbackAnimations };
83437
- if (resizeGroup === "size")
83438
- return commitSizeAtPlayhead(selection, anim, resizeProps, iframe, dragOffset, callbacks);
83958
+ const callbacks = { commitMutation, fetchAnimations: fetchFallbackAnimations, stamp };
83959
+ if (resizeGroup === "size") {
83960
+ const written2 = await commitSizeAtPlayhead(
83961
+ selection,
83962
+ anim,
83963
+ resizeProps,
83964
+ iframe,
83965
+ dragOffset,
83966
+ callbacks
83967
+ );
83968
+ return handOverDraftSize(selection, written2, resizeProps, draw);
83969
+ }
83439
83970
  const written = await commitValueAtPlayhead(selection, anim, resizeProps, iframe, callbacks, {
83440
83971
  label: "Resize",
83441
83972
  backfill: resizeBackfill
@@ -83445,7 +83976,7 @@ async function tryGsapResizeIntercept(selection, size, animations, iframe, commi
83445
83976
  }
83446
83977
 
83447
83978
  // src/hooks/useAnimatedPropertyCommit.ts
83448
- import { useCallback as useCallback145 } from "react";
83979
+ import { useCallback as useCallback146 } from "react";
83449
83980
  import { classifyPropertyGroup as classifyPropertyGroup4 } from "@hyperframes/core/gsap-parser";
83450
83981
  function pickBestAnimation(animations, selector, property) {
83451
83982
  const targetGroup = property ? classifyPropertyGroup4(property) : void 0;
@@ -83640,7 +84171,7 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
83640
84171
  for (const k of Object.keys(properties)) {
83641
84172
  if (!(k in p) && backfillDefaults[k] != null) p[k] = backfillDefaults[k];
83642
84173
  }
83643
- return { percentage: newPct, properties: p };
84174
+ return { percentage: newPct, properties: p, ...kf.ease ? { ease: kf.ease } : {} };
83644
84175
  });
83645
84176
  remapped.push({ percentage: toNewPct(ct), properties });
83646
84177
  remapped.sort((a, b) => a.percentage - b.percentage);
@@ -83652,7 +84183,8 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
83652
84183
  targetSelector: anim.targetSelector,
83653
84184
  position: roundTo3(newStart),
83654
84185
  duration: roundTo3(newDuration),
83655
- keyframes: remapped
84186
+ keyframes: remapped,
84187
+ ...keyframeEases(anim)
83656
84188
  },
83657
84189
  { label: `Edit ${primaryProp} (extended keyframe)`, keyframeAction: "add", softReload: true }
83658
84190
  );
@@ -83683,7 +84215,7 @@ async function commitKeyframeProps(selection, anim, props, propEntries, primaryP
83683
84215
  }
83684
84216
  function useAnimatedPropertyCommit(deps) {
83685
84217
  const { selectedGsapAnimations, gsapCommitMutation, previewIframeRef, bumpGsapCache } = deps;
83686
- const commitAnimatedProperties = useCallback145(
84218
+ const commitAnimatedProperties = useCallback146(
83687
84219
  // This is the single routing boundary for set, keyframe, whole-tween, and first-group writes.
83688
84220
  // fallow-ignore-next-line complexity
83689
84221
  async (selection, props, commit2 = gsapCommitMutation) => {
@@ -83789,7 +84321,7 @@ function useAnimatedPropertyCommit(deps) {
83789
84321
  },
83790
84322
  [selectedGsapAnimations, gsapCommitMutation, previewIframeRef, bumpGsapCache]
83791
84323
  );
83792
- const commitAnimatedProperty = useCallback145(
84324
+ const commitAnimatedProperty = useCallback146(
83793
84325
  (selection, property, value) => commitAnimatedProperties(selection, { [property]: value }),
83794
84326
  [commitAnimatedProperties]
83795
84327
  );
@@ -83816,14 +84348,14 @@ function useGsapAwareEditing({
83816
84348
  setArcPath,
83817
84349
  updateArcSegment
83818
84350
  }) {
83819
- const getGsapAnimationsForSelection = useCallback146(
84351
+ const getGsapAnimationsForSelection = useCallback147(
83820
84352
  (selection) => {
83821
84353
  if (domEditSelection?.element === selection.element) return selectedGsapAnimations;
83822
84354
  return makeFetchFallback(selection, { failOnFetchError: true })();
83823
84355
  },
83824
84356
  [domEditSelection, selectedGsapAnimations, makeFetchFallback]
83825
84357
  );
83826
- const handleGsapAwarePathOffsetCommit = useCallback146(
84358
+ const handleGsapAwarePathOffsetCommit = useCallback147(
83827
84359
  async (selection, next, modifiers) => {
83828
84360
  const writes = observeGsapGesture(gsapCommitMutation);
83829
84361
  if (modifiers?.plainTranslate ?? editsPlainCss(selection.element, "move")) {
@@ -83874,8 +84406,9 @@ function useGsapAwareEditing({
83874
84406
  stageElementPositionOffset,
83875
84407
  showToast
83876
84408
  });
83877
- const handleGsapAwareBoxSizeCommit = useCallback146(
84409
+ const handleGsapAwareBoxSizeCommit = useCallback147(
83878
84410
  async (selection, next, offset, restore2 = () => void 0, route) => {
84411
+ const stamp = freezeDragStamp(selection.element);
83879
84412
  const writes = observeGsapGesture(gsapCommitMutation);
83880
84413
  if (route?.plainTranslate ?? editsPlainCss(selection.element, "resize")) {
83881
84414
  const result = await handleDomBoxSizeCommit(selection, next, offset, restore2);
@@ -83916,7 +84449,8 @@ function useGsapAwareEditing({
83916
84449
  targetAnimations,
83917
84450
  previewIframeRef.current,
83918
84451
  commitMutation2,
83919
- makeFetchFallback(selection)
84452
+ makeFetchFallback(selection),
84453
+ { stamp }
83920
84454
  );
83921
84455
  await saveMove(dragOutcome, async () => {
83922
84456
  const plainAfterSettle = editsPlainCss(selection.element, "move");
@@ -83937,7 +84471,12 @@ function useGsapAwareEditing({
83937
84471
  x: 0,
83938
84472
  y: 0
83939
84473
  };
83940
- const { newX, newY } = computeDraggedGsapPosition(selection.element, offset, gsapPos);
84474
+ const { newX, newY } = computeDraggedGsapPosition(
84475
+ selection.element,
84476
+ offset,
84477
+ gsapPos,
84478
+ stamp
84479
+ );
83941
84480
  logResize("sync-settle", { gsapPos, offset, newX, newY });
83942
84481
  setElementGsapPosition(selection.element, newX, newY);
83943
84482
  });
@@ -83954,7 +84493,8 @@ function useGsapAwareEditing({
83954
84493
  commitMutation2,
83955
84494
  makeFetchFallback(selection),
83956
84495
  offset,
83957
- writes.drawKeepingUndone
84496
+ writes.drawKeepingUndone,
84497
+ stamp
83958
84498
  );
83959
84499
  assertGsapEditPersisted(outcome);
83960
84500
  if (outcome.status === "element-size") {
@@ -84008,7 +84548,7 @@ function useGsapAwareEditing({
84008
84548
  getGsapAnimationsForSelection
84009
84549
  ]
84010
84550
  );
84011
- const handleGsapAwareRotationCommit = useCallback146(
84551
+ const handleGsapAwareRotationCommit = useCallback147(
84012
84552
  async (selection, next) => {
84013
84553
  const writes = observeGsapGesture(gsapCommitMutation);
84014
84554
  if (next.plain === void 0 ? editsPlainCss(selection.element, "rotate") : next.plain) {
@@ -84051,7 +84591,7 @@ function useGsapAwareEditing({
84051
84591
  previewIframeRef,
84052
84592
  bumpGsapCache
84053
84593
  });
84054
- const commitAnimatedProperties = useCallback146(
84594
+ const commitAnimatedProperties = useCallback147(
84055
84595
  async (selection, properties) => {
84056
84596
  try {
84057
84597
  await writeSizeWithCrop(
@@ -84073,25 +84613,25 @@ function useGsapAwareEditing({
84073
84613
  trackGsapInteractionFailure
84074
84614
  ]
84075
84615
  );
84076
- const commitAnimatedProperty = useCallback146(
84616
+ const commitAnimatedProperty = useCallback147(
84077
84617
  (selection, property, value) => commitAnimatedProperties(selection, { [property]: value }),
84078
84618
  [commitAnimatedProperties]
84079
84619
  );
84080
- const handleSetArcPath = useCallback146(
84620
+ const handleSetArcPath = useCallback147(
84081
84621
  (animId, config) => {
84082
84622
  if (!domEditSelection) return;
84083
84623
  setArcPath(domEditSelection, animId, config);
84084
84624
  },
84085
84625
  [domEditSelection, setArcPath]
84086
84626
  );
84087
- const handleUpdateArcSegment = useCallback146(
84627
+ const handleUpdateArcSegment = useCallback147(
84088
84628
  (animId, segmentIndex, update) => {
84089
84629
  if (!domEditSelection) return;
84090
84630
  updateArcSegment(domEditSelection, animId, segmentIndex, update);
84091
84631
  },
84092
84632
  [domEditSelection, updateArcSegment]
84093
84633
  );
84094
- const noopCommit = useCallback146(async () => {
84634
+ const noopCommit = useCallback147(async () => {
84095
84635
  }, []);
84096
84636
  const trackGsapSaveFailure = useGsapSaveFailureTelemetry(null);
84097
84637
  const safeGsapCommit = useSafeGsapCommitMutation(
@@ -84099,14 +84639,14 @@ function useGsapAwareEditing({
84099
84639
  trackGsapSaveFailure,
84100
84640
  showToast
84101
84641
  );
84102
- const commitMutation = useCallback146(
84642
+ const commitMutation = useCallback147(
84103
84643
  async (mutation, options, selection = domEditSelection) => {
84104
84644
  if (!selection) return;
84105
84645
  await safeGsapCommit(selection, mutation, options);
84106
84646
  },
84107
84647
  [domEditSelection, safeGsapCommit]
84108
84648
  );
84109
- const handleUnroll = useCallback146(() => {
84649
+ const handleUnroll = useCallback147(() => {
84110
84650
  void commitMutation(
84111
84651
  { type: "unroll-timeline" },
84112
84652
  { label: "Unroll to literal tweens", softReload: true }
@@ -84257,12 +84797,12 @@ function useStudioSelectionPublisher({
84257
84797
  }
84258
84798
 
84259
84799
  // src/hooks/useKeyframeEaseCommits.ts
84260
- import { useCallback as useCallback147 } from "react";
84800
+ import { useCallback as useCallback148 } from "react";
84261
84801
  function useKeyframeEaseCommits({
84262
84802
  gsapCommitMutation,
84263
84803
  domEditSelectionRef
84264
84804
  }) {
84265
- const handleUpdateSegmentEase = useCallback147(
84805
+ const handleUpdateSegmentEase = useCallback148(
84266
84806
  (targets, ease) => {
84267
84807
  const selection = domEditSelectionRef.current;
84268
84808
  if (!selection || targets.length === 0) return;
@@ -84296,11 +84836,11 @@ function useKeyframeEaseCommits({
84296
84836
  },
84297
84837
  [gsapCommitMutation, domEditSelectionRef]
84298
84838
  );
84299
- const handleUpdateKeyframeEase = useCallback147(
84839
+ const handleUpdateKeyframeEase = useCallback148(
84300
84840
  (animationId, percentage, ease) => handleUpdateSegmentEase([{ animationId, tweenPercentage: percentage }], ease),
84301
84841
  [handleUpdateSegmentEase]
84302
84842
  );
84303
- const handleSetAllKeyframeEases = useCallback147(
84843
+ const handleSetAllKeyframeEases = useCallback148(
84304
84844
  (animationId, ease) => {
84305
84845
  const sel = domEditSelectionRef.current;
84306
84846
  if (!sel) return;
@@ -84736,7 +85276,7 @@ function useDomEditSession({
84736
85276
  forceReloadSdkSession,
84737
85277
  timelineElements
84738
85278
  });
84739
- const handleDomEditElementDelete = useCallback148(
85279
+ const handleDomEditElementDelete = useCallback149(
84740
85280
  async (selection, options) => {
84741
85281
  if (isRecordingRef?.current) {
84742
85282
  showToast("Cannot edit timeline while recording", "error");
@@ -84759,7 +85299,7 @@ function useDomEditSession({
84759
85299
  timelineElements
84760
85300
  ]
84761
85301
  );
84762
- const handleGroupSelection = useCallback148(() => {
85302
+ const handleGroupSelection = useCallback149(() => {
84763
85303
  const group = domEditGroupSelectionsRef.current;
84764
85304
  const single = domEditSelectionRef.current;
84765
85305
  const members = group.length > 0 ? group : single ? [single] : [];
@@ -84777,7 +85317,7 @@ function useDomEditSession({
84777
85317
  trackStudioEvent("group", { action: "create", count: members.length });
84778
85318
  void groupSelection(members);
84779
85319
  }, [domEditGroupSelectionsRef, domEditSelectionRef, groupSelection, showToast]);
84780
- const handleUngroupSelection = useCallback148(() => {
85320
+ const handleUngroupSelection = useCallback149(() => {
84781
85321
  const sel = domEditSelectionRef.current;
84782
85322
  if (!sel?.element.hasAttribute("data-hf-group")) {
84783
85323
  showToast("Select a group to ungroup", "info");
@@ -84885,7 +85425,7 @@ function useDomEditSession({
84885
85425
  setArcPath,
84886
85426
  updateArcSegment
84887
85427
  });
84888
- const handleDomGroupMoveBy = useCallback148(
85428
+ const handleDomGroupMoveBy = useCallback149(
84889
85429
  (moves) => moveDomGroupBy(
84890
85430
  moves,
84891
85431
  (updates) => handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false })
@@ -85011,7 +85551,7 @@ function useSdkSelectionSync(session, domEditSelection, domEditGroupSelections)
85011
85551
  import { useEffect as useEffect126 } from "react";
85012
85552
 
85013
85553
  // src/hooks/useSdkSession.ts
85014
- import { useState as useState121, useEffect as useEffect125, useCallback as useCallback149, useRef as useRef154 } from "react";
85554
+ import { useState as useState121, useEffect as useEffect125, useCallback as useCallback150, useRef as useRef154 } from "react";
85015
85555
  import { openComposition as openComposition3 } from "@hyperframes/sdk";
85016
85556
 
85017
85557
  // src/hooks/externalFileReloadBus.ts
@@ -85225,8 +85765,8 @@ function useSdkSession(projectId, activeCompPath, fileTree = [], fileTreeLoaded
85225
85765
  }
85226
85766
  };
85227
85767
  }, [projectId, activeCompPath, reloadToken]);
85228
- const forceReload = useCallback149(() => setReloadToken((t2) => t2 + 1), []);
85229
- const publish = useCallback149(({ candidate, expectedSession, targetPath }) => {
85768
+ const forceReload = useCallback150(() => setReloadToken((t2) => t2 + 1), []);
85769
+ const publish = useCallback150(({ candidate, expectedSession, targetPath }) => {
85230
85770
  const expectedOwner = sessionOwnersRef.current.get(expectedSession);
85231
85771
  const current = ownedSessionRef.current;
85232
85772
  if (!isSessionOwnerActive(
@@ -85269,7 +85809,7 @@ function useStudioSdkSessions(projectId, activeCompPath, masterCompPath, fileTre
85269
85809
  }
85270
85810
 
85271
85811
  // src/hooks/useStudioExternalFileChanges.ts
85272
- import { useCallback as useCallback151 } from "react";
85812
+ import { useCallback as useCallback152 } from "react";
85273
85813
 
85274
85814
  // src/utils/externalConflictStorage.ts
85275
85815
  var DB_NAME = "hyperframes-studio-external-conflicts";
@@ -85370,7 +85910,7 @@ async function deleteExternalConflictSnapshot(projectId, filePath) {
85370
85910
  }
85371
85911
 
85372
85912
  // src/hooks/useExternalFileChangeCoordinator.ts
85373
- import { useCallback as useCallback150, useEffect as useEffect127, useRef as useRef155, useState as useState122 } from "react";
85913
+ import { useCallback as useCallback151, useEffect as useEffect127, useRef as useRef155, useState as useState122 } from "react";
85374
85914
  function testHotAdapter() {
85375
85915
  const value = globalThis.__HF_STUDIO_HOT_TEST_ADAPTER__;
85376
85916
  if (!value || typeof value !== "object") return null;
@@ -85431,7 +85971,7 @@ function useExternalFileChangeCoordinator({
85431
85971
  const drainingRef = useRef155(false);
85432
85972
  const pendingPayloadRef = useRef155(null);
85433
85973
  blockedRef.current = blocked;
85434
- const setBlocked = useCallback150((next) => {
85974
+ const setBlocked = useCallback151((next) => {
85435
85975
  setBlockedState(
85436
85976
  (held) => next && held ? { ...next, payload: mergeFileChangeAffectedCompositions(held.payload, next.payload) } : next
85437
85977
  );
@@ -85486,7 +86026,7 @@ function useExternalFileChangeCoordinator({
85486
86026
  cancelled = true;
85487
86027
  };
85488
86028
  }, [loadConflictSnapshot, projectId, recoveryFilePath, setBlocked]);
85489
- const reloadAcceptedGeneration = useCallback150(
86029
+ const reloadAcceptedGeneration = useCallback151(
85490
86030
  (path, affectsPreview = true) => {
85491
86031
  logReload(affectsPreview ? "reload" : "file-tree only", {
85492
86032
  path,
@@ -85498,7 +86038,7 @@ function useExternalFileChangeCoordinator({
85498
86038
  },
85499
86039
  [reloadPreview, reloadSdkSession, refreshFileTree]
85500
86040
  );
85501
- const persistSnapshotInOrder = useCallback150(async (write) => {
86041
+ const persistSnapshotInOrder = useCallback151(async (write) => {
85502
86042
  const next = snapshotWriteTailRef.current.catch(() => void 0).then(write);
85503
86043
  snapshotWriteTailRef.current = next.then(
85504
86044
  () => void 0,
@@ -85506,7 +86046,7 @@ function useExternalFileChangeCoordinator({
85506
86046
  );
85507
86047
  await next;
85508
86048
  }, []);
85509
- const drainOnePending = useCallback150(
86049
+ const drainOnePending = useCallback151(
85510
86050
  // fallow-ignore-next-line complexity
85511
86051
  async (payload) => {
85512
86052
  const path = readStudioFileChangePath(payload);
@@ -85605,7 +86145,7 @@ function useExternalFileChangeCoordinator({
85605
86145
  onAcceptedPersistedFileChange
85606
86146
  ]
85607
86147
  );
85608
- const startDrainLoop = useCallback150(async () => {
86148
+ const startDrainLoop = useCallback151(async () => {
85609
86149
  if (drainingRef.current) return;
85610
86150
  drainingRef.current = true;
85611
86151
  try {
@@ -85619,7 +86159,7 @@ function useExternalFileChangeCoordinator({
85619
86159
  drainingRef.current = false;
85620
86160
  }
85621
86161
  }, [drainOnePending]);
85622
- const processChange = useCallback150(
86162
+ const processChange = useCallback151(
85623
86163
  // fallow-ignore-next-line complexity
85624
86164
  (payload) => {
85625
86165
  const path = readStudioFileChangePath(payload);
@@ -85678,14 +86218,14 @@ function useExternalFileChangeCoordinator({
85678
86218
  }
85679
86219
  return sseFileChangeChannel(handler);
85680
86220
  }, []);
85681
- const retry = useCallback150(async () => {
86221
+ const retry = useCallback151(async () => {
85682
86222
  const current = blockedRef.current;
85683
86223
  if (!current || current.status === "conflict" || current.recovered) return;
85684
86224
  resetSaveQueues?.();
85685
86225
  lastEventIdentityRef.current = null;
85686
86226
  processChange(current.payload);
85687
86227
  }, [processChange, resetSaveQueues]);
85688
- const useExternalFile = useCallback150(
86228
+ const useExternalFile = useCallback151(
85689
86229
  // fallow-ignore-next-line complexity
85690
86230
  async () => {
85691
86231
  const current = blockedRef.current;
@@ -85713,7 +86253,7 @@ function useExternalFileChangeCoordinator({
85713
86253
  resetSaveQueues
85714
86254
  ]
85715
86255
  );
85716
- const keepStudioFile = useCallback150(async () => {
86256
+ const keepStudioFile = useCallback151(async () => {
85717
86257
  const current = blockedRef.current;
85718
86258
  if (!current || !projectId) return;
85719
86259
  if (current.generation !== generationRef.current) return;
@@ -85784,19 +86324,19 @@ function useStudioExternalFileChanges({
85784
86324
  const { flushPendingSourceSave, discardPendingSourceSave } = fileManager;
85785
86325
  const { drainPendingDomEditSaves, resetDomEditSaveQueueBreaker } = previewPersistence;
85786
86326
  const bumpThumbnailRevisions = usePlayerStore((state) => state.bumpThumbnailRevisions);
85787
- const onAcceptedPersistedFileChange = useCallback151(
86327
+ const onAcceptedPersistedFileChange = useCallback152(
85788
86328
  (_path, affectedCompositions) => {
85789
86329
  onOutsideChange?.();
85790
86330
  bumpThumbnailRevisions(affectedCompositions);
85791
86331
  },
85792
86332
  [bumpThumbnailRevisions, onOutsideChange]
85793
86333
  );
85794
- const drainPendingChanges = useCallback151(async () => {
86334
+ const drainPendingChanges = useCallback152(async () => {
85795
86335
  const source = await flushPendingSourceSave();
85796
86336
  if (source.status !== "clean") return source;
85797
86337
  return drainPendingDomEditSaves();
85798
86338
  }, [drainPendingDomEditSaves, flushPendingSourceSave]);
85799
- const discardPendingChanges = useCallback151(() => {
86339
+ const discardPendingChanges = useCallback152(() => {
85800
86340
  discardPendingSourceSave();
85801
86341
  resetDomEditSaveQueueBreaker();
85802
86342
  }, [discardPendingSourceSave, resetDomEditSaveQueueBreaker]);
@@ -85824,7 +86364,7 @@ function useStudioExternalFileChanges({
85824
86364
  }
85825
86365
 
85826
86366
  // src/hooks/useBlockHandlers.ts
85827
- import { useCallback as useCallback152, useMemo as useMemo75, useRef as useRef156, useState as useState123 } from "react";
86367
+ import { useCallback as useCallback153, useMemo as useMemo75, useRef as useRef156, useState as useState123 } from "react";
85828
86368
 
85829
86369
  // src/utils/blockInstaller.ts
85830
86370
  function getMaxZIndexFromIframe(iframe) {
@@ -86043,7 +86583,7 @@ function useBlockHandlers({
86043
86583
  );
86044
86584
  const installingBlockRef = useRef156(false);
86045
86585
  const { showToast, dismissToast } = blockCtxDeps;
86046
- const runBlockInstall = useCallback152(
86586
+ const runBlockInstall = useCallback153(
86047
86587
  async (blockName, install) => {
86048
86588
  if (installingBlockRef.current) {
86049
86589
  showToast("A block is already installing \u2014 one moment\u2026", "info");
@@ -86060,7 +86600,7 @@ function useBlockHandlers({
86060
86600
  },
86061
86601
  [showToast, dismissToast]
86062
86602
  );
86063
- const handleAddBlock = useCallback152(
86603
+ const handleAddBlock = useCallback153(
86064
86604
  (blockName) => {
86065
86605
  if (!projectId) return;
86066
86606
  void (async () => {
@@ -86090,7 +86630,7 @@ function useBlockHandlers({
86090
86630
  },
86091
86631
  [projectId, blockCtx, previewIframeRef, runBlockInstall, setRightCollapsed, setRightPanelTab]
86092
86632
  );
86093
- const handleTimelineBlockDrop = useCallback152(
86633
+ const handleTimelineBlockDrop = useCallback153(
86094
86634
  (blockName, placement) => {
86095
86635
  if (!projectId) return;
86096
86636
  void runBlockInstall(
@@ -86109,7 +86649,7 @@ function useBlockHandlers({
86109
86649
  },
86110
86650
  [projectId, blockCtx, previewIframeRef, runBlockInstall]
86111
86651
  );
86112
- const handleAddMediaOverlay = useCallback152(
86652
+ const handleAddMediaOverlay = useCallback153(
86113
86653
  async (blockName, placement) => {
86114
86654
  if (!projectId) return;
86115
86655
  const { compositionPath, ...timelinePlacement } = placement;
@@ -86128,7 +86668,7 @@ function useBlockHandlers({
86128
86668
  },
86129
86669
  [projectId, blockCtx, previewIframeRef, runBlockInstall]
86130
86670
  );
86131
- const handlePreviewBlockDrop = useCallback152(
86671
+ const handlePreviewBlockDrop = useCallback153(
86132
86672
  (blockName, position) => {
86133
86673
  if (!projectId) return;
86134
86674
  void runBlockInstall(
@@ -86158,7 +86698,7 @@ function useBlockHandlers({
86158
86698
  }
86159
86699
 
86160
86700
  // src/hooks/useAppHotkeys.ts
86161
- import { useCallback as useCallback154, useEffect as useEffect128, useRef as useRef157 } from "react";
86701
+ import { useCallback as useCallback155, useEffect as useEffect128, useRef as useRef157 } from "react";
86162
86702
 
86163
86703
  // src/hooks/appHotkeysDispatch.ts
86164
86704
  function handleUndoRedoKey(event, onUndo, onRedo) {
@@ -86355,7 +86895,7 @@ function dispatchPlainKey(event, key2, cb) {
86355
86895
  }
86356
86896
 
86357
86897
  // src/hooks/useEditHistoryActions.ts
86358
- import { useCallback as useCallback153, useMemo as useMemo76 } from "react";
86898
+ import { useCallback as useCallback154, useMemo as useMemo76 } from "react";
86359
86899
  function useEditHistoryActions({
86360
86900
  editHistory,
86361
86901
  readOptionalProjectFile,
@@ -86369,20 +86909,21 @@ function useEditHistoryActions({
86369
86909
  activeCompPath,
86370
86910
  forceReloadSdkSession
86371
86911
  }) {
86372
- const readHistoryFile = useCallback153(
86912
+ const readHistoryFile = useCallback154(
86373
86913
  (path) => path === STUDIO_MOTION_PATH ? readOptionalProjectFile(path) : readProjectFile(path),
86374
86914
  [readOptionalProjectFile, readProjectFile]
86375
86915
  );
86376
- const serializeHistoryFiles = useCallback153(
86916
+ const serializeHistoryFiles = useCallback154(
86377
86917
  (paths, task) => serializeStudioFileMutations(writeProjectFile, paths, task),
86378
86918
  [writeProjectFile]
86379
86919
  );
86380
- const apply = useCallback153(
86920
+ const apply = useCallback154(
86381
86921
  async (direction) => {
86382
86922
  const noun = direction === "undo" ? "Undo" : "Redo";
86383
86923
  const pendingEditShown = direction === "undo" ? paintBackNewestStudioPendingEdit() : null;
86384
86924
  const predicted = pendingEditShown ? null : editHistory.predict?.(direction) ?? null;
86385
86925
  const predictedShown = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
86926
+ const shownLanded = predictedShown ? beginStudioShownRestore() : null;
86386
86927
  const putBack = predictedShown ?? pendingEditShown?.showAgain;
86387
86928
  const claimedAfter = pendingEditShown?.claimsAtBegin ?? (direction === "undo" && hasStudioPendingEdits() ? editHistory.claims?.() : void 0);
86388
86929
  let result = { ok: false, reason: "failed" };
@@ -86402,6 +86943,7 @@ function useEditHistoryActions({
86402
86943
  serverSteppedShown = predictedShown ? stepped && result.undoes === predicted?.id : stepped && editClaimed;
86403
86944
  } finally {
86404
86945
  if (putBack && !serverSteppedShown && !revertIsTheUndo) putBack();
86946
+ shownLanded?.();
86405
86947
  }
86406
86948
  if (!result.ok && result.reason === "content-mismatch") {
86407
86949
  showToast(
@@ -86438,8 +86980,8 @@ function useEditHistoryActions({
86438
86980
  forceReloadSdkSession
86439
86981
  ]
86440
86982
  );
86441
- const undo = useCallback153(() => apply("undo"), [apply]);
86442
- const redo = useCallback153(() => apply("redo"), [apply]);
86983
+ const undo = useCallback154(() => apply("undo"), [apply]);
86984
+ const redo = useCallback154(() => apply("redo"), [apply]);
86443
86985
  return useMemo76(() => ({ undo, redo }), [undo, redo]);
86444
86986
  }
86445
86987
  function fromShown(files, shown2) {
@@ -86528,7 +87070,7 @@ function useAppHotkeys({
86528
87070
  activeCompPath,
86529
87071
  forceReloadSdkSession
86530
87072
  });
86531
- const applyHistory = useCallback154(
87073
+ const applyHistory = useCallback155(
86532
87074
  async (direction) => {
86533
87075
  const captionState = useCaptionStore.getState();
86534
87076
  if (captionState.isEditMode && isCaptionPreviewVisible()) {
@@ -86544,8 +87086,8 @@ function useAppHotkeys({
86544
87086
  },
86545
87087
  [editHistory.state, fileHistory, showToast]
86546
87088
  );
86547
- const handleUndo = useCallback154(() => applyHistory("undo"), [applyHistory]);
86548
- const handleRedo = useCallback154(() => applyHistory("redo"), [applyHistory]);
87089
+ const handleUndo = useCallback155(() => applyHistory("undo"), [applyHistory]);
87090
+ const handleRedo = useCallback155(() => applyHistory("redo"), [applyHistory]);
86549
87091
  const cbRef = useRef157(null);
86550
87092
  cbRef.current = {
86551
87093
  handleTimelineElementsDelete,
@@ -86568,7 +87110,7 @@ function useAppHotkeys({
86568
87110
  showToast,
86569
87111
  readOnlyPreview
86570
87112
  };
86571
- const handleAppKeyDown = useCallback154((event) => {
87113
+ const handleAppKeyDown = useCallback155((event) => {
86572
87114
  const cb = cbRef.current;
86573
87115
  const key2 = event.key.toLowerCase();
86574
87116
  if (dispatchLinkShortcut(event, cb)) return;
@@ -86582,7 +87124,7 @@ function useAppHotkeys({
86582
87124
  window.addEventListener("keydown", handleAppKeyDown, true);
86583
87125
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
86584
87126
  }, [handleAppKeyDown]);
86585
- const syncPreviewHotkeys = useCallback154(
87127
+ const syncPreviewHotkeys = useCallback155(
86586
87128
  (iframe) => {
86587
87129
  previewHistoryCleanupRef.current?.();
86588
87130
  previewHistoryCleanupRef.current = null;
@@ -86612,7 +87154,7 @@ function useAppHotkeys({
86612
87154
  }
86613
87155
 
86614
87156
  // src/hooks/useClipboard.ts
86615
- import { useCallback as useCallback155, useRef as useRef158 } from "react";
87157
+ import { useCallback as useCallback156, useRef as useRef158 } from "react";
86616
87158
  import { EXCLUDED_TAGS, mintHfId, walkCompositionDescendants as walkCompositionDescendants2 } from "@hyperframes/parsers/hf-ids";
86617
87159
 
86618
87160
  // src/utils/clipboardPayload.ts
@@ -86892,7 +87434,7 @@ function useClipboard({
86892
87434
  const clipboardRef = useRef158(null);
86893
87435
  const projectIdRef = useRef158(projectId);
86894
87436
  projectIdRef.current = projectId;
86895
- const readSaved = useCallback155(
87437
+ const readSaved = useCallback156(
86896
87438
  async (path) => {
86897
87439
  const pid = projectIdRef.current;
86898
87440
  if (!pid) throw new Error("No project is open.");
@@ -86906,7 +87448,7 @@ function useClipboard({
86906
87448
  },
86907
87449
  [waitForPendingDomEditSaves, writeProjectFile]
86908
87450
  );
86909
- const findSelectedClips = useCallback155(() => {
87451
+ const findSelectedClips = useCallback156(() => {
86910
87452
  const selected = getSelectedElements();
86911
87453
  if (selected.length === 0) return null;
86912
87454
  const lives = [];
@@ -86920,7 +87462,7 @@ function useClipboard({
86920
87462
  }
86921
87463
  return { elements: selected, lives };
86922
87464
  }, [activeCompPath, previewIframeRef, showToast]);
86923
- const readClips = useCallback155(
87465
+ const readClips = useCallback156(
86924
87466
  async (targets) => {
86925
87467
  const paths = targets.elements.map((el) => el.sourceFile || activeCompPath || "index.html");
86926
87468
  const markup = await readSavedMarkup(readSaved, paths, targets.lives);
@@ -86936,7 +87478,7 @@ function useClipboard({
86936
87478
  },
86937
87479
  [activeCompPath, readSaved]
86938
87480
  );
86939
- const copyTimelineSelection = useCallback155(() => {
87481
+ const copyTimelineSelection = useCallback156(() => {
86940
87482
  const targets = findSelectedClips();
86941
87483
  if (!targets) return null;
86942
87484
  const sourceFile = targets.elements[0]?.sourceFile || activeCompPath || "index.html";
@@ -86951,7 +87493,7 @@ function useClipboard({
86951
87493
  };
86952
87494
  });
86953
87495
  }, [activeCompPath, findSelectedClips, readClips, showToast]);
86954
- const copyDomSelection = useCallback155(
87496
+ const copyDomSelection = useCallback156(
86955
87497
  (domSelection) => {
86956
87498
  const live = getSelectedDomElement(previewIframeRef, domSelection, activeCompPath);
86957
87499
  if (!live) {
@@ -86973,7 +87515,7 @@ function useClipboard({
86973
87515
  },
86974
87516
  [activeCompPath, previewIframeRef, readSaved, showToast]
86975
87517
  );
86976
- const copyToClipboard = useCallback155(() => {
87518
+ const copyToClipboard = useCallback156(() => {
86977
87519
  const domSelection = domEditSelectionRef.current;
86978
87520
  let pending2;
86979
87521
  if (usePlayerStore.getState().selectedElementId) pending2 = copyTimelineSelection();
@@ -86995,8 +87537,8 @@ function useClipboard({
86995
87537
  });
86996
87538
  return own;
86997
87539
  }, [copyDomSelection, copyTimelineSelection, domEditSelectionRef, showToast]);
86998
- const handleCopy2 = useCallback155(() => copyToClipboard() !== null, [copyToClipboard]);
86999
- const handlePaste2 = useCallback155(
87540
+ const handleCopy2 = useCallback156(() => copyToClipboard() !== null, [copyToClipboard]);
87541
+ const handlePaste2 = useCallback156(
87000
87542
  async (options) => {
87001
87543
  const payload = await clipboardRef.current;
87002
87544
  if (!payload) {
@@ -87044,7 +87586,7 @@ function useClipboard({
87044
87586
  },
87045
87587
  [activeCompPath, recordEdit, reloadPreview, showToast, writeProjectFile]
87046
87588
  );
87047
- const handleDuplicate = useCallback155(
87589
+ const handleDuplicate = useCallback156(
87048
87590
  async (options) => {
87049
87591
  const targets = findSelectedClips();
87050
87592
  if (!targets) return false;
@@ -87096,7 +87638,7 @@ function useClipboard({
87096
87638
  writeProjectFile
87097
87639
  ]
87098
87640
  );
87099
- const handleCut = useCallback155(async () => {
87641
+ const handleCut = useCallback156(async () => {
87100
87642
  const selected = getSelectedElements();
87101
87643
  const domSelection = domEditSelectionRef.current;
87102
87644
  const copied = copyToClipboard();
@@ -87116,7 +87658,7 @@ function useClipboard({
87116
87658
  handleTimelineElementsDelete,
87117
87659
  handleDomEditElementDelete
87118
87660
  ]);
87119
- const canPaste = useCallback155(() => clipboardRef.current !== null, []);
87661
+ const canPaste = useCallback156(() => clipboardRef.current !== null, []);
87120
87662
  return { handleCopy: handleCopy2, handlePaste: handlePaste2, handleCut, handleDuplicate, canPaste };
87121
87663
  }
87122
87664
 
@@ -87475,11 +88017,11 @@ function useCaptionDetection({
87475
88017
  }
87476
88018
 
87477
88019
  // src/hooks/useConsoleErrorCapture.ts
87478
- import { useCallback as useCallback156, useEffect as useEffect130, useRef as useRef160, useState as useState124 } from "react";
88020
+ import { useCallback as useCallback157, useEffect as useEffect130, useRef as useRef160, useState as useState124 } from "react";
87479
88021
  function useConsoleErrorCapture(previewIframe) {
87480
88022
  const [consoleErrors, setConsoleErrors] = useState124(null);
87481
88023
  const consoleErrorsRef = useRef160([]);
87482
- const resetErrors = useCallback156(() => {
88024
+ const resetErrors = useCallback157(() => {
87483
88025
  consoleErrorsRef.current = [];
87484
88026
  setConsoleErrors(null);
87485
88027
  }, []);
@@ -87548,7 +88090,7 @@ function useConsoleErrorCapture(previewIframe) {
87548
88090
  }
87549
88091
 
87550
88092
  // src/hooks/useFrameCapture.ts
87551
- import { useMemo as useMemo77, useState as useState125, useCallback as useCallback157, useRef as useRef161 } from "react";
88093
+ import { useMemo as useMemo77, useState as useState125, useCallback as useCallback158, useRef as useRef161 } from "react";
87552
88094
  function useFrameCapture({
87553
88095
  projectId,
87554
88096
  activeCompPath,
@@ -87566,16 +88108,16 @@ function useFrameCapture({
87566
88108
  livePlayheadRef.current = time;
87567
88109
  })
87568
88110
  );
87569
- const playheadTime = useCallback157(() => {
88111
+ const playheadTime = useCallback158(() => {
87570
88112
  const { isPlaying, currentTime } = usePlayerStore.getState();
87571
88113
  return isPlaying ? livePlayheadRef.current : currentTime;
87572
88114
  }, []);
87573
88115
  const [captureStamp, setCaptureStamp] = useState125(Date.now);
87574
- const refreshCaptureFrameTime = useCallback157(() => {
88116
+ const refreshCaptureFrameTime = useCallback158(() => {
87575
88117
  setCaptureFrameTime(playheadTime());
87576
88118
  setCaptureStamp(Date.now());
87577
88119
  }, [playheadTime]);
87578
- const handleCaptureFrameClick = useCallback157(
88120
+ const handleCaptureFrameClick = useCallback158(
87579
88121
  async (event) => {
87580
88122
  if (!projectId) return;
87581
88123
  event.preventDefault();
@@ -87659,7 +88201,7 @@ function useFrameCapture({
87659
88201
  }
87660
88202
 
87661
88203
  // src/hooks/useLintModal.ts
87662
- import { useState as useState126, useCallback as useCallback158, useEffect as useEffect131, useRef as useRef162, useMemo as useMemo78 } from "react";
88204
+ import { useState as useState126, useCallback as useCallback159, useEffect as useEffect131, useRef as useRef162, useMemo as useMemo78 } from "react";
87663
88205
  function parseFinding(f) {
87664
88206
  return {
87665
88207
  severity: f.severity === "error" ? "error" : "warning",
@@ -87679,7 +88221,7 @@ function useLintModal(projectId, refreshKey) {
87679
88221
  const [linting, setLinting] = useState126(false);
87680
88222
  const [backgroundFindings, setBackgroundFindings] = useState126([]);
87681
88223
  const autoLintRanRef = useRef162(false);
87682
- const runBackgroundLint = useCallback158(async () => {
88224
+ const runBackgroundLint = useCallback159(async () => {
87683
88225
  if (!projectId) return;
87684
88226
  if (!isPreviewBooted(projectId) && !await whenPreviewBooted(projectId)) return;
87685
88227
  try {
@@ -87687,7 +88229,7 @@ function useLintModal(projectId, refreshKey) {
87687
88229
  } catch {
87688
88230
  }
87689
88231
  }, [projectId]);
87690
- const handleLint = useCallback158(async () => {
88232
+ const handleLint = useCallback159(async () => {
87691
88233
  if (!projectId) return;
87692
88234
  setLinting(true);
87693
88235
  try {
@@ -87716,8 +88258,8 @@ function useLintModal(projectId, refreshKey) {
87716
88258
  const timer = setTimeout(() => void runBackgroundLint(), 1e3);
87717
88259
  return () => clearTimeout(timer);
87718
88260
  }, [projectId, refreshKey, runBackgroundLint]);
87719
- const closeLintModal = useCallback158(() => setLintModal(null), []);
87720
- const groupFindings = useCallback158(
88261
+ const closeLintModal = useCallback159(() => setLintModal(null), []);
88262
+ const groupFindings = useCallback159(
87721
88263
  (keyFn) => {
87722
88264
  const map = /* @__PURE__ */ new Map();
87723
88265
  for (const f of backgroundFindings) {
@@ -87754,7 +88296,7 @@ function useLintModal(projectId, refreshKey) {
87754
88296
  }
87755
88297
 
87756
88298
  // src/hooks/useToast.ts
87757
- import { useState as useState127, useCallback as useCallback159, useRef as useRef163 } from "react";
88299
+ import { useState as useState127, useCallback as useCallback160, useRef as useRef163 } from "react";
87758
88300
  var AUTO_DISMISS_MS2 = 4e3;
87759
88301
  var EXIT_MS2 = 160;
87760
88302
  var MAX_TOASTS = 3;
@@ -87762,21 +88304,21 @@ var nextToastId = 1;
87762
88304
  function useToast() {
87763
88305
  const [toasts, setToasts] = useState127([]);
87764
88306
  const timersRef = useRef163(/* @__PURE__ */ new Map());
87765
- const clearTimer = useCallback159((id) => {
88307
+ const clearTimer = useCallback160((id) => {
87766
88308
  const timer = timersRef.current.get(id);
87767
88309
  if (timer) {
87768
88310
  clearTimeout(timer);
87769
88311
  timersRef.current.delete(id);
87770
88312
  }
87771
88313
  }, []);
87772
- const removeToast = useCallback159(
88314
+ const removeToast = useCallback160(
87773
88315
  (id) => {
87774
88316
  clearTimer(id);
87775
88317
  setToasts((prev) => prev.filter((t2) => t2.id !== id));
87776
88318
  },
87777
88319
  [clearTimer]
87778
88320
  );
87779
- const dismissToast = useCallback159(
88321
+ const dismissToast = useCallback160(
87780
88322
  (id) => {
87781
88323
  clearTimer(id);
87782
88324
  setToasts((prev) => prev.map((t2) => t2.id === id ? { ...t2, leaving: true } : t2));
@@ -87785,7 +88327,7 @@ function useToast() {
87785
88327
  },
87786
88328
  [clearTimer, removeToast]
87787
88329
  );
87788
- const showToast = useCallback159(
88330
+ const showToast = useCallback160(
87789
88331
  (message, tone = "error") => {
87790
88332
  const id = nextToastId++;
87791
88333
  setToasts((prev) => {
@@ -87812,7 +88354,7 @@ function useToast() {
87812
88354
  }
87813
88355
 
87814
88356
  // src/hooks/useStudioUrlState.ts
87815
- import { useCallback as useCallback160, useEffect as useEffect132, useRef as useRef164, useState as useState128 } from "react";
88357
+ import { useCallback as useCallback161, useEffect as useEffect132, useRef as useRef164, useState as useState128 } from "react";
87816
88358
 
87817
88359
  // src/utils/studioUrlState.ts
87818
88360
  var VALID_TABS = ["layers", "design", "renders", "slideshow", "variables"];
@@ -88051,7 +88593,7 @@ function useStudioUrlState({
88051
88593
  const pendingSelectionRef = useRef164(initialState2.selection);
88052
88594
  const stableTimeRef = useRef164(initialState2.currentTime);
88053
88595
  const selectionApplySeqRef = useRef164(0);
88054
- const buildUrlState = useCallback160(
88596
+ const buildUrlState = useCallback161(
88055
88597
  () => ({
88056
88598
  activeCompPath,
88057
88599
  currentTime: stableTimeRef.current,
@@ -88062,7 +88604,7 @@ function useStudioUrlState({
88062
88604
  }),
88063
88605
  [activeCompPath, domEditGroupSelections, domEditSelection, rightCollapsed, rightPanelTab]
88064
88606
  );
88065
- const applyUrlSelection = useCallback160(
88607
+ const applyUrlSelection = useCallback161(
88066
88608
  (selection) => {
88067
88609
  const applySeq = ++selectionApplySeqRef.current;
88068
88610
  if (!selection) {
@@ -88207,7 +88749,7 @@ function useEffectiveTimelineDuration(timelineDuration2, timelineElements) {
88207
88749
  }
88208
88750
 
88209
88751
  // src/hooks/useStudioContextValue.ts
88210
- import { useCallback as useCallback161, useMemo as useMemo80 } from "react";
88752
+ import { useCallback as useCallback162, useMemo as useMemo80 } from "react";
88211
88753
  function buildStudioContextValue(input) {
88212
88754
  return {
88213
88755
  projectId: input.projectId,
@@ -88252,11 +88794,11 @@ function useInspectorState(rightPanel, isPlaying, domEditSelection, isGestureRec
88252
88794
  }, [rightPanel, isPlaying, isGestureRecording, domEditSelection]);
88253
88795
  }
88254
88796
  function useFileDropTarget(onImportFiles) {
88255
- const onDragOver = useCallback161((e) => {
88797
+ const onDragOver = useCallback162((e) => {
88256
88798
  if (!e.dataTransfer.types.includes("Files")) return;
88257
88799
  e.preventDefault();
88258
88800
  }, []);
88259
- const onDrop = useCallback161(
88801
+ const onDrop = useCallback162(
88260
88802
  (e) => {
88261
88803
  if (e.defaultPrevented) return;
88262
88804
  e.preventDefault();
@@ -88267,7 +88809,7 @@ function useFileDropTarget(onImportFiles) {
88267
88809
  return { onDragOver, onDrop };
88268
88810
  }
88269
88811
  function useGlobalFileDrop(handleTimelineFileDrop) {
88270
- const onDrop = useCallback161(
88812
+ const onDrop = useCallback162(
88271
88813
  (files) => {
88272
88814
  const start = usePlayerStore.getState().currentTime;
88273
88815
  void handleTimelineFileDrop(Array.from(files), { start, track: 0 });
@@ -88754,10 +89296,10 @@ var StudioHeader = memo44(function StudioHeader2({
88754
89296
  });
88755
89297
 
88756
89298
  // src/hooks/useGestureCommit.ts
88757
- import { useState as useState131, useCallback as useCallback163, useRef as useRef167, useEffect as useEffect135 } from "react";
89299
+ import { useState as useState131, useCallback as useCallback164, useRef as useRef167, useEffect as useEffect135 } from "react";
88758
89300
 
88759
89301
  // src/hooks/useGestureRecording.ts
88760
- import { useCallback as useCallback162, useEffect as useEffect134, useRef as useRef166, useState as useState130 } from "react";
89302
+ import { useCallback as useCallback163, useEffect as useEffect134, useRef as useRef166, useState as useState130 } from "react";
88761
89303
  function readBasePosition(element, iframeEl) {
88762
89304
  let baseOpacity = 1;
88763
89305
  let baseScale = 1;
@@ -88914,7 +89456,7 @@ function useGestureRecording() {
88914
89456
  r.cleanup = null;
88915
89457
  };
88916
89458
  }, []);
88917
- const startRecording = useCallback162(
89459
+ const startRecording = useCallback163(
88918
89460
  (element, iframeEl, elementEndTime) => {
88919
89461
  if (isRecordingRef.current) return;
88920
89462
  isRecordingRef.current = true;
@@ -89013,7 +89555,7 @@ function useGestureRecording() {
89013
89555
  []
89014
89556
  // No deps — uses refs only for all mutable state
89015
89557
  );
89016
- const stopRecording = useCallback162(() => {
89558
+ const stopRecording = useCallback163(() => {
89017
89559
  if (!isRecordingRef.current) return [];
89018
89560
  isRecordingRef.current = false;
89019
89561
  const r = refs.current;
@@ -89025,7 +89567,7 @@ function useGestureRecording() {
89025
89567
  setIsRecording(false);
89026
89568
  return frozen;
89027
89569
  }, []);
89028
- const clearSamples = useCallback162(() => {
89570
+ const clearSamples = useCallback163(() => {
89029
89571
  const r = refs.current;
89030
89572
  r.samples = [];
89031
89573
  r.trail = [];
@@ -89297,7 +89839,7 @@ function useGestureCommit({
89297
89839
  const capturedSelectionRef = useRef167(null);
89298
89840
  const capturedAnimationsRef = useRef167([]);
89299
89841
  useEffect135(() => () => clearInterval(recordingAutoStopRef.current), []);
89300
- const cancelRecording = useCallback163(() => {
89842
+ const cancelRecording = useCallback164(() => {
89301
89843
  clearInterval(recordingAutoStopRef.current);
89302
89844
  gestureRecording.stopRecording();
89303
89845
  gestureRecording.clearSamples();
@@ -89309,7 +89851,7 @@ function useGestureCommit({
89309
89851
  useEffect135(() => {
89310
89852
  if (readOnlyPreview && gestureStateRef.current === "recording") cancelRecording();
89311
89853
  }, [cancelRecording, readOnlyPreview]);
89312
- const stopAndCommitRecording = useCallback163(async () => {
89854
+ const stopAndCommitRecording = useCallback164(async () => {
89313
89855
  clearInterval(recordingAutoStopRef.current);
89314
89856
  if (commitInFlightRef.current) {
89315
89857
  return;
@@ -89403,19 +89945,28 @@ function useGestureCommit({
89403
89945
  const overlaps3 = recStart < tweenEnd + 0.05 && recEnd > tweenStart - 0.05;
89404
89946
  if (overlaps3) {
89405
89947
  const existingKfs = existingPositionTween.keyframes?.keyframes ?? [];
89406
- const rangeStartPct = tweenDur > 0 ? Math.max(0, (recStart - tweenStart) / tweenDur * 100) : 0;
89407
- const rangeEndPct = tweenDur > 0 ? Math.min(100, (recEnd - tweenStart) / tweenDur * 100) : 100;
89408
- const preserved = existingKfs.filter(
89409
- (kf) => kf.percentage < rangeStartPct - 0.5 || kf.percentage > rangeEndPct + 0.5
89410
- ).map((kf) => ({
89948
+ const startTimePct = tweenDur > 0 ? Math.max(0, (recStart - tweenStart) / tweenDur * 100) : 0;
89949
+ const endTimePct = tweenDur > 0 ? Math.min(100, (recEnd - tweenStart) / tweenDur * 100) : 100;
89950
+ const runEase = runEaseOf(existingPositionTween);
89951
+ const roundPct3 = warpsTime(runEase) ? (pct) => pct : roundTo3;
89952
+ const preserved = existingKfs.filter((kf) => {
89953
+ const playsAt = timeAtProgress(runEase, kf.percentage);
89954
+ return playsAt < startTimePct - 0.5 || playsAt > endTimePct + 0.5;
89955
+ }).map((kf) => ({
89411
89956
  percentage: kf.percentage,
89412
89957
  properties: kf.properties,
89413
89958
  ...kf.ease ? { ease: kf.ease } : {}
89414
89959
  }));
89415
- const mapped = keyframes.map((kf) => ({
89416
- percentage: rangeStartPct + kf.percentage / 100 * (rangeEndPct - rangeStartPct),
89960
+ const constantSpeed = { ease: "none" };
89961
+ const mapped = keyframes.map((kf, index) => ({
89962
+ percentage: roundPct3(
89963
+ progressAtTime(
89964
+ runEase,
89965
+ startTimePct + kf.percentage / 100 * (endTimePct - startTimePct)
89966
+ )
89967
+ ),
89417
89968
  properties: kf.properties,
89418
- ...kf.ease ? { ease: kf.ease } : {}
89969
+ ...kf.ease ? { ease: kf.ease } : index > 0 ? constantSpeed : {}
89419
89970
  }));
89420
89971
  const merged = [...preserved, ...mapped].sort((a, b) => a.percentage - b.percentage);
89421
89972
  await commit2(
@@ -89425,7 +89976,8 @@ function useGestureCommit({
89425
89976
  targetSelector: selector,
89426
89977
  position: typeof existingPositionTween.position === "number" ? existingPositionTween.position : tweenStart,
89427
89978
  duration: tweenDur,
89428
- keyframes: merged
89979
+ keyframes: merged,
89980
+ ...keyframeEases(existingPositionTween)
89429
89981
  },
89430
89982
  { label: "Gesture recording (merge)", softReload: true, keyframeAction: "add" }
89431
89983
  );
@@ -89488,7 +90040,7 @@ function useGestureCommit({
89488
90040
  commitInFlightRef.current = false;
89489
90041
  }
89490
90042
  }, [gestureRecording, showToast, isGestureRecordingRef, domEditSessionRef]);
89491
- const armAutoStop = useCallback163(
90043
+ const armAutoStop = useCallback164(
89492
90044
  (elementEnd) => {
89493
90045
  clearInterval(recordingAutoStopRef.current);
89494
90046
  const autoStopAt = elementEnd ?? Infinity;
@@ -89502,7 +90054,7 @@ function useGestureCommit({
89502
90054
  },
89503
90055
  [stopAndCommitRecording]
89504
90056
  );
89505
- const startRecording = useCallback163(
90057
+ const startRecording = useCallback164(
89506
90058
  (method) => {
89507
90059
  if (readOnlyPreview) return;
89508
90060
  const sel = domEditSessionRef.current.domEditSelection;
@@ -89537,7 +90089,7 @@ function useGestureCommit({
89537
90089
  armAutoStop
89538
90090
  ]
89539
90091
  );
89540
- const handleToggleRecording = useCallback163(
90092
+ const handleToggleRecording = useCallback164(
89541
90093
  (method = "button") => {
89542
90094
  if (commitInFlightRef.current) return;
89543
90095
  if (gestureStateRef.current === "recording") {
@@ -89699,7 +90251,7 @@ function useGestureTrailOverlay({
89699
90251
  import { useMemo as useMemo88 } from "react";
89700
90252
 
89701
90253
  // src/components/StudioLeftPanels.tsx
89702
- import { memo as memo49, useCallback as useCallback171 } from "react";
90254
+ import { memo as memo49, useCallback as useCallback172 } from "react";
89703
90255
 
89704
90256
  // src/components/MediaPreview.tsx
89705
90257
  import { useState as useState132 } from "react";
@@ -89818,7 +90370,7 @@ function MediaPreview({ projectId, filePath }) {
89818
90370
  }
89819
90371
 
89820
90372
  // src/components/sidebar/AssetsTab.tsx
89821
- import { memo as memo46, useState as useState138, useCallback as useCallback167, useRef as useRef172, useMemo as useMemo84, useEffect as useEffect141 } from "react";
90373
+ import { memo as memo46, useState as useState138, useCallback as useCallback168, useRef as useRef172, useMemo as useMemo84, useEffect as useEffect141 } from "react";
89822
90374
 
89823
90375
  // src/components/ui/SearchInput.tsx
89824
90376
  import { jsx as jsx210, jsxs as jsxs167 } from "react/jsx-runtime";
@@ -89929,11 +90481,11 @@ var CATEGORY_LABELS = {
89929
90481
  var FILTER_ORDER = ["audio", "images", "video", "fonts"];
89930
90482
 
89931
90483
  // src/components/sidebar/AudioRow.tsx
89932
- import { useState as useState134, useRef as useRef170, useEffect as useEffect137, useCallback as useCallback165 } from "react";
90484
+ import { useState as useState134, useRef as useRef170, useEffect as useEffect137, useCallback as useCallback166 } from "react";
89933
90485
  import { classifyWebAudioMediaRoute } from "@hyperframes/core/runtime/web-audio-route";
89934
90486
 
89935
90487
  // src/components/sidebar/AssetContextMenu.tsx
89936
- import { useCallback as useCallback164, useEffect as useEffect136, useLayoutEffect as useLayoutEffect14, useRef as useRef169, useState as useState133 } from "react";
90488
+ import { useCallback as useCallback165, useEffect as useEffect136, useLayoutEffect as useLayoutEffect14, useRef as useRef169, useState as useState133 } from "react";
89937
90489
  import { Fragment as Fragment59, jsx as jsx211, jsxs as jsxs168 } from "react/jsx-runtime";
89938
90490
  function isValidAssetName(name) {
89939
90491
  return name.length > 0 && !/[/\\]/.test(name) && !name.includes("..");
@@ -89993,7 +90545,7 @@ function ContextMenu3({
89993
90545
  menuRef.current?.querySelector('[role="menuitem"]')?.focus();
89994
90546
  }
89995
90547
  }, [mode]);
89996
- const commitRename = useCallback164(() => {
90548
+ const commitRename = useCallback165(() => {
89997
90549
  const trimmed = renameDraft.trim();
89998
90550
  if (trimmed === filename(asset)) {
89999
90551
  onClose();
@@ -90209,7 +90761,7 @@ function AudioRow({
90209
90761
  const serveUrl = resolveMediaPreviewUrl(asset, projectId);
90210
90762
  const isCopied = copyFeedback?.path === asset && copyFeedback.ok;
90211
90763
  const copyFailed = copyFeedback?.path === asset && !copyFeedback.ok;
90212
- const stopPlayback = useCallback165(() => {
90764
+ const stopPlayback = useCallback166(() => {
90213
90765
  audioRef.current?.pause();
90214
90766
  setPlaying(false);
90215
90767
  cancelAnimationFrame(animRef.current);
@@ -90220,7 +90772,7 @@ function AudioRow({
90220
90772
  const elements = usePlayerStore((s) => s.elements);
90221
90773
  const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
90222
90774
  const clearPreviewAsset = useAssetPreviewStore((s) => s.clearPreviewAsset);
90223
- const activateRow = useCallback165(() => {
90775
+ const activateRow = useCallback166(() => {
90224
90776
  if (used) {
90225
90777
  const clip = findClipForAsset(elements, asset);
90226
90778
  if (clip) {
@@ -90242,10 +90794,10 @@ function AudioRow({
90242
90794
  setPreviewAsset,
90243
90795
  clearPreviewAsset
90244
90796
  ]);
90245
- const handlePointerDown = useCallback165((e) => {
90797
+ const handlePointerDown = useCallback166((e) => {
90246
90798
  pointerDownRef.current = { x: e.clientX, y: e.clientY };
90247
90799
  }, []);
90248
- const handlePointerUp = useCallback165(
90800
+ const handlePointerUp = useCallback166(
90249
90801
  (e) => {
90250
90802
  const origin2 = pointerDownRef.current;
90251
90803
  pointerDownRef.current = null;
@@ -90291,7 +90843,7 @@ function AudioRow({
90291
90843
  }
90292
90844
  return () => cancelAnimationFrame(animRef.current);
90293
90845
  }, [playing]);
90294
- const togglePlay = useCallback165(async () => {
90846
+ const togglePlay = useCallback166(async () => {
90295
90847
  if (playing) {
90296
90848
  stopPlayback();
90297
90849
  if (stopCurrentPreview === stopPlayback) stopCurrentPreview = null;
@@ -90489,7 +91041,7 @@ function GlobalAssetsView({ searchQuery }) {
90489
91041
  }
90490
91042
 
90491
91043
  // src/components/sidebar/AssetCard.tsx
90492
- import { useState as useState137, useEffect as useEffect140, useRef as useRef171, useCallback as useCallback166 } from "react";
91044
+ import { useState as useState137, useEffect as useEffect140, useRef as useRef171, useCallback as useCallback167 } from "react";
90493
91045
 
90494
91046
  // src/components/ui/VideoFrameThumbnail.tsx
90495
91047
  import { useState as useState136, useEffect as useEffect139 } from "react";
@@ -90636,10 +91188,10 @@ function AssetCard({
90636
91188
  const elements = usePlayerStore((s) => s.elements);
90637
91189
  const setPreviewAsset = useAssetPreviewStore((s) => s.setPreviewAsset);
90638
91190
  const clearPreviewAsset = useAssetPreviewStore((s) => s.clearPreviewAsset);
90639
- const handlePointerDown = useCallback166((e) => {
91191
+ const handlePointerDown = useCallback167((e) => {
90640
91192
  pointerDownRef.current = { x: e.clientX, y: e.clientY };
90641
91193
  }, []);
90642
- const activateCard = useCallback166(() => {
91194
+ const activateCard = useCallback167(() => {
90643
91195
  if (used) {
90644
91196
  const clip = findClipForAsset(elements, asset);
90645
91197
  if (clip) {
@@ -90661,7 +91213,7 @@ function AssetCard({
90661
91213
  setPreviewAsset,
90662
91214
  clearPreviewAsset
90663
91215
  ]);
90664
- const handlePointerUp = useCallback166(
91216
+ const handlePointerUp = useCallback167(
90665
91217
  (e) => {
90666
91218
  const origin2 = pointerDownRef.current;
90667
91219
  pointerDownRef.current = null;
@@ -90993,7 +91545,7 @@ var AssetsTab = memo46(function AssetsTab2({
90993
91545
  cancelled = true;
90994
91546
  };
90995
91547
  }, [projectId, assetsKey]);
90996
- const handleImport = useCallback167(
91548
+ const handleImport = useCallback168(
90997
91549
  async (files) => {
90998
91550
  if (!onImport) return;
90999
91551
  setImporting(true);
@@ -91005,7 +91557,7 @@ var AssetsTab = memo46(function AssetsTab2({
91005
91557
  },
91006
91558
  [onImport]
91007
91559
  );
91008
- const handleDrop = useCallback167(
91560
+ const handleDrop = useCallback168(
91009
91561
  (e) => {
91010
91562
  e.preventDefault();
91011
91563
  setDragOver(false);
@@ -91013,7 +91565,7 @@ var AssetsTab = memo46(function AssetsTab2({
91013
91565
  },
91014
91566
  [handleImport]
91015
91567
  );
91016
- const handleCopyPath = useCallback167(async (path) => {
91568
+ const handleCopyPath = useCallback168(async (path) => {
91017
91569
  const copied = await copyTextToClipboard(path);
91018
91570
  setCopyFeedback({ path, ok: copied });
91019
91571
  setTimeout(() => setCopyFeedback(null), copied ? 1500 : 3e3);
@@ -91223,11 +91775,11 @@ var AssetsTab = memo46(function AssetsTab2({
91223
91775
  });
91224
91776
 
91225
91777
  // src/components/sidebar/BlocksTab.tsx
91226
- import { memo as memo47, useState as useState140, useCallback as useCallback169, useRef as useRef174, useEffect as useEffect143 } from "react";
91778
+ import { memo as memo47, useState as useState140, useCallback as useCallback170, useRef as useRef174, useEffect as useEffect143 } from "react";
91227
91779
  import { createPortal as createPortal13 } from "react-dom";
91228
91780
 
91229
91781
  // src/components/sidebar/PromptPreviewModal.tsx
91230
- import { useState as useState139, useCallback as useCallback168, useEffect as useEffect142, useRef as useRef173 } from "react";
91782
+ import { useState as useState139, useCallback as useCallback169, useEffect as useEffect142, useRef as useRef173 } from "react";
91231
91783
  import { jsx as jsx217, jsxs as jsxs173 } from "react/jsx-runtime";
91232
91784
  function PromptPreviewModal({
91233
91785
  title,
@@ -91253,7 +91805,7 @@ function PromptPreviewModal({
91253
91805
  textareaRef.current?.focus();
91254
91806
  });
91255
91807
  }, []);
91256
- const handleCopy2 = useCallback168(async () => {
91808
+ const handleCopy2 = useCallback169(async () => {
91257
91809
  try {
91258
91810
  await navigator.clipboard.writeText(valueRef.current);
91259
91811
  setCopyState("copied");
@@ -91539,13 +92091,13 @@ function BlockCard({
91539
92091
  const hoverTimer = useRef174(null);
91540
92092
  const colors = getCategoryColors(category);
91541
92093
  const needsWebGL = tags?.includes("html-in-canvas") || tags?.includes("webgl");
91542
- const handleEnter = useCallback169(() => {
92094
+ const handleEnter = useCallback170(() => {
91543
92095
  hoverTimer.current = setTimeout(() => {
91544
92096
  setHovered(true);
91545
92097
  onPreview?.({ videoUrl, posterUrl, title });
91546
92098
  }, 300);
91547
92099
  }, [onPreview, videoUrl, posterUrl, title]);
91548
- const handleLeave = useCallback169(() => {
92100
+ const handleLeave = useCallback170(() => {
91549
92101
  if (hoverTimer.current) {
91550
92102
  clearTimeout(hoverTimer.current);
91551
92103
  hoverTimer.current = null;
@@ -91558,7 +92110,7 @@ function BlockCard({
91558
92110
  if (hoverTimer.current) clearTimeout(hoverTimer.current);
91559
92111
  };
91560
92112
  }, []);
91561
- const handleAdd = useCallback169(
92113
+ const handleAdd = useCallback170(
91562
92114
  async (e) => {
91563
92115
  e.stopPropagation();
91564
92116
  if (addState !== "idle" || !onAdd) return;
@@ -91574,7 +92126,7 @@ function BlockCard({
91574
92126
  [onAdd, addState]
91575
92127
  );
91576
92128
  const { activeCompPath, compositionDimensions } = useStudioShellContext();
91577
- const handleShowPrompt = useCallback169(
92129
+ const handleShowPrompt = useCallback170(
91578
92130
  (e) => {
91579
92131
  e.stopPropagation();
91580
92132
  const state = usePlayerStore.getState();
@@ -91714,7 +92266,7 @@ function BlockCard({
91714
92266
  }
91715
92267
 
91716
92268
  // src/components/sidebar/CompositionsTab.tsx
91717
- import { memo as memo48, useCallback as useCallback170, useEffect as useEffect144, useLayoutEffect as useLayoutEffect15, useMemo as useMemo85, useRef as useRef175, useState as useState141 } from "react";
92269
+ import { memo as memo48, useCallback as useCallback171, useEffect as useEffect144, useLayoutEffect as useLayoutEffect15, useMemo as useMemo85, useRef as useRef175, useState as useState141 } from "react";
91718
92270
  import { jsx as jsx219, jsxs as jsxs175 } from "react/jsx-runtime";
91719
92271
  var DEFAULT_PREVIEW_STAGE = { width: 1920, height: 1080 };
91720
92272
  var CARD_W = 80;
@@ -91825,7 +92377,7 @@ function CompCard({
91825
92377
  const hoverTimer = useRef175(null);
91826
92378
  const syncTimer = useRef175(null);
91827
92379
  const draggedRef = useRef175(false);
91828
- const requestIframePlaybackSync = useCallback170((shouldPlay) => {
92380
+ const requestIframePlaybackSync = useCallback171((shouldPlay) => {
91829
92381
  if (syncTimer.current) {
91830
92382
  clearTimeout(syncTimer.current);
91831
92383
  syncTimer.current = null;
@@ -92205,7 +92757,7 @@ var StudioLeftPanels = memo49(function StudioLeftPanels2({
92205
92757
  handleImportFiles,
92206
92758
  handleContentChange
92207
92759
  } = useFileManagerContext();
92208
- const handleRenderComposition = useCallback171(
92760
+ const handleRenderComposition = useCallback172(
92209
92761
  async (comp) => {
92210
92762
  if (renderQueue.ffmpegMissing) {
92211
92763
  useDockLayoutStore.getState().activatePanel("renders");
@@ -92315,13 +92867,28 @@ function CodeBody({
92315
92867
  }
92316
92868
 
92317
92869
  // src/components/StudioRightPanels.tsx
92318
- import { memo as memo59, useCallback as useCallback188 } from "react";
92870
+ import { memo as memo59, useCallback as useCallback189 } from "react";
92319
92871
 
92320
92872
  // src/components/editor/LayersPanel.tsx
92321
- import { memo as memo50, useState as useState143, useCallback as useCallback173, useEffect as useEffect145, useRef as useRef177 } from "react";
92873
+ import { memo as memo50, useState as useState143, useCallback as useCallback174, useEffect as useEffect145, useRef as useRef177 } from "react";
92874
+
92875
+ // src/components/editor/layerKind.ts
92876
+ function isCompositionHost(el) {
92877
+ return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
92878
+ }
92879
+ function layerKindOf(el, hasChildren) {
92880
+ const tag = el.tagName.toLowerCase();
92881
+ if (tag === "img" || tag === "picture") return "image";
92882
+ if (tag === "video") return "video";
92883
+ if (tag === "audio") return "audio";
92884
+ if (tag === "svg") return "vector";
92885
+ if (el.hasAttribute("data-hf-group") || isCompositionHost(el)) return "group";
92886
+ if (hasOnlyFormattingChildren(el) && el.textContent?.trim()) return "text";
92887
+ return hasChildren ? "group" : "shape";
92888
+ }
92322
92889
 
92323
92890
  // src/components/editor/useLayerDrag.ts
92324
- import { useRef as useRef176, useState as useState142, useCallback as useCallback172 } from "react";
92891
+ import { useRef as useRef176, useState as useState142, useCallback as useCallback173 } from "react";
92325
92892
  var DRAG_THRESHOLD_PX4 = 4;
92326
92893
  function isLayerDraggable(layer) {
92327
92894
  if (!(layer.selector || layer.id)) return false;
@@ -92391,7 +92958,7 @@ function useLayerDrag({
92391
92958
  const dragRef = useRef176(null);
92392
92959
  const [dragKey, setDragKey] = useState142(null);
92393
92960
  const [insertionLineY, setInsertionLineY] = useState142(null);
92394
- const handleRowPointerDown = useCallback172(
92961
+ const handleRowPointerDown = useCallback173(
92395
92962
  (layerIndex, e) => {
92396
92963
  if (e.button !== 0) return;
92397
92964
  const layer = visibleLayers[layerIndex];
@@ -92418,7 +92985,7 @@ function useLayerDrag({
92418
92985
  },
92419
92986
  [visibleLayers, scrollContainerRef, onSingleSibling]
92420
92987
  );
92421
- const handleContainerPointerMove = useCallback172(
92988
+ const handleContainerPointerMove = useCallback173(
92422
92989
  (e) => {
92423
92990
  const drag = dragRef.current;
92424
92991
  if (!drag) return;
@@ -92444,7 +93011,7 @@ function useLayerDrag({
92444
93011
  },
92445
93012
  [visibleLayers, scrollContainerRef]
92446
93013
  );
92447
- const handleContainerPointerUp = useCallback172(() => {
93014
+ const handleContainerPointerUp = useCallback173(() => {
92448
93015
  const drag = dragRef.current;
92449
93016
  dragRef.current = null;
92450
93017
  setDragKey(null);
@@ -92531,36 +93098,17 @@ function getVisibleLayers(layers, collapsed) {
92531
93098
  // src/components/editor/LayersPanel.tsx
92532
93099
  import { jsx as jsx223, jsxs as jsxs178 } from "react/jsx-runtime";
92533
93100
  var LAYERS_PANEL_MAX_ROWS = 80;
92534
- var TAG_ICONS = {
92535
- video: "Vi",
92536
- audio: "Au",
92537
- img: "Im",
92538
- svg: "Sv",
92539
- canvas: "Cn",
92540
- div: "Di",
92541
- section: "Se",
92542
- span: "Sp",
92543
- p: "P",
92544
- h1: "H1",
92545
- h2: "H2",
92546
- h3: "H3",
92547
- h4: "H4",
92548
- h5: "H5",
92549
- h6: "H6",
92550
- a: "A",
92551
- button: "Bt",
92552
- ul: "Ul",
92553
- ol: "Ol",
92554
- li: "Li",
92555
- style: "St",
92556
- template: "Te"
93101
+ var [CARET, ICON, GAP] = [16, 20, 6];
93102
+ var CHILD_INDENT = CARET + GAP + ICON + GAP;
93103
+ var KIND_ICON = {
93104
+ image: ImageIcon,
93105
+ video: Film,
93106
+ audio: Music,
93107
+ vector: Vector,
93108
+ group: Folder,
93109
+ text: Type,
93110
+ shape: Square
92557
93111
  };
92558
- function getTagBadge(tagName) {
92559
- return TAG_ICONS[tagName] ?? tagName.slice(0, 2).toUpperCase();
92560
- }
92561
- function isCompositionHost(el) {
92562
- return el.hasAttribute("data-composition-src") || el.hasAttribute("data-composition-file");
92563
- }
92564
93112
  function createRafThrottle(run, throttleMs = 100) {
92565
93113
  let rafId = null;
92566
93114
  let lastFired = 0;
@@ -92619,7 +93167,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92619
93167
  });
92620
93168
  const isMasterView = !activeCompPath || activeCompPath === "index.html";
92621
93169
  const livePreviewIframe = useLivePreviewIframe();
92622
- const collectLayers = useCallback173(() => {
93170
+ const collectLayers = useCallback174(() => {
92623
93171
  const iframe = previewIframeRef.current;
92624
93172
  if (!iframe) return;
92625
93173
  let doc = null;
@@ -92667,7 +93215,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92667
93215
  throttle.cancel();
92668
93216
  };
92669
93217
  }, [collectLayers]);
92670
- const resolveSelection = useCallback173(
93218
+ const resolveSelection = useCallback174(
92671
93219
  (layer) => {
92672
93220
  const doc = previewIframeRef.current?.contentDocument;
92673
93221
  const el = liveLayerElement(layer, doc, activeCompPath);
@@ -92680,7 +93228,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92680
93228
  },
92681
93229
  [activeCompPath, isMasterView, previewIframeRef, activeGroupElement]
92682
93230
  );
92683
- const seekToLayer = useCallback173(
93231
+ const seekToLayer = useCallback174(
92684
93232
  async (layer) => {
92685
93233
  const selection = await resolveSelection(layer);
92686
93234
  if (!selection) return;
@@ -92709,7 +93257,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92709
93257
  },
92710
93258
  [currentTime, resolveSelection, timelineElements]
92711
93259
  );
92712
- const handleSelectLayer = useCallback173(
93260
+ const handleSelectLayer = useCallback174(
92713
93261
  async (layer) => {
92714
93262
  const selection = await resolveSelection(layer);
92715
93263
  if (!selection) return;
@@ -92719,7 +93267,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92719
93267
  },
92720
93268
  [resolveSelection, applyDomSelection, seekToLayer, scheduleReveal]
92721
93269
  );
92722
- const handleLayerDoubleClick = useCallback173(
93270
+ const handleLayerDoubleClick = useCallback174(
92723
93271
  async (layer) => {
92724
93272
  const selection = await resolveSelection(layer);
92725
93273
  if (selection?.element.hasAttribute("data-hf-group")) {
@@ -92730,7 +93278,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92730
93278
  },
92731
93279
  [resolveSelection, setActiveGroupElement, handleSelectLayer]
92732
93280
  );
92733
- const handleLayerHover = useCallback173(
93281
+ const handleLayerHover = useCallback174(
92734
93282
  async (layer) => {
92735
93283
  if (!layer) {
92736
93284
  updateDomEditHoverSelection(null);
@@ -92741,11 +93289,11 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92741
93289
  },
92742
93290
  [resolveSelection, updateDomEditHoverSelection]
92743
93291
  );
92744
- const toggleCollapse = useCallback173((key2, e) => {
93292
+ const toggleCollapse = useCallback174((key2, e) => {
92745
93293
  e.stopPropagation();
92746
93294
  setCollapsed((prev) => ({ ...prev, [key2]: !prev[key2] }));
92747
93295
  }, []);
92748
- const handleReorder = useCallback173(
93296
+ const handleReorder = useCallback174(
92749
93297
  (event) => {
92750
93298
  const { siblingLayers, fromIndex, toIndex } = event;
92751
93299
  const reordered = [...siblingLayers];
@@ -92808,7 +93356,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92808
93356
  );
92809
93357
  const selectedKey = domEditSelection ? getDomEditLayerKey(domEditSelection) : null;
92810
93358
  const visibleLayers = getVisibleLayers(layers, collapsed);
92811
- const handleSingleSibling = useCallback173(() => {
93359
+ const handleSingleSibling = useCallback174(() => {
92812
93360
  showToast("Only one layer at this level", "info");
92813
93361
  }, [showToast]);
92814
93362
  const {
@@ -92855,7 +93403,7 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92855
93403
  {
92856
93404
  type: "button",
92857
93405
  onClick: () => setActiveGroupElement(null),
92858
- className: "flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] text-panel-text-3 hover:bg-panel-hover/40 hover:text-panel-text-1",
93406
+ className: "mx-1 flex w-[calc(100%-0.5rem)] items-center gap-1.5 rounded-md px-2 py-1 text-left text-[11px] text-panel-text-3 hover:bg-panel-hover/40 hover:text-panel-text-1",
92859
93407
  children: [
92860
93408
  /* @__PURE__ */ jsx223("span", { "aria-hidden": "true", children: "\u2190" }),
92861
93409
  /* @__PURE__ */ jsx223("span", { className: "truncate", children: activeGroupElement.getAttribute("data-hf-group") || "Group" })
@@ -92869,6 +93417,10 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92869
93417
  const isCollapsed = collapsed[layer.key] ?? false;
92870
93418
  const hasChildren = layer.childCount > 0;
92871
93419
  const isCompHost = isCompositionHost(layer.element);
93420
+ const kind = layerKindOf(layer.element, hasChildren);
93421
+ const KindIcon = KIND_ICON[kind];
93422
+ const kindName = kind[0].toUpperCase() + kind.slice(1);
93423
+ const hint = draggable ? layer.element.hasAttribute("data-hf-group") ? "Double-click to enter group" : void 0 : "This layer can't be reordered";
92872
93424
  return /* @__PURE__ */ jsxs178(
92873
93425
  "div",
92874
93426
  {
@@ -92885,9 +93437,9 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92885
93437
  handleSelectLayer(layer);
92886
93438
  }
92887
93439
  },
92888
- className: `group flex w-full items-center gap-1.5 px-2 py-1 text-left transition-colors ${isDragged ? "opacity-40" : selected ? "bg-panel-accent/14 text-accent-ink" : "text-panel-text-2 hover:bg-panel-hover/40 hover:text-panel-text-1"} ${dragKey ? "cursor-grabbing" : "cursor-pointer"}`,
92889
- style: { paddingLeft: 8 + layer.depth * 16 },
92890
- title: draggable ? layer.element.hasAttribute("data-hf-group") ? "Double-click to enter group" : void 0 : "This layer can't be reordered",
93440
+ className: `group mx-1 flex items-center gap-1.5 rounded-md px-2 py-1 text-left transition-colors ${isDragged ? "opacity-40" : selected ? "bg-panel-accent/14 text-accent-ink" : "text-panel-text-2 hover:bg-panel-hover/40 hover:text-panel-text-1"} ${dragKey ? "cursor-grabbing" : "cursor-pointer"}`,
93441
+ style: { paddingLeft: 8 + layer.depth * CHILD_INDENT },
93442
+ title: hint,
92891
93443
  children: [
92892
93444
  hasChildren ? /* @__PURE__ */ jsx223(
92893
93445
  "button",
@@ -92909,12 +93461,14 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92909
93461
  }
92910
93462
  )
92911
93463
  }
92912
- ) : /* @__PURE__ */ jsx223("span", { className: "w-4 shrink-0" }),
93464
+ ) : null,
92913
93465
  /* @__PURE__ */ jsx223(
92914
93466
  "span",
92915
93467
  {
92916
- className: `flex h-5 w-5 shrink-0 items-center justify-center rounded text-[8px] font-bold uppercase ${selected ? "bg-panel-accent/18 text-accent-ink" : isCompHost ? "bg-on text-accent-ink" : "bg-panel-hover text-panel-text-4"}`,
92917
- children: getTagBadge(layer.tagName)
93468
+ "data-layer-kind": kind,
93469
+ title: hint ? `${kindName} \xB7 ${hint}` : kindName,
93470
+ className: `flex h-5 w-5 shrink-0 items-center justify-center rounded ${selected ? "bg-panel-accent/18 text-accent-ink" : isCompHost ? "bg-on text-accent-ink" : "bg-panel-hover text-panel-text-4"}`,
93471
+ children: /* @__PURE__ */ jsx223(KindIcon, { size: 12 })
92918
93472
  }
92919
93473
  ),
92920
93474
  /* @__PURE__ */ jsx223("span", { className: "min-w-0 flex-1 truncate text-[11px]", children: layer.label }),
@@ -92940,10 +93494,10 @@ var LayersPanel = memo50(function LayersPanel2({ host }) {
92940
93494
  });
92941
93495
 
92942
93496
  // src/captions/components/CaptionPropertyPanel.tsx
92943
- import { memo as memo52, useCallback as useCallback175, useState as useState145 } from "react";
93497
+ import { memo as memo52, useCallback as useCallback176, useState as useState145 } from "react";
92944
93498
 
92945
93499
  // src/captions/components/CaptionAnimationPanel.tsx
92946
- import { memo as memo51, useCallback as useCallback174 } from "react";
93500
+ import { memo as memo51, useCallback as useCallback175 } from "react";
92947
93501
 
92948
93502
  // src/captions/components/shared.tsx
92949
93503
  import { useEffect as useEffect146, useRef as useRef178, useState as useState144 } from "react";
@@ -93168,25 +93722,25 @@ var CaptionAnimationPanel = memo51(function CaptionAnimationPanel2() {
93168
93722
  }
93169
93723
  const group = resolvedGroupId ? model?.groups.get(resolvedGroupId) : void 0;
93170
93724
  const animation = group?.animation;
93171
- const handleEntranceChange = useCallback174(
93725
+ const handleEntranceChange = useCallback175(
93172
93726
  (update) => {
93173
93727
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "entrance", update);
93174
93728
  },
93175
93729
  [resolvedGroupId, updateGroupAnimation]
93176
93730
  );
93177
- const handleHighlightChange = useCallback174(
93731
+ const handleHighlightChange = useCallback175(
93178
93732
  (update) => {
93179
93733
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "highlight", update);
93180
93734
  },
93181
93735
  [resolvedGroupId, updateGroupAnimation]
93182
93736
  );
93183
- const handleExitChange = useCallback174(
93737
+ const handleExitChange = useCallback175(
93184
93738
  (update) => {
93185
93739
  if (resolvedGroupId) updateGroupAnimation(resolvedGroupId, "exit", update);
93186
93740
  },
93187
93741
  [resolvedGroupId, updateGroupAnimation]
93188
93742
  );
93189
- const handleApplyToAll = useCallback174(() => {
93743
+ const handleApplyToAll = useCallback175(() => {
93190
93744
  if (animation) applyAnimationToAll(animation);
93191
93745
  }, [animation, applyAnimationToAll]);
93192
93746
  if (!group || !resolvedGroupId || !animation) {
@@ -93286,7 +93840,7 @@ var CaptionPropertyPanel = memo52(function CaptionPropertyPanel2({
93286
93840
  ...groupStyle,
93287
93841
  ...segmentOverrides
93288
93842
  };
93289
- const applyToIframeDom = useCallback175(
93843
+ const applyToIframeDom = useCallback176(
93290
93844
  (updates) => {
93291
93845
  const iframe = iframeRef.current;
93292
93846
  if (!iframe || !model) return;
@@ -93360,7 +93914,7 @@ var CaptionPropertyPanel = memo52(function CaptionPropertyPanel2({
93360
93914
  },
93361
93915
  [iframeRef, model, selectedSegmentIds]
93362
93916
  );
93363
- const handleStyleChange = useCallback175(
93917
+ const handleStyleChange = useCallback176(
93364
93918
  (updates) => {
93365
93919
  if (selectedGroupId) {
93366
93920
  updateGroupStyle(selectedGroupId, updates);
@@ -93465,7 +94019,7 @@ var CaptionPropertyPanel = memo52(function CaptionPropertyPanel2({
93465
94019
  });
93466
94020
 
93467
94021
  // src/components/editor/BlockParamsPanel.tsx
93468
- import { memo as memo53, useCallback as useCallback176, useEffect as useEffect147, useRef as useRef179, useState as useState146 } from "react";
94022
+ import { memo as memo53, useCallback as useCallback177, useEffect as useEffect147, useRef as useRef179, useState as useState146 } from "react";
93469
94023
  import { jsx as jsx227, jsxs as jsxs182 } from "react/jsx-runtime";
93470
94024
  var BlockParamsPanel = memo53(function BlockParamsPanel2({
93471
94025
  blockName,
@@ -93488,7 +94042,7 @@ var BlockParamsPanel = memo53(function BlockParamsPanel2({
93488
94042
  );
93489
94043
  const [commitState, setCommitState] = useState146({ tone: "idle" });
93490
94044
  const commitTimersRef = useRef179(/* @__PURE__ */ new Map());
93491
- const commitParamNow = useCallback176(
94045
+ const commitParamNow = useCallback177(
93492
94046
  async (key2, nextValue) => {
93493
94047
  if (!fileManager) return;
93494
94048
  const previous = appliedRef.current[key2];
@@ -93533,7 +94087,7 @@ var BlockParamsPanel = memo53(function BlockParamsPanel2({
93533
94087
  },
93534
94088
  [fileManager, compositionPath, setRefreshKey, blockName]
93535
94089
  );
93536
- const commitParam = useCallback176(
94090
+ const commitParam = useCallback177(
93537
94091
  (key2, nextValue) => fileManager ? serializeStudioFileMutation(
93538
94092
  fileManager.writeProjectFile,
93539
94093
  compositionPath,
@@ -93541,7 +94095,7 @@ var BlockParamsPanel = memo53(function BlockParamsPanel2({
93541
94095
  ) : Promise.resolve(),
93542
94096
  [commitParamNow, compositionPath, fileManager]
93543
94097
  );
93544
- const handleChange = useCallback176(
94098
+ const handleChange = useCallback177(
93545
94099
  (key2, value) => {
93546
94100
  setValues((prev) => ({ ...prev, [key2]: value }));
93547
94101
  const timers = commitTimersRef.current;
@@ -93704,7 +94258,7 @@ import { createPortal as createPortal14 } from "react-dom";
93704
94258
  import { CANVAS_DIMENSIONS } from "@hyperframes/parsers";
93705
94259
 
93706
94260
  // src/components/renders/RenderQueueItem.tsx
93707
- import { memo as memo54, useCallback as useCallback177, useState as useState147 } from "react";
94261
+ import { memo as memo54, useCallback as useCallback178, useState as useState147 } from "react";
93708
94262
  import { Fragment as Fragment64, jsx as jsx228, jsxs as jsxs183 } from "react/jsx-runtime";
93709
94263
  function formatDuration2(ms) {
93710
94264
  if (ms < 1e3) return `${ms}ms`;
@@ -93727,10 +94281,10 @@ var RenderQueueItem = memo54(function RenderQueueItem2({
93727
94281
  const [videoReady, setVideoReady] = useState147(false);
93728
94282
  const [confirmingDelete, setConfirmingDelete] = useState147(false);
93729
94283
  const fileSrc = buildProjectApiPath(projectId, `/renders/file/${job.filename}`);
93730
- const handleOpen = useCallback177(() => {
94284
+ const handleOpen = useCallback178(() => {
93731
94285
  window.open(fileSrc, "_blank");
93732
94286
  }, [fileSrc]);
93733
- const handleDownload = useCallback177(
94287
+ const handleDownload = useCallback178(
93734
94288
  (e) => {
93735
94289
  e.stopPropagation();
93736
94290
  const a = document.createElement("a");
@@ -94478,11 +95032,11 @@ var RenderQueuePanel = memo57(function RenderQueuePanel2() {
94478
95032
  });
94479
95033
 
94480
95034
  // src/components/panels/SlideshowPanel.tsx
94481
- import { useState as useState151, useEffect as useEffect150, useCallback as useCallback179, useRef as useRef182 } from "react";
95035
+ import { useState as useState151, useEffect as useEffect150, useCallback as useCallback180, useRef as useRef182 } from "react";
94482
95036
  import { parseSlideshowManifest } from "@hyperframes/core/slideshow";
94483
95037
 
94484
95038
  // src/components/panels/SlideshowSubPanels.tsx
94485
- import { useState as useState150, useCallback as useCallback178, useId as useId6 } from "react";
95039
+ import { useState as useState150, useCallback as useCallback179, useId as useId6 } from "react";
94486
95040
  import { jsx as jsx232, jsxs as jsxs186 } from "react/jsx-runtime";
94487
95041
  function SectionHeader({
94488
95042
  children,
@@ -94670,7 +95224,7 @@ function BranchTree({
94670
95224
  }) {
94671
95225
  const [newLabel, setNewLabel] = useState150("");
94672
95226
  const inputId = useId6();
94673
- const handleCreate = useCallback178(() => {
95227
+ const handleCreate = useCallback179(() => {
94674
95228
  const label = newLabel.trim();
94675
95229
  if (!label) return;
94676
95230
  onCreateSequence(label);
@@ -94732,7 +95286,7 @@ function BranchItem({
94732
95286
  const [editing, setEditing] = useState150(false);
94733
95287
  const [draft, setDraft] = useState150(seq.label);
94734
95288
  const [confirmingDelete, setConfirmingDelete] = useState150(false);
94735
- const commitRename = useCallback178(() => {
95289
+ const commitRename = useCallback179(() => {
94736
95290
  const label = draft.trim();
94737
95291
  if (label && label !== seq.label) onRename(seq.id, label);
94738
95292
  setEditing(false);
@@ -94865,7 +95419,7 @@ function HotspotTool({
94865
95419
  const selectedElementId = domEditSelection?.element?.id ?? null;
94866
95420
  const selectedHfId = domEditSelection?.hfId ?? null;
94867
95421
  const elementKey3 = selectedElementId || selectedHfId;
94868
- const handleMakeHotspot = useCallback178(() => {
95422
+ const handleMakeHotspot = useCallback179(() => {
94869
95423
  if (!selectedSceneId || !targetSequenceId || !elementKey3) return;
94870
95424
  const id = `hotspot-${elementKey3}-${generateId()}`;
94871
95425
  const label = hotspotLabel.trim() || elementKey3;
@@ -95190,7 +95744,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95190
95744
  setUndoDepth(0);
95191
95745
  setPersistError(false);
95192
95746
  }, [compHtml]);
95193
- const applyManifest = useCallback179(
95747
+ const applyManifest = useCallback180(
95194
95748
  async (next, opts) => {
95195
95749
  const merged = notesCtrlRef.current.mergeIntoDiscrete(next);
95196
95750
  if (!opts?.skipUndo) {
@@ -95210,14 +95764,14 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95210
95764
  },
95211
95765
  [onPersist]
95212
95766
  );
95213
- const handleUndo = useCallback179(() => {
95767
+ const handleUndo = useCallback180(() => {
95214
95768
  const prev = undoStackRef.current.pop();
95215
95769
  if (!prev) return;
95216
95770
  setUndoDepth(undoStackRef.current.length);
95217
95771
  applyManifest(prev, { skipUndo: true }).catch(() => {
95218
95772
  });
95219
95773
  }, [applyManifest]);
95220
- const handleRetryPersist = useCallback179(async () => {
95774
+ const handleRetryPersist = useCallback180(async () => {
95221
95775
  setRetrying(true);
95222
95776
  try {
95223
95777
  await onPersist(manifestRef.current);
@@ -95228,7 +95782,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95228
95782
  setRetrying(false);
95229
95783
  }
95230
95784
  }, [onPersist]);
95231
- const applyNotesManifest = useCallback179(
95785
+ const applyNotesManifest = useCallback180(
95232
95786
  (next) => {
95233
95787
  setManifest(next);
95234
95788
  manifestRef.current = next;
@@ -95242,7 +95796,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95242
95796
  ctrl.flush();
95243
95797
  };
95244
95798
  }, []);
95245
- const toggleSection = useCallback179((key2) => {
95799
+ const toggleSection = useCallback180((key2) => {
95246
95800
  setExpandedSections((prev) => {
95247
95801
  const next = new Set(prev);
95248
95802
  if (next.has(key2)) {
@@ -95256,25 +95810,25 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95256
95810
  const activeSlides = selectedSequenceId ? (manifest.slideSequences ?? []).find((s) => s.id === selectedSequenceId)?.slides ?? [] : manifest.slides;
95257
95811
  const selectedSlide = activeSlides.find((s) => s.sceneId === selectedSceneId);
95258
95812
  const sequences = manifest.slideSequences ?? [];
95259
- const handleSelectBranchSlide = useCallback179((sequenceId, sceneId) => {
95813
+ const handleSelectBranchSlide = useCallback180((sequenceId, sceneId) => {
95260
95814
  setSelectedSceneId(sceneId);
95261
95815
  setSelectedSequenceId(sequenceId);
95262
95816
  }, []);
95263
- const handleToggleSlide = useCallback179(
95817
+ const handleToggleSlide = useCallback180(
95264
95818
  (sceneId) => {
95265
95819
  applyManifest(toggleMainLineSlide(manifestRef.current, sceneId)).catch(() => {
95266
95820
  });
95267
95821
  },
95268
95822
  [applyManifest]
95269
95823
  );
95270
- const handleReorder = useCallback179(
95824
+ const handleReorder = useCallback180(
95271
95825
  (sceneId, dir) => {
95272
95826
  applyManifest(reorderMainLineSlide(manifestRef.current, sceneId, dir)).catch(() => {
95273
95827
  });
95274
95828
  },
95275
95829
  [applyManifest]
95276
95830
  );
95277
- const handleSetNotes = useCallback179(
95831
+ const handleSetNotes = useCallback180(
95278
95832
  (notes) => {
95279
95833
  if (!selectedSceneId) return;
95280
95834
  applyNotesManifest(
@@ -95283,7 +95837,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95283
95837
  },
95284
95838
  [selectedSceneId, selectedSequenceId, applyNotesManifest]
95285
95839
  );
95286
- const handleMarkFragment = useCallback179(() => {
95840
+ const handleMarkFragment = useCallback180(() => {
95287
95841
  if (!selectedSceneId) return;
95288
95842
  applyManifest(
95289
95843
  addFragment(
@@ -95295,7 +95849,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95295
95849
  ).catch(() => {
95296
95850
  });
95297
95851
  }, [selectedSceneId, selectedSequenceId, currentTime, applyManifest]);
95298
- const handleRemoveFragment = useCallback179(
95852
+ const handleRemoveFragment = useCallback180(
95299
95853
  (time) => {
95300
95854
  if (!selectedSceneId) return;
95301
95855
  applyManifest(
@@ -95305,7 +95859,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95305
95859
  },
95306
95860
  [selectedSceneId, selectedSequenceId, applyManifest]
95307
95861
  );
95308
- const handleCreateSequence = useCallback179(
95862
+ const handleCreateSequence = useCallback180(
95309
95863
  (label) => {
95310
95864
  const id = `seq-${generateId()}`;
95311
95865
  applyManifest(createSequence(manifestRef.current, id, label)).catch(() => {
@@ -95313,21 +95867,21 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95313
95867
  },
95314
95868
  [applyManifest]
95315
95869
  );
95316
- const handleRenameSequence = useCallback179(
95870
+ const handleRenameSequence = useCallback180(
95317
95871
  (id, label) => {
95318
95872
  applyManifest(renameSequence(manifestRef.current, id, label)).catch(() => {
95319
95873
  });
95320
95874
  },
95321
95875
  [applyManifest]
95322
95876
  );
95323
- const handleDeleteSequence = useCallback179(
95877
+ const handleDeleteSequence = useCallback180(
95324
95878
  (id) => {
95325
95879
  applyManifest(deleteSequence(manifestRef.current, id)).catch(() => {
95326
95880
  });
95327
95881
  },
95328
95882
  [applyManifest]
95329
95883
  );
95330
- const handleAssign = useCallback179(
95884
+ const handleAssign = useCallback180(
95331
95885
  (sequenceId, sceneId, assign) => {
95332
95886
  applyManifest(assignToBranch(manifestRef.current, sequenceId, sceneId, assign)).catch(
95333
95887
  () => {
@@ -95336,7 +95890,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95336
95890
  },
95337
95891
  [applyManifest]
95338
95892
  );
95339
- const handleAddHotspot = useCallback179(
95893
+ const handleAddHotspot = useCallback180(
95340
95894
  (sceneId, hotspot) => {
95341
95895
  applyManifest(
95342
95896
  addHotspot(manifestRef.current, sceneId, hotspot, selectedSequenceId ?? void 0)
@@ -95345,7 +95899,7 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95345
95899
  },
95346
95900
  [selectedSequenceId, applyManifest]
95347
95901
  );
95348
- const handleRemoveHotspot = useCallback179(
95902
+ const handleRemoveHotspot = useCallback180(
95349
95903
  (sceneId, hotspotId) => {
95350
95904
  applyManifest(
95351
95905
  removeHotspot(manifestRef.current, sceneId, hotspotId, selectedSequenceId ?? void 0)
@@ -95495,10 +96049,10 @@ function SlideshowPanel({ scenes, onPersist, onPersistNotes }) {
95495
96049
  }
95496
96050
 
95497
96051
  // src/components/panels/VariablesPanel.tsx
95498
- import { memo as memo58, useCallback as useCallback183, useEffect as useEffect152, useMemo as useMemo86, useState as useState155 } from "react";
96052
+ import { memo as memo58, useCallback as useCallback184, useEffect as useEffect152, useMemo as useMemo86, useState as useState155 } from "react";
95499
96053
 
95500
96054
  // src/hooks/useVariablesPersist.ts
95501
- import { useCallback as useCallback180 } from "react";
96055
+ import { useCallback as useCallback181 } from "react";
95502
96056
  function useVariablesPersist({
95503
96057
  sdkSession,
95504
96058
  activeCompPath,
@@ -95508,7 +96062,7 @@ function useVariablesPersist({
95508
96062
  reloadPreview,
95509
96063
  publishSdkSession
95510
96064
  }) {
95511
- return useCallback180(
96065
+ return useCallback181(
95512
96066
  async (label, mutate) => {
95513
96067
  if (!sdkSession) return false;
95514
96068
  const path = activeCompPath ?? "index.html";
@@ -95543,10 +96097,10 @@ function useVariablesPersist({
95543
96097
  }
95544
96098
 
95545
96099
  // src/components/panels/VariablesOtherCompositions.tsx
95546
- import { useCallback as useCallback182, useState as useState154 } from "react";
96100
+ import { useCallback as useCallback183, useState as useState154 } from "react";
95547
96101
 
95548
96102
  // src/hooks/useProjectCompositionVariables.ts
95549
- import { useCallback as useCallback181, useEffect as useEffect151, useState as useState152 } from "react";
96103
+ import { useCallback as useCallback182, useEffect as useEffect151, useState as useState152 } from "react";
95550
96104
  import { openComposition as openComposition4 } from "@hyperframes/sdk";
95551
96105
  async function readGroup(path, readProjectFile) {
95552
96106
  let content;
@@ -95589,7 +96143,7 @@ function useProjectCompositionVariables(compositionPaths, excludePath, readProje
95589
96143
  }
95590
96144
  function useEditVariablesInFile(deps) {
95591
96145
  const { readProjectFile, writeProjectFile, recordEdit, reloadPreview } = deps;
95592
- return useCallback181(
96146
+ return useCallback182(
95593
96147
  async (path, label, mutate) => {
95594
96148
  const originalContent = await readProjectFile(path);
95595
96149
  await persistSdkSerialize(
@@ -95966,7 +96520,7 @@ function VariablesOtherCompositions({
95966
96520
  reloadPreview
95967
96521
  });
95968
96522
  const [editingKey, setEditingKey] = useState154(null);
95969
- const onSave = useCallback182(
96523
+ const onSave = useCallback183(
95970
96524
  (path, decl) => {
95971
96525
  setEditingKey(null);
95972
96526
  void editInFile(
@@ -95977,7 +96531,7 @@ function VariablesOtherCompositions({
95977
96531
  },
95978
96532
  [editInFile]
95979
96533
  );
95980
- const onRemove = useCallback182(
96534
+ const onRemove = useCallback183(
95981
96535
  (path, id) => {
95982
96536
  void editInFile(
95983
96537
  path,
@@ -96211,7 +96765,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96211
96765
  // eslint-disable-next-line react-hooks/exhaustive-deps
96212
96766
  [sdkSession, previewValues, refreshKey, revision]
96213
96767
  );
96214
- const copyToClipboard = useCallback183(
96768
+ const copyToClipboard = useCallback184(
96215
96769
  (text, what) => {
96216
96770
  void copyTextToClipboard(text).then(
96217
96771
  (ok) => showToast(
@@ -96222,7 +96776,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96222
96776
  },
96223
96777
  [showToast]
96224
96778
  );
96225
- const dropPreviewOverride = useCallback183(
96779
+ const dropPreviewOverride = useCallback184(
96226
96780
  (id) => {
96227
96781
  if (previewValues && id in previewValues) {
96228
96782
  const next = { ...previewValues };
@@ -96232,7 +96786,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96232
96786
  },
96233
96787
  [previewValues, setPreviewValues]
96234
96788
  );
96235
- const commitPreviewValue = useCallback183(
96789
+ const commitPreviewValue = useCallback184(
96236
96790
  (id, value, declDefault) => {
96237
96791
  const next = { ...previewValues ?? {} };
96238
96792
  if (JSON.stringify(value) === JSON.stringify(declDefault)) {
@@ -96245,7 +96799,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96245
96799
  },
96246
96800
  [previewValues, setPreviewValues, reloadPreview]
96247
96801
  );
96248
- const runSchemaEdit = useCallback183(
96802
+ const runSchemaEdit = useCallback184(
96249
96803
  async (label, mutate) => {
96250
96804
  try {
96251
96805
  const changed = await persistVariables(label, mutate);
@@ -96259,7 +96813,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96259
96813
  },
96260
96814
  [persistVariables, showToast]
96261
96815
  );
96262
- const handleAdd = useCallback183(
96816
+ const handleAdd = useCallback184(
96263
96817
  (decl) => {
96264
96818
  if (!sdkSession) return;
96265
96819
  const check = sdkSession.can({ type: "declareVariable", declaration: decl });
@@ -96272,7 +96826,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96272
96826
  },
96273
96827
  [sdkSession, runSchemaEdit, showToast]
96274
96828
  );
96275
- const handleUpdate = useCallback183(
96829
+ const handleUpdate = useCallback184(
96276
96830
  (decl) => {
96277
96831
  if (!sdkSession) return;
96278
96832
  const check = sdkSession.can({
@@ -96292,7 +96846,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96292
96846
  },
96293
96847
  [sdkSession, runSchemaEdit, showToast]
96294
96848
  );
96295
- const handleRemove = useCallback183(
96849
+ const handleRemove = useCallback184(
96296
96850
  (id) => {
96297
96851
  if (!sdkSession) return;
96298
96852
  const check = sdkSession.can({ type: "removeVariableDeclaration", id });
@@ -96308,18 +96862,18 @@ var VariablesPanel = memo58(function VariablesPanel2({
96308
96862
  },
96309
96863
  [sdkSession, runSchemaEdit, dropPreviewOverride, showToast]
96310
96864
  );
96311
- const handleSetDefault = useCallback183(
96865
+ const handleSetDefault = useCallback184(
96312
96866
  (id, value) => {
96313
96867
  void runSchemaEdit(`Set default for "${id}"`, (s) => s.setVariableValue(id, value));
96314
96868
  dropPreviewOverride(id);
96315
96869
  },
96316
96870
  [runSchemaEdit, dropPreviewOverride]
96317
96871
  );
96318
- const resetPreview = useCallback183(() => {
96872
+ const resetPreview = useCallback184(() => {
96319
96873
  setPreviewValues(null);
96320
96874
  reloadPreview();
96321
96875
  }, [setPreviewValues, reloadPreview]);
96322
- const handleBind = useCallback183(
96876
+ const handleBind = useCallback184(
96323
96877
  // Guard chain (session, selection, type-compat) — one branch per guard.
96324
96878
  // fallow-ignore-next-line complexity
96325
96879
  (action, id) => {
@@ -96431,7 +96985,7 @@ var VariablesPanel = memo58(function VariablesPanel2({
96431
96985
  });
96432
96986
 
96433
96987
  // src/hooks/useSlideshowPersist.ts
96434
- import { useCallback as useCallback184 } from "react";
96988
+ import { useCallback as useCallback185 } from "react";
96435
96989
 
96436
96990
  // src/utils/setSlideshowManifest.ts
96437
96991
  import {
@@ -96488,7 +97042,7 @@ function useSlideshowPersist({
96488
97042
  publishSdkSession,
96489
97043
  coalesceKey
96490
97044
  }) {
96491
- return useCallback184(
97045
+ return useCallback185(
96492
97046
  async (manifest) => {
96493
97047
  if (!sdkSession) return;
96494
97048
  const path = activeCompPath ?? "index.html";
@@ -96546,7 +97100,7 @@ function useSlideshowTabState(params) {
96546
97100
  }
96547
97101
 
96548
97102
  // src/components/DesignPanelPromoteProvider.tsx
96549
- import { useCallback as useCallback185 } from "react";
97103
+ import { useCallback as useCallback186 } from "react";
96550
97104
  import { jsx as jsx238 } from "react/jsx-runtime";
96551
97105
  function DesignPanelPromoteProvider({
96552
97106
  selection,
@@ -96565,7 +97119,7 @@ function DesignPanelPromoteProvider({
96565
97119
  publishSdkSession: handle.publish,
96566
97120
  activeCompPath: targetPath
96567
97121
  });
96568
- const persist2 = useCallback185(
97122
+ const persist2 = useCallback186(
96569
97123
  async (label, mutate) => {
96570
97124
  const committed = await rawPersist(label, mutate);
96571
97125
  if (committed) forceReloadSharedSdkSession?.();
@@ -96573,7 +97127,7 @@ function DesignPanelPromoteProvider({
96573
97127
  },
96574
97128
  [rawPersist, forceReloadSharedSdkSession]
96575
97129
  );
96576
- const handlePersistError = useCallback185(
97130
+ const handlePersistError = useCallback186(
96577
97131
  (error) => showToast(getStudioSaveErrorMessage(error), "error"),
96578
97132
  [showToast]
96579
97133
  );
@@ -96590,7 +97144,7 @@ function DesignPanelPromoteProvider({
96590
97144
  }
96591
97145
 
96592
97146
  // src/hooks/useRemoveBackground.ts
96593
- import { useCallback as useCallback186, useEffect as useEffect153, useRef as useRef183 } from "react";
97147
+ import { useCallback as useCallback187, useEffect as useEffect153, useRef as useRef183 } from "react";
96594
97148
 
96595
97149
  // src/components/studioMediaJobs.ts
96596
97150
  var MEDIA_JOB_RECONNECT_TIMEOUT_MS = 15e3;
@@ -96703,7 +97257,7 @@ function useRemoveBackground(projectId, refreshFileTree, showToast) {
96703
97257
  },
96704
97258
  []
96705
97259
  );
96706
- return useCallback186(
97260
+ return useCallback187(
96707
97261
  // fallow-ignore-next-line complexity
96708
97262
  async (inputPath, options) => {
96709
97263
  const response = await studioApiFetch(
@@ -96742,7 +97296,7 @@ function useRemoveBackground(projectId, refreshFileTree, showToast) {
96742
97296
  }
96743
97297
 
96744
97298
  // src/hooks/useApplyColorGradingScope.ts
96745
- import { useCallback as useCallback187 } from "react";
97299
+ import { useCallback as useCallback188 } from "react";
96746
97300
 
96747
97301
  // src/components/editor/colorGradingScopePatch.ts
96748
97302
  var MEDIA_TAG_RE = /<\s*(video|img)\b(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
@@ -96846,7 +97400,7 @@ function useApplyColorGradingScope(recordEdit, reloadPreview) {
96846
97400
  const { projectId, activeCompPath, showToast, waitForPendingDomEditSaves } = useStudioShellContext();
96847
97401
  const { domEditSelection } = useDomEditContext();
96848
97402
  const { readProjectFile, writeProjectFile, compositions } = useFileManagerContext();
96849
- return useCallback187(
97403
+ return useCallback188(
96850
97404
  async (scope, value) => applyColorGradingScopeUpdate({
96851
97405
  scope,
96852
97406
  value,
@@ -96994,11 +97548,11 @@ var StudioRightPanels = memo59(function StudioRightPanels2({
96994
97548
  useCaptionDesignFocus(captionEditMode);
96995
97549
  const handleApplyColorGradingScope = useApplyColorGradingScope(recordEdit, reloadPreview);
96996
97550
  const handleRemoveBackground = useRemoveBackground(projectId, refreshFileTree, showToast);
96997
- const setAttributeWhileDragging = useCallback188(
97551
+ const setAttributeWhileDragging = useCallback189(
96998
97552
  (attr, value) => handleDomAttributeLiveCommit(attr, value, void 0, { previewOnly: true }),
96999
97553
  [handleDomAttributeLiveCommit]
97000
97554
  );
97001
- const handleHideAllSelected = useCallback188(() => {
97555
+ const handleHideAllSelected = useCallback189(() => {
97002
97556
  if (!canHideSelections(domEditGroupSelections)) {
97003
97557
  showToast("Audio can't be hidden \u2014 use the group's own controls", "info");
97004
97558
  return;
@@ -97007,7 +97561,7 @@ var StudioRightPanels = memo59(function StudioRightPanels2({
97007
97561
  const keys = timelineKeysForSelections(domEditGroupSelections, elements, activeCompPath);
97008
97562
  if (keys.length > 0) void onToggleElementHidden?.(keys, true);
97009
97563
  }, [domEditGroupSelections, showToast, activeCompPath, onToggleElementHidden]);
97010
- const convertToKeyframes = useCallback188(
97564
+ const convertToKeyframes = useCallback189(
97011
97565
  (animId, duration) => handleGsapConvertToKeyframes(animId, void 0, duration),
97012
97566
  [handleGsapConvertToKeyframes]
97013
97567
  );
@@ -97163,340 +97717,6 @@ import {
97163
97717
  MagnifyingGlassPlus
97164
97718
  } from "@phosphor-icons/react";
97165
97719
 
97166
- // src/hooks/useEnableKeyframes.ts
97167
- import { useCallback as useCallback189 } from "react";
97168
- var enableKeyframesTransactionCounter = 0;
97169
- function animatedProps(anim) {
97170
- if (!anim) return ["x", "y"];
97171
- const own = Object.keys(anim.properties ?? {});
97172
- if (own.length > 0) return own;
97173
- const stops = anim.keyframes?.keyframes;
97174
- if (stops?.length) {
97175
- const keys = /* @__PURE__ */ new Set();
97176
- for (const stop2 of stops) for (const k of Object.keys(stop2.properties ?? {})) keys.add(k);
97177
- if (keys.size > 0) return [...keys];
97178
- }
97179
- return ["x", "y"];
97180
- }
97181
- function isPlayheadWithinTween(anim, currentTime) {
97182
- const start = resolveTweenStart(anim);
97183
- if (start === null) return true;
97184
- return isTimeWithinTween(currentTime, start, resolveTweenDuration(anim));
97185
- }
97186
- async function replaceSetWithSingleKeyframe(session, sel, setAnim, t2, iframe, selector) {
97187
- const position = readElementPosition(iframe, sel, setAnim);
97188
- if (Object.keys(position).length === 0) {
97189
- for (const [key2, held] of Object.entries(setAnim.properties ?? {})) {
97190
- if (typeof held === "number") position[key2] = held;
97191
- }
97192
- }
97193
- if (Object.keys(position).length === 0 || !session.commitMutation) return;
97194
- const range = resolveNewTweenRange(sel.dataAttributes?.start, sel.dataAttributes?.duration, t2);
97195
- await session.commitMutation(
97196
- {
97197
- type: "replace-with-keyframes",
97198
- animationId: setAnim.id,
97199
- targetSelector: selector,
97200
- position: roundTo3(range.start),
97201
- duration: roundTo3(range.duration),
97202
- keyframes: [{ percentage: 0, properties: position }],
97203
- ease: setAnim.ease
97204
- },
97205
- { label: "Enable keyframes", keyframeAction: "add", softReload: true }
97206
- );
97207
- }
97208
- function readElementPosition(iframe, sel, anim) {
97209
- const result = {};
97210
- if (!iframe?.contentWindow) return result;
97211
- let gsap3;
97212
- try {
97213
- gsap3 = iframe.contentWindow.gsap;
97214
- } catch {
97215
- return result;
97216
- }
97217
- const element = sel.element;
97218
- if (!element?.isConnected || !gsap3?.getProperty) return result;
97219
- const props = animatedProps(anim);
97220
- for (const prop of props) {
97221
- const val = Number(gsap3.getProperty(element, prop));
97222
- if (!Number.isFinite(val)) continue;
97223
- result[prop] = POSITION_PROPS.has(prop) ? Math.round(val) : roundTo3(val);
97224
- }
97225
- return result;
97226
- }
97227
- function resolveNewTweenRange(authoredStart, authoredDuration, currentTime) {
97228
- const t2 = Math.max(0, roundTo3(currentTime));
97229
- const start = authoredStart != null ? Number.parseFloat(authoredStart) : Number.NaN;
97230
- const duration = authoredDuration != null ? Number.parseFloat(authoredDuration) : Number.NaN;
97231
- if (!Number.isFinite(start) || !Number.isFinite(duration) || duration <= 0) {
97232
- return { start: t2, duration: 1 };
97233
- }
97234
- const end = start + duration;
97235
- const clampedStart = Math.min(Math.max(t2, start), end);
97236
- return { start: clampedStart, duration: Math.max(0.5, roundTo3(end - clampedStart)) };
97237
- }
97238
- async function tryFetchAnimationsForElement(sel) {
97239
- const projectId = window.location.hash.match(/project\/([^?/]+)/)?.[1];
97240
- if (!projectId) return null;
97241
- const sourceFile = sel.sourceFile || "index.html";
97242
- const parsed = await fetchParsedAnimations(projectId, sourceFile);
97243
- if (!parsed) return null;
97244
- return getAnimationsForElement(parsed.animations, {
97245
- id: sel.id,
97246
- selector: sel.selector
97247
- });
97248
- }
97249
- async function fetchAnimationsForElement(sel) {
97250
- return await tryFetchAnimationsForElement(sel) ?? [];
97251
- }
97252
- async function extendKeyframedTweenToPlayhead(session, sel, anim, currentTime, duration, iframe, commitOverrides) {
97253
- const selector = existingTweenTargetSelector(anim, sel);
97254
- const position = readElementPosition(iframe, sel, anim);
97255
- if (!selector || Object.keys(position).length === 0 || !session.commitMutation) return;
97256
- const extended = buildExtendedKeyframes(anim, currentTime, position, duration);
97257
- await session.commitMutation(
97258
- {
97259
- type: "replace-with-keyframes",
97260
- animationId: anim.id,
97261
- targetSelector: selector,
97262
- position: extended.position,
97263
- duration: extended.duration,
97264
- keyframes: extended.keyframes,
97265
- ...keyframeEases(anim)
97266
- },
97267
- {
97268
- label: "Add keyframe",
97269
- keyframeAction: "add",
97270
- softReload: true,
97271
- ...commitOverrides
97272
- }
97273
- );
97274
- }
97275
- async function applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe, commitOverrides, liveTween) {
97276
- const keepAuthoredDuration = kfAnim.durationUnresolved === true;
97277
- if (keepAuthoredDuration) {
97278
- kfAnim = withLiveTiming(kfAnim, liveTween ?? findParsedTween(iframe, sel.element, kfAnim));
97279
- }
97280
- if (keepAuthoredDuration && kfAnim.duration == null) return;
97281
- const duration = resolveTweenDuration(kfAnim);
97282
- const start = resolveTweenStart(kfAnim);
97283
- if (start !== null && !isTimeWithinTween(t2, start, duration)) {
97284
- if (keepAuthoredDuration) return;
97285
- await extendKeyframedTweenToPlayhead(
97286
- session,
97287
- sel,
97288
- kfAnim,
97289
- t2,
97290
- duration,
97291
- iframe,
97292
- commitOverrides
97293
- );
97294
- return;
97295
- }
97296
- const pct = start === null ? computeElementPercentage(t2, sel) : absoluteToPercentageForAnimation(t2, kfAnim) ?? 0;
97297
- const existing = kfAnim.keyframes?.keyframes.find((k) => playsNear(kfAnim, k.percentage, pct));
97298
- if (existing) {
97299
- session.handleGsapRemoveKeyframe(kfAnim.id, existing.percentage);
97300
- return;
97301
- }
97302
- if (session.handleGsapAddKeyframeBatch) {
97303
- const position = readElementPosition(iframe, sel, kfAnim);
97304
- if (Object.keys(position).length > 0) {
97305
- await session.handleGsapAddKeyframeBatch(kfAnim.id, pct, position, commitOverrides);
97306
- }
97307
- }
97308
- }
97309
- async function promoteSetToKeyframes(session, sel, setAnim, t2, iframe) {
97310
- const selector = existingTweenTargetSelector(setAnim, sel);
97311
- const setStart = resolveTweenStart(setAnim) ?? 0;
97312
- if (!selector || !session.commitMutation) return;
97313
- if (t2 <= setStart) {
97314
- await replaceSetWithSingleKeyframe(session, sel, setAnim, t2, iframe, selector);
97315
- return;
97316
- }
97317
- const endPosition = readElementPosition(iframe, sel, setAnim);
97318
- if (Object.keys(endPosition).length === 0) return;
97319
- const startPosition = {};
97320
- for (const key2 of Object.keys(endPosition)) {
97321
- const held = setAnim.properties?.[key2];
97322
- if (typeof held === "number") startPosition[key2] = held;
97323
- }
97324
- await session.commitMutation(
97325
- {
97326
- type: "replace-with-keyframes",
97327
- animationId: setAnim.id,
97328
- targetSelector: selector,
97329
- position: roundTo3(setStart),
97330
- duration: roundTo3(t2 - setStart),
97331
- keyframes: [
97332
- {
97333
- percentage: 0,
97334
- properties: Object.keys(startPosition).length > 0 ? startPosition : endPosition,
97335
- auto: true
97336
- },
97337
- { percentage: 100, properties: endPosition }
97338
- ],
97339
- ease: setAnim.ease
97340
- },
97341
- { label: "Add keyframe", keyframeAction: "add", softReload: true }
97342
- );
97343
- }
97344
- async function applyArcKeyframeAtPlayhead(session, sel, arcAnim, t2, iframe) {
97345
- if (!session.commitMutation) return;
97346
- const targetSelector = existingTweenTargetSelector(arcAnim, sel);
97347
- if (!targetSelector) return;
97348
- const start = resolveTweenStart(arcAnim) ?? 0;
97349
- const duration = resolveTweenDuration(arcAnim);
97350
- if (!isTimeWithinTween(t2, start, duration)) {
97351
- const held = readElementPosition(iframe, sel, arcAnim);
97352
- if (t2 <= start || typeof held.x !== "number" || typeof held.y !== "number") return;
97353
- const extended = buildExtendedKeyframes(arcAnim, t2, { x: held.x, y: held.y }, duration, "none");
97354
- await session.commitMutation(
97355
- {
97356
- type: "replace-with-keyframes",
97357
- animationId: arcAnim.id,
97358
- targetSelector,
97359
- position: extended.position,
97360
- duration: extended.duration,
97361
- keyframes: extended.keyframes,
97362
- ease: "none"
97363
- },
97364
- { label: "Add keyframe", keyframeAction: "add", softReload: true }
97365
- );
97366
- return;
97367
- }
97368
- const nodes = linearArcKeyframes(arcAnim);
97369
- const playheadPercentage = absoluteToPercentage(t2, start, duration);
97370
- const timedNodeIndex = nodes.findIndex(
97371
- (node) => Math.abs(node.percentage - playheadPercentage) <= KEYFRAME_PCT_MATCH
97372
- );
97373
- if (timedNodeIndex !== -1) {
97374
- if (timedNodeIndex > 0 && timedNodeIndex < nodes.length - 1) {
97375
- await session.commitMutation(
97376
- {
97377
- type: "replace-with-keyframes",
97378
- animationId: arcAnim.id,
97379
- targetSelector,
97380
- position: roundTo3(start),
97381
- duration: roundTo3(duration),
97382
- keyframes: nodes.filter((_, index) => index !== timedNodeIndex),
97383
- ease: "none"
97384
- },
97385
- { label: "Remove keyframe", softReload: true }
97386
- );
97387
- }
97388
- return;
97389
- }
97390
- const live = readElementPosition(iframe, sel, arcAnim);
97391
- if (typeof live.x !== "number" || typeof live.y !== "number") return;
97392
- await session.commitMutation(
97393
- {
97394
- type: "replace-with-keyframes",
97395
- animationId: arcAnim.id,
97396
- targetSelector,
97397
- position: roundTo3(start),
97398
- duration: roundTo3(duration),
97399
- keyframes: buildTemporalArcKeyframes(arcAnim, playheadPercentage, {
97400
- x: live.x,
97401
- y: live.y
97402
- }),
97403
- ease: "none"
97404
- },
97405
- { label: "Add keyframe", keyframeAction: "add", softReload: true }
97406
- );
97407
- }
97408
- function useEnableKeyframes(sessionRef) {
97409
- return useCallback189(async () => {
97410
- const session = sessionRef.current;
97411
- if (!session) return;
97412
- const sel = session.domEditSelection;
97413
- if (!sel) return;
97414
- const t2 = usePlayerStore.getState().currentTime;
97415
- const iframe = session.previewIframeRef?.current ?? null;
97416
- const fetched = await tryFetchAnimationsForElement(sel);
97417
- const anims = fetched ?? session.selectedGsapAnimations;
97418
- const arcAnim = anims.find((a) => a.arcPath);
97419
- const kfAnim = anims.find((a) => a.keyframes && !a.arcPath);
97420
- const setAnim = anims.find((a) => isInstantHold(a) && !a.keyframes && !a.arcPath);
97421
- const flatAnim = anims.find((a) => !a.keyframes && !a.arcPath && !isInstantHold(a));
97422
- if (arcAnim) {
97423
- await applyArcKeyframeAtPlayhead(session, sel, arcAnim, t2, iframe);
97424
- } else if (kfAnim) {
97425
- await applyKeyframeAtPlayhead(session, sel, kfAnim, t2, iframe);
97426
- } else if (setAnim) {
97427
- await promoteSetToKeyframes(session, sel, setAnim, t2, iframe);
97428
- } else if (flatAnim) {
97429
- const flatLive = flatAnim.durationUnresolved ? findParsedTween(iframe, sel.element, flatAnim) : void 0;
97430
- enableKeyframesTransactionCounter += 1;
97431
- const coalesceKey = `enable-keyframes:${flatAnim.id}:${enableKeyframesTransactionCounter}`;
97432
- const convertCommitOverrides = {
97433
- keyframeTelemetry: false,
97434
- skipReload: true,
97435
- coalesceKey,
97436
- coalesceMs: Number.POSITIVE_INFINITY
97437
- };
97438
- await session.handleGsapConvertToKeyframes(
97439
- flatAnim.id,
97440
- void 0,
97441
- void 0,
97442
- convertCommitOverrides
97443
- );
97444
- const converted = (await fetchAnimationsForElement(sel)).find((a) => a.keyframes);
97445
- if (converted) {
97446
- const applyCommitOverrides = {
97447
- softReload: true,
97448
- coalesceKey,
97449
- // Must match the convert phase's window: editHistory keys coalescing
97450
- // off the incoming entry, so without Infinity here the apply (landing
97451
- // after two POSTs + a fetch) falls back to the 300ms default and the
97452
- // conversion splits into two undo entries under real latency.
97453
- coalesceMs: Number.POSITIVE_INFINITY
97454
- };
97455
- await applyKeyframeAtPlayhead(
97456
- session,
97457
- sel,
97458
- converted,
97459
- t2,
97460
- iframe,
97461
- applyCommitOverrides,
97462
- flatLive
97463
- );
97464
- }
97465
- } else {
97466
- const position = readElementPosition(iframe, sel, null);
97467
- const { start: elStart, duration: elDuration } = resolveNewTweenRange(
97468
- sel.dataAttributes?.start,
97469
- sel.dataAttributes?.duration,
97470
- t2
97471
- );
97472
- const selector = writeTargetSelector(sel);
97473
- if (!selector) {
97474
- session.handleGsapAddAnimation("to");
97475
- return;
97476
- }
97477
- if (Object.keys(position).length === 0) {
97478
- position.x = 0;
97479
- position.y = 0;
97480
- }
97481
- const keyframes = [{ percentage: 0, properties: { ...position } }];
97482
- if (session.commitMutation) {
97483
- await session.commitMutation(
97484
- {
97485
- type: "add-with-keyframes",
97486
- targetSelector: selector,
97487
- position: roundTo3(elStart),
97488
- duration: roundTo3(elDuration),
97489
- keyframes
97490
- },
97491
- { label: "Enable keyframes", softReload: true }
97492
- );
97493
- } else {
97494
- session.handleGsapAddAnimation("to");
97495
- }
97496
- }
97497
- }, [sessionRef]);
97498
- }
97499
-
97500
97720
  // src/hooks/useKeyframeKeyboard.ts
97501
97721
  import { useEffect as useEffect154, useCallback as useCallback190 } from "react";
97502
97722
  function useKeyframeKeyboard({ enabled, onAddKeyframe }) {