hyperframes 0.8.99 → 0.8.101

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 (84) hide show
  1. package/dist/{add-GHUB7AVH.js → add-M5KBSDPJ.js} +5 -5
  2. package/dist/{asset-MRT34PQQ.js → asset-2NJUOCPI.js} +2 -2
  3. package/dist/{auth-BBWBKAZF.js → auth-423RKRS6.js} +1 -1
  4. package/dist/{autoUpdate-BMLRSJPE.js → autoUpdate-HCAC6VDI.js} +2 -2
  5. package/dist/{backgroundChecks-I5JE6HZK.js → backgroundChecks-JIDPHRJ2.js} +3 -3
  6. package/dist/backgroundChecksWorker.js +3 -3
  7. package/dist/{beats-F2YQUUDN.js → beats-R6V7WZG4.js} +4 -4
  8. package/dist/{benchmark-RVBTP5TB.js → benchmark-3VGFAD46.js} +5 -5
  9. package/dist/{browser-247ZM3UP.js → browser-5SLUGL46.js} +3 -3
  10. package/dist/{catalog-NFO3XKI2.js → catalog-PNNGLZDM.js} +5 -5
  11. package/dist/{check-S4WORRJY.js → check-YPOAHNJC.js} +9 -9
  12. package/dist/{checkBrowser-UMS7OM7G.js → checkBrowser-TNHECLLW.js} +7 -7
  13. package/dist/{chunk-R6KCZGDI.js → chunk-CU67AYDW.js} +1 -1
  14. package/dist/{chunk-MDADXVOE.js → chunk-DQPLITE6.js} +2 -2
  15. package/dist/{chunk-T6OPV3KB.js → chunk-DXA3NT4O.js} +1 -1
  16. package/dist/{chunk-IJ677ALU.js → chunk-HT4EXFB7.js} +2 -2
  17. package/dist/{chunk-EUSLU4BH.js → chunk-KT72ISZW.js} +1 -1
  18. package/dist/{chunk-NDRJHYG6.js → chunk-QH3XIWBC.js} +2 -2
  19. package/dist/{chunk-LBZ54I42.js → chunk-QKRI7R7S.js} +1 -1
  20. package/dist/{chunk-XMENEJZ5.js → chunk-RCXGMESW.js} +2 -2
  21. package/dist/{chunk-7D2QWSXG.js → chunk-VAX7B75N.js} +1 -1
  22. package/dist/{chunk-5LLSHC7N.js → chunk-VOHOVOIF.js} +2 -2
  23. package/dist/{chunk-XFVS7YWL.js → chunk-WBAU2ZER.js} +1 -1
  24. package/dist/{chunk-MM7KX3QX.js → chunk-WV5HXMVM.js} +2 -2
  25. package/dist/{chunk-CHCKIZRG.js → chunk-YBDVWFXZ.js} +2 -2
  26. package/dist/{chunk-J5ES3BN2.js → chunk-YIVWMI2I.js} +1 -1
  27. package/dist/{chunk-FTPQUXVG.js → chunk-Z5E4I37A.js} +2 -2
  28. package/dist/{clean-65SGSRD3.js → clean-G7NQCBM3.js} +3 -3
  29. package/dist/cli.js +44 -44
  30. package/dist/{cloud-CBUJO3BE.js → cloud-52R7HCW5.js} +4 -4
  31. package/dist/{compare-55Y5B73R.js → compare-CBQXDLWD.js} +4 -4
  32. package/dist/{component-QXUJVQCV.js → component-7ALRLLD5.js} +3 -3
  33. package/dist/{compositions-ZU4LJ2VE.js → compositions-JDEN5OQG.js} +5 -5
  34. package/dist/{delete-YCFTXLIQ.js → delete-J4SAQYES.js} +2 -2
  35. package/dist/{doctor-R4MX64N7.js → doctor-FMO7BK6A.js} +3 -3
  36. package/dist/{events-AMCPKS2O.js → events-7K4NY4PI.js} +2 -2
  37. package/dist/{events-G2M67K5X.js → events-TUTMBJQH.js} +3 -3
  38. package/dist/{feedback-7PDQW7BY.js → feedback-5DZLRPOS.js} +4 -4
  39. package/dist/{figma-V3CJKPEP.js → figma-SGQGEZ2E.js} +3 -3
  40. package/dist/fontLocalizeCli.js +1 -1
  41. package/dist/{get-SVFBRH24.js → get-XM6A2IAO.js} +2 -2
  42. package/dist/{grade-compare-KDJ6L775.js → grade-compare-EX2DWA6T.js} +4 -4
  43. package/dist/{help-EW2PIPB5.js → help-BYZET5LF.js} +1 -1
  44. package/dist/{history-V7FE6H7U.js → history-KDMR56PG.js} +5 -5
  45. package/dist/hyperframes-player.global.js +1 -1
  46. package/dist/{info-U32TSQMU.js → info-DWER3M4R.js} +5 -5
  47. package/dist/{init-3S4Y6J5G.js → init-52NPEJD4.js} +6 -6
  48. package/dist/{inspect-WWJPKRGL.js → inspect-D2SO6CR5.js} +6 -6
  49. package/dist/{keyframes-Z4R6UFAQ.js → keyframes-TYQUMARP.js} +5 -5
  50. package/dist/{layout-LXW2H5JT.js → layout-WPTFL7PL.js} +6 -6
  51. package/dist/{lint-OK2DN3VX.js → lint-Y7XKAY6I.js} +6 -6
  52. package/dist/{list-DTY2BOKI.js → list-5IUCPJ5F.js} +2 -2
  53. package/dist/{login-AEKQ24BU.js → login-TYGLKBKQ.js} +4 -4
  54. package/dist/{media-treatment-FQQAY66F.js → media-treatment-R22MNQKD.js} +5 -5
  55. package/dist/{normalize-audio-KWWA6IIN.js → normalize-audio-VPDMEDFY.js} +4 -4
  56. package/dist/{play-HWFIDZWY.js → play-PYPQ5BX5.js} +4 -4
  57. package/dist/{present-EBMTSB5D.js → present-EGIEW2QS.js} +4 -4
  58. package/dist/{preview-HAWENKYG.js → preview-MOKQFMZS.js} +5 -5
  59. package/dist/{render-Q4NHGHBP.js → render-3DDDZIRS.js} +5 -5
  60. package/dist/{render-TDTMQSYX.js → render-3UAZC5BG.js} +5 -5
  61. package/dist/{skills-FKUY5J3V.js → skills-KJATAPZG.js} +5 -5
  62. package/dist/{skillsUpdateCheck-VRGY5IRQ.js → skillsUpdateCheck-XSVUFHU4.js} +3 -3
  63. package/dist/{snapshot-OSW3KFRL.js → snapshot-WFYVYTAY.js} +4 -4
  64. package/dist/studio/assets/{hyperframes-player-Rml34FRW.js → hyperframes-player-CFJLrHRF.js} +1 -1
  65. package/dist/studio/assets/{index-CVDArg3m.js → index-BBNVnnyH.js} +179 -179
  66. package/dist/studio/assets/{index-D9YzDuBx.js → index-C33EDBL2.js} +1 -1
  67. package/dist/studio/assets/{index-C_fi-jTe.js → index-CJOzbslX.js} +1 -1
  68. package/dist/studio/{chunk-U3EE4ACX.js → chunk-7X4XRP3L.js} +14 -13
  69. package/dist/studio/chunk-7X4XRP3L.js.map +1 -0
  70. package/dist/studio/index.d.ts +91 -21
  71. package/dist/studio/index.html +1 -1
  72. package/dist/studio/index.js +826 -538
  73. package/dist/studio/index.js.map +1 -1
  74. package/dist/studio/player.js +1 -1
  75. package/dist/{studioServer-YGYE5AZF.js → studioServer-TX22I3X6.js} +4 -4
  76. package/dist/{telemetry-SW2VUF5A.js → telemetry-ICRURYFF.js} +3 -3
  77. package/dist/{timeline-KXD4SKGX.js → timeline-4BYN4LEL.js} +5 -5
  78. package/dist/{tokens-TVDW7X7Q.js → tokens-YKDAFFM6.js} +2 -2
  79. package/dist/{transcribe-WNVFCNC5.js → transcribe-QRDNNU7W.js} +3 -3
  80. package/dist/{updateCheck-DAF2B2QZ.js → updateCheck-BQ3MSJWJ.js} +2 -2
  81. package/dist/{upgrade-ZGJPNQOO.js → upgrade-3AGGX5TW.js} +2 -2
  82. package/dist/{validate-AY5MWLFK.js → validate-LHZX5HXO.js} +6 -6
  83. package/package.json +1 -1
  84. package/dist/studio/chunk-U3EE4ACX.js.map +0 -1
@@ -43,6 +43,7 @@ import {
43
43
  isTimelineIgnoredElement,
44
44
  liveTime,
45
45
  panelsInZone,
46
+ parseDockLayout,
46
47
  playbackStartAttributeForElement,
47
48
  previewElementFinder,
48
49
  readLayerRevealPriorZ,
@@ -66,7 +67,7 @@ import {
66
67
  whenPreviewBooted,
67
68
  wrapTimeline,
68
69
  writeStudioUiPreferences
69
- } from "./chunk-U3EE4ACX.js";
70
+ } from "./chunk-7X4XRP3L.js";
70
71
  import {
71
72
  Button,
72
73
  ContextMenu,
@@ -3338,6 +3339,73 @@ function isLaneFree(elements, track2, start, end, excludeKey) {
3338
3339
  );
3339
3340
  }
3340
3341
 
3342
+ // src/player/components/timelineSnapping.ts
3343
+ function resolveSnapGuide(moving, trimming) {
3344
+ const source = moving?.started ? moving : trimming;
3345
+ if (source?.snapTime == null || source.snapType == null) return null;
3346
+ return { time: source.snapTime, type: source.snapType };
3347
+ }
3348
+ var TIMELINE_SNAP_PX = 8;
3349
+ var TYPE_PRIORITY = {
3350
+ playhead: 0,
3351
+ "clip-edge": 1,
3352
+ beat: 2
3353
+ };
3354
+ function collectTimelineSnapTargets(input) {
3355
+ const byTime = /* @__PURE__ */ new Map();
3356
+ const add = (time, type) => {
3357
+ if (!Number.isFinite(time) || time < 0) return;
3358
+ const rounded2 = Math.round(time * 1e3) / 1e3;
3359
+ const existing = byTime.get(rounded2);
3360
+ if (!existing || TYPE_PRIORITY[type] < TYPE_PRIORITY[existing.type]) {
3361
+ byTime.set(rounded2, { time: rounded2, type });
3362
+ }
3363
+ };
3364
+ for (const beat of input.beatTimes) add(beat, "beat");
3365
+ for (const el of input.elements) {
3366
+ if (input.excludeElementKey != null && (el.key ?? el.id) === input.excludeElementKey) continue;
3367
+ add(el.start, "clip-edge");
3368
+ add(el.start + el.duration, "clip-edge");
3369
+ }
3370
+ if (input.playheadTime != null && input.includePlayhead !== false)
3371
+ add(input.playheadTime, "playhead");
3372
+ return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
3373
+ }
3374
+ function snapTimelineTime(time, targets, thresholdSecs) {
3375
+ let best = null;
3376
+ let bestDist = thresholdSecs;
3377
+ for (const target of targets) {
3378
+ const d = Math.abs(target.time - time);
3379
+ if (d < bestDist || d === bestDist && best && TYPE_PRIORITY[target.type] < TYPE_PRIORITY[best.type]) {
3380
+ bestDist = d;
3381
+ best = target;
3382
+ }
3383
+ }
3384
+ return best ? { time: best.time, target: best } : { time, target: null };
3385
+ }
3386
+ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
3387
+ if (targets.length === 0) return { start, snapTime: null, snapType: null };
3388
+ const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3389
+ const startSnap = snapTimelineTime(start, targets, thresholdSecs);
3390
+ const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs);
3391
+ const startMoved = startSnap.target !== null;
3392
+ const endMoved = endSnap.target !== null;
3393
+ let candidate = start;
3394
+ let target = null;
3395
+ if (startMoved && (!endMoved || Math.abs(startSnap.time - start) <= Math.abs(endSnap.time - (start + duration)))) {
3396
+ candidate = startSnap.time;
3397
+ target = startSnap.target;
3398
+ } else if (endMoved) {
3399
+ candidate = endSnap.time - duration;
3400
+ target = endSnap.target;
3401
+ }
3402
+ const maxStart = Math.max(0, timelineDuration2 - duration);
3403
+ const roundedCandidate = Math.round(candidate * 1e3) / 1e3;
3404
+ const clamped = Math.max(0, Math.min(maxStart, roundedCandidate));
3405
+ if (target && Math.abs(clamped - roundedCandidate) > 1e-6) target = null;
3406
+ return { start: clamped, snapTime: target?.time ?? null, snapType: target?.type ?? null };
3407
+ }
3408
+
3341
3409
  // src/player/components/timelineDragDrop.ts
3342
3410
  function applyJsonDropPayload(raw, pick, apply, placement) {
3343
3411
  try {
@@ -3358,12 +3426,20 @@ function resolveDropInsertRow(contentY, rowHeights, trackCount) {
3358
3426
  if (rowFloat >= trackCount) return null;
3359
3427
  return resolveInsertRow(rowFloat, trackCount);
3360
3428
  }
3361
- function placeDrop(geometry, clientX, clientY, canInsertTrack) {
3429
+ function alignDropStart(placement, elements, pixelsPerSecond) {
3430
+ const laneHasClips = placement.insertRow == null && elements.some((el) => el.track === placement.track);
3431
+ if (!laneHasClips) return { ...placement, start: 0 };
3432
+ if (!usePlayerStore.getState().timelineSnapEnabled) return placement;
3433
+ const targets = collectTimelineSnapTargets({ elements, playheadTime: null, beatTimes: [] });
3434
+ const threshold = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3435
+ return { ...placement, start: snapTimelineTime(placement.start, targets, threshold).time };
3436
+ }
3437
+ function placeDrop(geometry, elements, clientX, clientY, canInsertTrack) {
3362
3438
  const placement = resolveTimelineAssetDrop(geometry, clientX, clientY);
3363
- if (!canInsertTrack) return placement;
3364
3439
  const contentY = clientY - geometry.rectTop + geometry.scrollTop;
3365
- const insertRow = resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length);
3366
- return insertRow == null ? placement : { ...placement, insertRow, trackOrder: geometry.trackOrder };
3440
+ const insertRow = canInsertTrack ? resolveDropInsertRow(contentY, geometry.rowHeights, geometry.trackOrder.length) : null;
3441
+ const aimed = insertRow == null ? placement : { ...placement, insertRow, trackOrder: geometry.trackOrder };
3442
+ return alignDropStart(aimed, elements, geometry.pixelsPerSecond);
3367
3443
  }
3368
3444
  function invokeDropCallback(callback) {
3369
3445
  try {
@@ -3391,6 +3467,7 @@ function useTimelineAssetDrop({
3391
3467
  scrollRef,
3392
3468
  ppsRef,
3393
3469
  trackOrderRef,
3470
+ elementsRef,
3394
3471
  rowGeometryRef,
3395
3472
  contentOrigin,
3396
3473
  onFileDrop,
@@ -3458,12 +3535,13 @@ function useTimelineAssetDrop({
3458
3535
  rowHeights: rowGeometryRef.current.rowHeights,
3459
3536
  trackOrder: trackOrderRef.current
3460
3537
  },
3538
+ elementsRef.current,
3461
3539
  clientX,
3462
3540
  clientY,
3463
3541
  canInsertTrack
3464
3542
  );
3465
3543
  },
3466
- [scrollRef, ppsRef, trackOrderRef, rowGeometryRef, contentOrigin]
3544
+ [scrollRef, ppsRef, trackOrderRef, elementsRef, rowGeometryRef, contentOrigin]
3467
3545
  );
3468
3546
  const handleAssetDragOver = useCallback6(
3469
3547
  (e) => {
@@ -3558,73 +3636,6 @@ function useTimelineAssetDrop({
3558
3636
  // src/player/components/useTimelineClipDrag.ts
3559
3637
  import { useRef as useRef7, useState as useState4, useCallback as useCallback7, useMemo as useMemo3, useEffect as useEffect7 } from "react";
3560
3638
 
3561
- // src/player/components/timelineSnapping.ts
3562
- function resolveSnapGuide(moving, trimming) {
3563
- const source = moving?.started ? moving : trimming;
3564
- if (source?.snapTime == null || source.snapType == null) return null;
3565
- return { time: source.snapTime, type: source.snapType };
3566
- }
3567
- var TIMELINE_SNAP_PX = 8;
3568
- var TYPE_PRIORITY = {
3569
- playhead: 0,
3570
- "clip-edge": 1,
3571
- beat: 2
3572
- };
3573
- function collectTimelineSnapTargets(input) {
3574
- const byTime = /* @__PURE__ */ new Map();
3575
- const add = (time, type) => {
3576
- if (!Number.isFinite(time) || time < 0) return;
3577
- const rounded2 = Math.round(time * 1e3) / 1e3;
3578
- const existing = byTime.get(rounded2);
3579
- if (!existing || TYPE_PRIORITY[type] < TYPE_PRIORITY[existing.type]) {
3580
- byTime.set(rounded2, { time: rounded2, type });
3581
- }
3582
- };
3583
- for (const beat of input.beatTimes) add(beat, "beat");
3584
- for (const el of input.elements) {
3585
- if (input.excludeElementKey != null && (el.key ?? el.id) === input.excludeElementKey) continue;
3586
- add(el.start, "clip-edge");
3587
- add(el.start + el.duration, "clip-edge");
3588
- }
3589
- if (input.playheadTime != null && input.includePlayhead !== false)
3590
- add(input.playheadTime, "playhead");
3591
- return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
3592
- }
3593
- function snapTimelineTime(time, targets, thresholdSecs) {
3594
- let best = null;
3595
- let bestDist = thresholdSecs;
3596
- for (const target of targets) {
3597
- const d = Math.abs(target.time - time);
3598
- if (d < bestDist || d === bestDist && best && TYPE_PRIORITY[target.type] < TYPE_PRIORITY[best.type]) {
3599
- bestDist = d;
3600
- best = target;
3601
- }
3602
- }
3603
- return best ? { time: best.time, target: best } : { time, target: null };
3604
- }
3605
- function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
3606
- if (targets.length === 0) return { start, snapTime: null, snapType: null };
3607
- const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3608
- const startSnap = snapTimelineTime(start, targets, thresholdSecs);
3609
- const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs);
3610
- const startMoved = startSnap.target !== null;
3611
- const endMoved = endSnap.target !== null;
3612
- let candidate = start;
3613
- let target = null;
3614
- if (startMoved && (!endMoved || Math.abs(startSnap.time - start) <= Math.abs(endSnap.time - (start + duration)))) {
3615
- candidate = startSnap.time;
3616
- target = startSnap.target;
3617
- } else if (endMoved) {
3618
- candidate = endSnap.time - duration;
3619
- target = endSnap.target;
3620
- }
3621
- const maxStart = Math.max(0, timelineDuration2 - duration);
3622
- const roundedCandidate = Math.round(candidate * 1e3) / 1e3;
3623
- const clamped = Math.max(0, Math.min(maxStart, roundedCandidate));
3624
- if (target && Math.abs(clamped - roundedCandidate) > 1e-6) target = null;
3625
- return { start: clamped, snapTime: target?.time ?? null, snapType: target?.type ?? null };
3626
- }
3627
-
3628
3639
  // src/player/components/timelineAuthoredMoveTarget.ts
3629
3640
  function canMoveTimelineElement(element) {
3630
3641
  return getTimelineEditCapabilities({
@@ -13033,6 +13044,7 @@ function useTimelineProviderState({
13033
13044
  scrollRef,
13034
13045
  ppsRef,
13035
13046
  trackOrderRef,
13047
+ elementsRef: timelineElementsRef,
13036
13048
  rowGeometryRef,
13037
13049
  contentOrigin,
13038
13050
  onFileDrop: pinnedOnFileDrop,
@@ -32383,6 +32395,334 @@ function computeDraggedGsapPosition(element, studioOffset, fallbackBase) {
32383
32395
  };
32384
32396
  }
32385
32397
 
32398
+ // src/hooks/gsapRuntimeKeyframes.ts
32399
+ import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
32400
+ var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
32401
+ "ease",
32402
+ "duration",
32403
+ "delay",
32404
+ "stagger",
32405
+ "motionPath",
32406
+ "overwrite",
32407
+ "immediateRender",
32408
+ "onComplete",
32409
+ "onUpdate",
32410
+ "onStart",
32411
+ "keyframes"
32412
+ ]);
32413
+ function timelinesOf(iframe) {
32414
+ if (!iframe?.contentWindow) return null;
32415
+ try {
32416
+ return iframe.contentWindow.__timelines ?? null;
32417
+ } catch {
32418
+ return null;
32419
+ }
32420
+ }
32421
+ function isXY(p) {
32422
+ return !!p && typeof p.x === "number" && typeof p.y === "number";
32423
+ }
32424
+ function isZeroDurationSet(duration) {
32425
+ return !(duration > 0);
32426
+ }
32427
+ function coordsFromMotionPath(mp) {
32428
+ if (!mp || typeof mp !== "object") return null;
32429
+ const obj = mp;
32430
+ const pathVal = Array.isArray(mp) ? mp : obj.path;
32431
+ if (!Array.isArray(pathVal)) return null;
32432
+ const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
32433
+ if (coords.length < 2) return null;
32434
+ const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
32435
+ const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
32436
+ return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
32437
+ }
32438
+ function flatTweenKeyframes(vars) {
32439
+ const properties = {};
32440
+ for (const [k, v] of Object.entries(vars)) {
32441
+ if (FLAT_SKIP_KEYS.has(k)) continue;
32442
+ if (typeof v === "number") properties[k] = roundTo3(v);
32443
+ else if (typeof v === "string") properties[k] = v;
32444
+ }
32445
+ if (Object.keys(properties).length === 0) return null;
32446
+ return [
32447
+ { percentage: 0, properties },
32448
+ { percentage: 100, properties }
32449
+ ];
32450
+ }
32451
+ function readTween(vars) {
32452
+ if (vars.keyframes && typeof vars.keyframes === "object") {
32453
+ const parsed = parsePercentageKeyframes(vars.keyframes);
32454
+ if (parsed) return parsed;
32455
+ }
32456
+ const mp = coordsFromMotionPath(vars.motionPath);
32457
+ if (mp) {
32458
+ const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
32459
+ if (shape) {
32460
+ const n = shape.waypoints.length;
32461
+ const keyframes = shape.waypoints.map((wp, i) => ({
32462
+ percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
32463
+ properties: { x: wp.x, y: wp.y }
32464
+ }));
32465
+ return { keyframes, arcPath: shape.arcPath };
32466
+ }
32467
+ }
32468
+ const flat = flatTweenKeyframes(vars);
32469
+ return flat ? { keyframes: flat } : null;
32470
+ }
32471
+ function matchesElement(tween, el) {
32472
+ if (!tween.targets) return false;
32473
+ for (const t of tween.targets()) {
32474
+ if (t === el || el.id && t.id === el.id) return true;
32475
+ }
32476
+ return false;
32477
+ }
32478
+ function tweenTiming(tween) {
32479
+ const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
32480
+ const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
32481
+ return {
32482
+ start: Number.isFinite(rawStart) ? rawStart : 0,
32483
+ duration: Number.isFinite(rawDur) ? rawDur : 0
32484
+ };
32485
+ }
32486
+ function varsCarryChannel(vars, channels) {
32487
+ if (!vars) return false;
32488
+ for (const ch of channels) {
32489
+ if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
32490
+ }
32491
+ return false;
32492
+ }
32493
+ function keyframeVarsCarryChannel(vars, channels) {
32494
+ const kf = vars?.keyframes;
32495
+ if (!kf || typeof kf !== "object") return false;
32496
+ if (!Array.isArray(kf) && channels.some((ch) => Object.prototype.hasOwnProperty.call(kf, ch)))
32497
+ return true;
32498
+ const steps = Array.isArray(kf) ? kf : Object.values(kf);
32499
+ return steps.some(
32500
+ (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
32501
+ );
32502
+ }
32503
+ function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
32504
+ const timelines = timelinesOf(iframe);
32505
+ if (!timelines) return null;
32506
+ let targetEl = null;
32507
+ try {
32508
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32509
+ } catch {
32510
+ return null;
32511
+ }
32512
+ if (!targetEl) return null;
32513
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
32514
+ const wantChannels = channels && channels.length > 0 ? channels : null;
32515
+ let first = null;
32516
+ let channelMatch = null;
32517
+ for (const tlId of tlIds) {
32518
+ const timeline = timelines[tlId];
32519
+ if (!timeline?.getChildren) continue;
32520
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
32521
+ for (const tween of timeline.getChildren(true)) {
32522
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32523
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32524
+ const isSet = !(dur > 0);
32525
+ if (kind === "set" ? !isSet : isSet) continue;
32526
+ if (wantChannels) {
32527
+ const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
32528
+ if (carries) {
32529
+ if (channelMatch === null) channelMatch = { tween, timeline };
32530
+ } else if (first === null) {
32531
+ first = { tween, timeline };
32532
+ }
32533
+ continue;
32534
+ }
32535
+ if (first === null) first = { tween, timeline };
32536
+ if (kind === "keyframe" && now2 != null) {
32537
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
32538
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
32539
+ }
32540
+ }
32541
+ }
32542
+ return channelMatch ?? first;
32543
+ }
32544
+ function readCarriesChannel(read, channels) {
32545
+ return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
32546
+ }
32547
+ function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
32548
+ const timelines = timelinesOf(iframe);
32549
+ if (!timelines) return null;
32550
+ let targetEl = null;
32551
+ try {
32552
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32553
+ } catch {
32554
+ return null;
32555
+ }
32556
+ if (!targetEl) return null;
32557
+ const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
32558
+ if (tlIds.length === 0) return null;
32559
+ let firstRead = null;
32560
+ for (const tlId of tlIds) {
32561
+ const timeline = timelines[tlId];
32562
+ if (!timeline?.getChildren) continue;
32563
+ const now2 = typeof timeline.time === "function" ? timeline.time() : null;
32564
+ for (const tween of timeline.getChildren(true)) {
32565
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32566
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32567
+ if (isZeroDurationSet(dur)) continue;
32568
+ const read = readTween(tween.vars);
32569
+ if (!read) continue;
32570
+ if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
32571
+ if (firstRead === null) firstRead = read;
32572
+ if (now2 != null) {
32573
+ const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
32574
+ if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
32575
+ }
32576
+ }
32577
+ }
32578
+ return firstRead;
32579
+ }
32580
+ function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
32581
+ let targetEl = null;
32582
+ try {
32583
+ targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
32584
+ } catch {
32585
+ return false;
32586
+ }
32587
+ return !!targetEl && hasNonHoldTween(timelinesOf(iframe), targetEl, channels, compositionId);
32588
+ }
32589
+ var POSITION_CHANNELS = [
32590
+ "x",
32591
+ "y",
32592
+ "xPercent",
32593
+ "yPercent",
32594
+ "left",
32595
+ "top",
32596
+ // readTween reads the authored translateX/Y; GSAP normalizes them to x/y only at play time.
32597
+ "translateX",
32598
+ "translateY"
32599
+ ];
32600
+ var MOVE_CHANNELS = [...POSITION_CHANNELS, "motionPath"];
32601
+ function gsapWritesChannels(el, channels) {
32602
+ const win = el.ownerDocument.defaultView;
32603
+ return Object.values(win?.__timelines ?? {}).some(
32604
+ (tl) => (tl?.getChildren?.(true) ?? []).some(
32605
+ (tween) => !!tween.vars && matchesElement(tween, el) && (channels.some((ch) => ch in tween.vars) || keyframeVarsCarryChannel(tween.vars, channels))
32606
+ )
32607
+ );
32608
+ }
32609
+ function gsapWritesPosition(el) {
32610
+ const cache2 = el._gsap;
32611
+ return !!cache2?.renderTransform || gsapWritesChannels(el, MOVE_CHANNELS);
32612
+ }
32613
+ function elementHasNonHoldTween(el, channels) {
32614
+ const win = el.ownerDocument.defaultView;
32615
+ return hasNonHoldTween(win?.__timelines ?? null, el, channels);
32616
+ }
32617
+ function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
32618
+ if (!timelines) return false;
32619
+ for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
32620
+ for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
32621
+ if (!tween.vars || !matchesElement(tween, targetEl)) continue;
32622
+ const dur = typeof tween.duration === "function" ? tween.duration() : 0;
32623
+ if (isZeroDurationSet(dur)) continue;
32624
+ if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
32625
+ const read = readTween(tween.vars);
32626
+ if (read && (!channels || readCarriesChannel(read, channels))) return true;
32627
+ }
32628
+ }
32629
+ return false;
32630
+ }
32631
+ function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
32632
+ if (!clip || clip.duration <= 0) return keyframes;
32633
+ return keyframes.map((kf) => {
32634
+ const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
32635
+ return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
32636
+ });
32637
+ }
32638
+ function buildEntry(read, start, duration, clip) {
32639
+ return {
32640
+ keyframes: toClipRelative(read.keyframes, start, duration, clip),
32641
+ tweenStart: start,
32642
+ tweenDuration: duration,
32643
+ ...read.easeEach ? { easeEach: read.easeEach } : {},
32644
+ ...read.arcPath ? { arcPath: read.arcPath } : {}
32645
+ };
32646
+ }
32647
+ function addScanEntry(result, tween, clipById) {
32648
+ if (!tween.targets || !tween.vars) return;
32649
+ const { start, duration } = tweenTiming(tween);
32650
+ if (isZeroDurationSet(duration)) return;
32651
+ const read = readTween(tween.vars);
32652
+ if (!read) return;
32653
+ for (const target of tween.targets()) {
32654
+ const id = target.id;
32655
+ if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
32656
+ }
32657
+ }
32658
+ function scanAllRuntimeKeyframes(iframe, clipById) {
32659
+ const result = /* @__PURE__ */ new Map();
32660
+ const timelines = timelinesOf(iframe);
32661
+ if (!timelines) return result;
32662
+ for (const timeline of Object.values(timelines)) {
32663
+ if (!timeline?.getChildren) continue;
32664
+ for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
32665
+ }
32666
+ return result;
32667
+ }
32668
+
32669
+ // src/components/editor/plainTranslate.ts
32670
+ var TERM = /^(-?\d*\.?\d+(?:e[-+]?\d+)?)(px|%)$/i;
32671
+ var SIGN = { "+": 1, "-": -1 };
32672
+ function termPx(term, side) {
32673
+ const match = TERM.exec(term ?? "");
32674
+ return match ? Number(match[1]) * (match[2] === "%" ? side / 100 : 1) : null;
32675
+ }
32676
+ function resolveLength(value, side) {
32677
+ const parts = (/^calc\((.*)\)$/.exec(value)?.[1] ?? value).split(" ");
32678
+ if (parts.length % 2 === 0) return null;
32679
+ let total = termPx(parts[0], side);
32680
+ for (let i = 1; i < parts.length && total !== null; i += 2) {
32681
+ const term = termPx(parts[i + 1], side);
32682
+ const sign = SIGN[parts[i] ?? ""];
32683
+ total = term === null || !sign ? null : total + sign * term;
32684
+ }
32685
+ return total;
32686
+ }
32687
+ function borderBox(cs, size, a, b) {
32688
+ const n = (prop) => Number.parseFloat(cs.getPropertyValue(prop)) || 0;
32689
+ const pad = n(`padding-${a}`) + n(`padding-${b}`) + n(`border-${a}-width`) + n(`border-${b}-width`);
32690
+ return n(size) + (cs.boxSizing === "border-box" ? 0 : pad);
32691
+ }
32692
+ function resolveInTransform(el, x, y) {
32693
+ const style = el.style;
32694
+ const saved2 = [style.getPropertyValue("transform"), style.getPropertyPriority("transform")];
32695
+ style.setProperty("transform", `translate(${x}, ${y})`, "important");
32696
+ try {
32697
+ const resolved2 = el.ownerDocument.defaultView?.getComputedStyle(el).transform ?? "";
32698
+ const matrix = new DOMMatrixReadOnly(resolved2.startsWith("matrix") ? resolved2 : void 0);
32699
+ return { x: matrix.m41, y: matrix.m42 };
32700
+ } finally {
32701
+ style.setProperty("transform", saved2[0] ?? "", saved2[1] ?? "");
32702
+ }
32703
+ }
32704
+ function readTranslatePx(el) {
32705
+ const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
32706
+ const value = el.style.getPropertyValue("translate") || cs?.translate || "none";
32707
+ if (value === "none") return { x: 0, y: 0 };
32708
+ const [x = "0px", y = "0px"] = splitTopLevelWhitespace(value);
32709
+ const px = cs && resolveLength(x, borderBox(cs, "width", "left", "right"));
32710
+ const py = cs && resolveLength(y, borderBox(cs, "height", "top", "bottom"));
32711
+ return px != null && py != null ? { x: px, y: py } : resolveInTransform(el, x, y);
32712
+ }
32713
+ function formatTranslatePx(p) {
32714
+ return `${roundTo3(p.x)}px ${roundTo3(p.y)}px`;
32715
+ }
32716
+ function translatePatch(p) {
32717
+ return { type: "inline-style", property: "translate", value: formatTranslatePx(p) };
32718
+ }
32719
+ function writeTranslatePx(el, p) {
32720
+ el.style.setProperty("translate", formatTranslatePx(p));
32721
+ }
32722
+ function readMoveOffset(el) {
32723
+ return gsapWritesPosition(el) ? readStudioPathOffset(el) : readTranslatePx(el);
32724
+ }
32725
+
32386
32726
  // src/components/editor/manualOffsetDrag.ts
32387
32727
  function getOffsetDragGsap(element) {
32388
32728
  const win = element.ownerDocument.defaultView;
@@ -32494,14 +32834,15 @@ function measureManualOffsetDragScreenToOffsetMatrix(element, initialOffset, opt
32494
32834
  if (!Number.isFinite(probeSize) || probeSize <= 0) {
32495
32835
  return { ok: false, reason: "Invalid movement probe size." };
32496
32836
  }
32837
+ const draw = options.plainTranslate ? writeTranslatePx : applyStudioPathOffsetDraft;
32497
32838
  const snapshot3 = captureStudioPathOffset(element);
32498
32839
  try {
32499
- applyStudioPathOffsetDraft(element, initialOffset);
32840
+ draw(element, initialOffset);
32500
32841
  const origin = getRectCenter(element);
32501
32842
  if (!origin) {
32502
32843
  return { ok: false, reason: "Element has no measurable box." };
32503
32844
  }
32504
- applyStudioPathOffsetDraft(element, {
32845
+ draw(element, {
32505
32846
  x: initialOffset.x + probeSize,
32506
32847
  y: initialOffset.y
32507
32848
  });
@@ -32509,7 +32850,7 @@ function measureManualOffsetDragScreenToOffsetMatrix(element, initialOffset, opt
32509
32850
  if (!probeX) {
32510
32851
  return { ok: false, reason: "Element X movement could not be measured." };
32511
32852
  }
32512
- applyStudioPathOffsetDraft(element, {
32853
+ draw(element, {
32513
32854
  x: initialOffset.x,
32514
32855
  y: initialOffset.y + probeSize
32515
32856
  });
@@ -32554,14 +32895,13 @@ function resolveManualOffsetForPointerDelta(input) {
32554
32895
  };
32555
32896
  }
32556
32897
  function createManualOffsetDragMember(input) {
32557
- const initialOffset = readAppliedStudioPathOffset(input.element);
32558
- input.element.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
32559
- input.element.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
32898
+ const plainTranslate = !!input.gesture && !gsapWritesPosition(input.element);
32899
+ const initialOffset = plainTranslate ? readTranslatePx(input.element) : readAppliedStudioPathOffset(input.element);
32560
32900
  const win = input.element.ownerDocument.defaultView;
32561
- const gsapX = win?.gsap?.getProperty?.(input.element, "x") || 0;
32562
- const gsapY = win?.gsap?.getProperty?.(input.element, "y") || 0;
32563
- input.element.setAttribute("data-hf-drag-gsap-base-x", String(gsapX));
32564
- input.element.setAttribute("data-hf-drag-gsap-base-y", String(gsapY));
32901
+ const gsapX = !plainTranslate && win?.gsap?.getProperty?.(input.element, "x") || 0;
32902
+ const gsapY = !plainTranslate && win?.gsap?.getProperty?.(input.element, "y") || 0;
32903
+ const baseGsap = { x: gsapX, y: gsapY };
32904
+ if (!plainTranslate) stampGestureBase(input.element, initialOffset, baseGsap);
32565
32905
  if (win?.__timelines) {
32566
32906
  const paused = [];
32567
32907
  for (const [id, tl] of Object.entries(win.__timelines)) {
@@ -32581,9 +32921,9 @@ function createManualOffsetDragMember(input) {
32581
32921
  const gestureToken = beginStudioManualEditGesture(input.element);
32582
32922
  const measured = measureManualOffsetDragScreenToOffsetMatrix(input.element, initialOffset, {
32583
32923
  scaleX: input.rect.editScaleX,
32584
- scaleY: input.rect.editScaleY
32924
+ scaleY: input.rect.editScaleY,
32925
+ plainTranslate
32585
32926
  });
32586
- const baseGsap = { x: gsapX, y: gsapY };
32587
32927
  if (!measured.ok) {
32588
32928
  const scaleX = input.rect.editScaleX || 1;
32589
32929
  const scaleY = input.rect.editScaleY || 1;
@@ -32595,6 +32935,7 @@ function createManualOffsetDragMember(input) {
32595
32935
  selection: input.selection,
32596
32936
  element: input.element,
32597
32937
  initialOffset,
32938
+ plainTranslate,
32598
32939
  baseGsap,
32599
32940
  initialPathOffset,
32600
32941
  gestureToken,
@@ -32610,6 +32951,7 @@ function createManualOffsetDragMember(input) {
32610
32951
  selection: input.selection,
32611
32952
  element: input.element,
32612
32953
  initialOffset,
32954
+ plainTranslate,
32613
32955
  baseGsap,
32614
32956
  initialPathOffset,
32615
32957
  gestureToken,
@@ -32626,24 +32968,27 @@ function resolveManualOffsetDragMemberOffset(member, dx, dy) {
32626
32968
  dy
32627
32969
  });
32628
32970
  }
32971
+ function drawMemberOffset(member, offset, commit) {
32972
+ if (member.plainTranslate) writeTranslatePx(member.element, offset);
32973
+ else if (applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) return;
32974
+ else if (commit) applyStudioPathOffset(member.element, offset);
32975
+ else applyStudioPathOffsetDraft(member.element, offset);
32976
+ }
32629
32977
  function applyManualOffsetDragDraft(member, dx, dy) {
32630
32978
  const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
32631
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32632
- applyStudioPathOffsetDraft(member.element, offset);
32633
- }
32979
+ drawMemberOffset(member, offset, false);
32634
32980
  return offset;
32635
32981
  }
32636
- function restampManualOffsetDragGestureBase(member) {
32637
- member.element.setAttribute("data-hf-drag-gsap-base-x", String(member.baseGsap.x));
32638
- member.element.setAttribute("data-hf-drag-gsap-base-y", String(member.baseGsap.y));
32639
- member.element.setAttribute("data-hf-drag-initial-offset-x", String(member.initialOffset.x));
32640
- member.element.setAttribute("data-hf-drag-initial-offset-y", String(member.initialOffset.y));
32982
+ function stampGestureBase(el, initialOffset, baseGsap) {
32983
+ el.setAttribute("data-hf-drag-gsap-base-x", String(baseGsap.x));
32984
+ el.setAttribute("data-hf-drag-gsap-base-y", String(baseGsap.y));
32985
+ el.setAttribute("data-hf-drag-initial-offset-x", String(initialOffset.x));
32986
+ el.setAttribute("data-hf-drag-initial-offset-y", String(initialOffset.y));
32641
32987
  }
32642
32988
  function applyManualOffsetCommitValue(member, offset) {
32643
- restampManualOffsetDragGestureBase(member);
32644
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32645
- applyStudioPathOffset(member.element, offset);
32646
- }
32989
+ if (!member.plainTranslate)
32990
+ stampGestureBase(member.element, member.initialOffset, member.baseGsap);
32991
+ drawMemberOffset(member, offset, true);
32647
32992
  return offset;
32648
32993
  }
32649
32994
  function applyManualOffsetDragCommit(member, dx, dy) {
@@ -32654,9 +32999,7 @@ function applyManualOffsetNudgeDraft(member, delta) {
32654
32999
  x: member.initialOffset.x + delta.x,
32655
33000
  y: member.initialOffset.y + delta.y
32656
33001
  };
32657
- if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
32658
- applyStudioPathOffsetDraft(member.element, offset);
32659
- }
33002
+ drawMemberOffset(member, offset, false);
32660
33003
  return offset;
32661
33004
  }
32662
33005
  function applyManualOffsetNudgeCommit(member, delta) {
@@ -32667,7 +33010,9 @@ function applyManualOffsetNudgeCommit(member, delta) {
32667
33010
  }
32668
33011
  function restoreManualOffsetDragMember(member) {
32669
33012
  restoreStudioPathOffset(member.element, member.initialPathOffset);
32670
- getOffsetDragGsap(member.element)?.set(member.element, { ...member.baseGsap });
33013
+ if (!member.plainTranslate) {
33014
+ getOffsetDragGsap(member.element)?.set(member.element, { ...member.baseGsap });
33015
+ }
32671
33016
  endStudioManualEditGesture(member.element, member.gestureToken);
32672
33017
  }
32673
33018
  function restoreManualOffsetDragMembers(members) {
@@ -32692,7 +33037,7 @@ function endManualOffsetDragMembers(members) {
32692
33037
  member.element.removeAttribute("data-hf-drag-initial-offset-y");
32693
33038
  member.element.removeAttribute("data-hf-drag-gsap-base-x");
32694
33039
  member.element.removeAttribute("data-hf-drag-gsap-base-y");
32695
- const gsapOwnsPosition = Boolean(getOffsetDragGsap(member.element));
33040
+ const gsapOwnsPosition = !member.plainTranslate && Boolean(getOffsetDragGsap(member.element));
32696
33041
  if (gsapOwnsPosition) clearStudioPathOffset(member.element);
32697
33042
  settleDraftTranslate(member.element, gsapOwnsPosition);
32698
33043
  resumeGsapTimelines(member.element);
@@ -33345,7 +33690,8 @@ function startGroupDrag(e, opts) {
33345
33690
  key: item.key,
33346
33691
  selection: item.selection,
33347
33692
  element: item.element,
33348
- rect: item.rect
33693
+ rect: item.rect,
33694
+ gesture: "drag"
33349
33695
  });
33350
33696
  if (!result.ok) {
33351
33697
  restoreManualOffsetDragMembers(members);
@@ -33409,7 +33755,6 @@ function startGesture(kind, e, opts, options) {
33409
33755
  if (kind === "resize" && (!Number.isFinite(rect.width) || !Number.isFinite(rect.height)))
33410
33756
  return false;
33411
33757
  const size = readStudioBoxSize(sel.element);
33412
- const rotation = { angle: readGsapRotation(sel.element) + readStudioRotation(sel.element).angle };
33413
33758
  const layoutWidth = sel.element.offsetWidth;
33414
33759
  const layoutHeight = sel.element.offsetHeight;
33415
33760
  const actualWidth = size.width > 0 ? size.width : layoutWidth > 0 ? layoutWidth : rect.width / rect.editScaleX;
@@ -33440,7 +33785,8 @@ function startGesture(kind, e, opts, options) {
33440
33785
  key: selectionCacheKey(sel),
33441
33786
  selection: sel,
33442
33787
  element: sel.element,
33443
- rect
33788
+ rect,
33789
+ gesture: "drag"
33444
33790
  });
33445
33791
  if (!result.ok) {
33446
33792
  opts.onBlockedMoveRef.current(result.selection);
@@ -33469,6 +33815,8 @@ function startGesture(kind, e, opts, options) {
33469
33815
  manualEditDragToken = beginStudioManualEditGesture(sel.element);
33470
33816
  }
33471
33817
  }
33818
+ const gsapRotation = pathOffsetMember?.plainTranslate ? 0 : readGsapRotation(sel.element);
33819
+ const rotation = { angle: gsapRotation + readStudioRotation(sel.element).angle };
33472
33820
  const overlayBounds = overlayEl?.getBoundingClientRect();
33473
33821
  const centerX = (overlayBounds?.left ?? 0) + rect.left + rect.width / 2;
33474
33822
  const centerY = (overlayBounds?.top ?? 0) + rect.top + rect.height / 2;
@@ -33619,6 +33967,7 @@ var DomEditSaveQueueOpenError = class extends Error {
33619
33967
  function createDomEditSaveQueue(options = {}) {
33620
33968
  const failureThreshold = options.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
33621
33969
  let tail = Promise.resolve();
33970
+ let queuedCount = 0;
33622
33971
  let consecutiveFailures = 0;
33623
33972
  let breakerOpen = false;
33624
33973
  let drainError = null;
@@ -33657,13 +34006,14 @@ function createDomEditSaveQueue(options = {}) {
33657
34006
  return {
33658
34007
  enqueue(save) {
33659
34008
  if (breakerOpen) return Promise.reject(new DomEditSaveQueueOpenError());
34009
+ queuedCount += 1;
33660
34010
  const queued = tail.catch(() => void 0).then(() => {
33661
34011
  if (breakerOpen) throw new DomEditSaveQueueOpenError();
33662
34012
  return run(save);
33663
34013
  });
33664
34014
  tail = queued.then(
33665
- () => void 0,
33666
- () => void 0
34015
+ () => void (queuedCount -= 1),
34016
+ () => void (queuedCount -= 1)
33667
34017
  );
33668
34018
  return queued;
33669
34019
  },
@@ -33675,6 +34025,7 @@ function createDomEditSaveQueue(options = {}) {
33675
34025
  if (drainError != null) return { status: "failed", error: drainError };
33676
34026
  return { status: "clean" };
33677
34027
  },
34028
+ isIdle: () => queuedCount === 0,
33678
34029
  reset,
33679
34030
  destroy() {
33680
34031
  reset(false);
@@ -34185,7 +34536,7 @@ function useDomEditNudge(params) {
34185
34536
  if (!targets) return null;
34186
34537
  const members = [];
34187
34538
  for (const target of targets) {
34188
- const result = createManualOffsetDragMember(target);
34539
+ const result = createManualOffsetDragMember({ ...target, gesture: "nudge" });
34189
34540
  if (!result.ok) {
34190
34541
  restoreManualOffsetDragMembers(members);
34191
34542
  return null;
@@ -34375,253 +34726,6 @@ var SnapGuideOverlay = memo23(function SnapGuideOverlay2({
34375
34726
  // src/components/editor/DomEditCropHandles.tsx
34376
34727
  import { useEffect as useEffect44, useRef as useRef65, useState as useState40 } from "react";
34377
34728
 
34378
- // src/hooks/gsapRuntimeKeyframes.ts
34379
- import { buildArcPath } from "@hyperframes/core/gsap-parser-acorn";
34380
- var FLAT_SKIP_KEYS = /* @__PURE__ */ new Set([
34381
- "ease",
34382
- "duration",
34383
- "delay",
34384
- "stagger",
34385
- "motionPath",
34386
- "overwrite",
34387
- "immediateRender",
34388
- "onComplete",
34389
- "onUpdate",
34390
- "onStart",
34391
- "keyframes"
34392
- ]);
34393
- function timelinesOf(iframe) {
34394
- if (!iframe?.contentWindow) return null;
34395
- try {
34396
- return iframe.contentWindow.__timelines ?? null;
34397
- } catch {
34398
- return null;
34399
- }
34400
- }
34401
- function isXY(p) {
34402
- return !!p && typeof p.x === "number" && typeof p.y === "number";
34403
- }
34404
- function isZeroDurationSet(duration) {
34405
- return !(duration > 0);
34406
- }
34407
- function coordsFromMotionPath(mp) {
34408
- if (!mp || typeof mp !== "object") return null;
34409
- const obj = mp;
34410
- const pathVal = Array.isArray(mp) ? mp : obj.path;
34411
- if (!Array.isArray(pathVal)) return null;
34412
- const coords = pathVal.filter(isXY).map((p) => ({ x: p.x, y: p.y }));
34413
- if (coords.length < 2) return null;
34414
- const curviness = typeof obj.curviness === "number" ? obj.curviness : 1;
34415
- const autoRotate = typeof obj.autoRotate === "number" ? obj.autoRotate : obj.autoRotate === true;
34416
- return { coords, curviness, autoRotate, isCubic: obj.type === "cubic" };
34417
- }
34418
- function flatTweenKeyframes(vars) {
34419
- const properties = {};
34420
- for (const [k, v] of Object.entries(vars)) {
34421
- if (FLAT_SKIP_KEYS.has(k)) continue;
34422
- if (typeof v === "number") properties[k] = roundTo3(v);
34423
- else if (typeof v === "string") properties[k] = v;
34424
- }
34425
- if (Object.keys(properties).length === 0) return null;
34426
- return [
34427
- { percentage: 0, properties },
34428
- { percentage: 100, properties }
34429
- ];
34430
- }
34431
- function readTween(vars) {
34432
- if (vars.keyframes && typeof vars.keyframes === "object") {
34433
- const parsed = parsePercentageKeyframes(vars.keyframes);
34434
- if (parsed) return parsed;
34435
- }
34436
- const mp = coordsFromMotionPath(vars.motionPath);
34437
- if (mp) {
34438
- const shape = buildArcPath(mp.coords, mp.curviness, mp.autoRotate, mp.isCubic);
34439
- if (shape) {
34440
- const n = shape.waypoints.length;
34441
- const keyframes = shape.waypoints.map((wp, i) => ({
34442
- percentage: n > 1 ? Math.round(i / (n - 1) * 100) : 0,
34443
- properties: { x: wp.x, y: wp.y }
34444
- }));
34445
- return { keyframes, arcPath: shape.arcPath };
34446
- }
34447
- }
34448
- const flat = flatTweenKeyframes(vars);
34449
- return flat ? { keyframes: flat } : null;
34450
- }
34451
- function matchesElement(tween, el) {
34452
- if (!tween.targets) return false;
34453
- for (const t of tween.targets()) {
34454
- if (t === el || el.id && t.id === el.id) return true;
34455
- }
34456
- return false;
34457
- }
34458
- function tweenTiming(tween) {
34459
- const rawStart = typeof tween.startTime === "function" ? tween.startTime() : 0;
34460
- const rawDur = typeof tween.duration === "function" ? tween.duration() : 0;
34461
- return {
34462
- start: Number.isFinite(rawStart) ? rawStart : 0,
34463
- duration: Number.isFinite(rawDur) ? rawDur : 0
34464
- };
34465
- }
34466
- function varsCarryChannel(vars, channels) {
34467
- if (!vars) return false;
34468
- for (const ch of channels) {
34469
- if (Object.prototype.hasOwnProperty.call(vars, ch)) return true;
34470
- }
34471
- return false;
34472
- }
34473
- function keyframeVarsCarryChannel(vars, channels) {
34474
- const kf = vars?.keyframes;
34475
- if (!kf || typeof kf !== "object") return false;
34476
- if (!Array.isArray(kf) && channels.some((ch) => Object.prototype.hasOwnProperty.call(kf, ch)))
34477
- return true;
34478
- const steps = Array.isArray(kf) ? kf : Object.values(kf);
34479
- return steps.some(
34480
- (step) => step != null && typeof step === "object" && channels.some((ch) => Object.prototype.hasOwnProperty.call(step, ch))
34481
- );
34482
- }
34483
- function resolveRuntimeTween(iframe, selector, kind, compositionId, channels) {
34484
- const timelines = timelinesOf(iframe);
34485
- if (!timelines) return null;
34486
- let targetEl = null;
34487
- try {
34488
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34489
- } catch {
34490
- return null;
34491
- }
34492
- if (!targetEl) return null;
34493
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34494
- const wantChannels = channels && channels.length > 0 ? channels : null;
34495
- let first = null;
34496
- let channelMatch = null;
34497
- for (const tlId of tlIds) {
34498
- const timeline = timelines[tlId];
34499
- if (!timeline?.getChildren) continue;
34500
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34501
- for (const tween of timeline.getChildren(true)) {
34502
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34503
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34504
- const isSet = !(dur > 0);
34505
- if (kind === "set" ? !isSet : isSet) continue;
34506
- if (wantChannels) {
34507
- const carries = kind === "set" ? varsCarryChannel(tween.vars, wantChannels) : keyframeVarsCarryChannel(tween.vars, wantChannels);
34508
- if (carries) {
34509
- if (channelMatch === null) channelMatch = { tween, timeline };
34510
- } else if (first === null) {
34511
- first = { tween, timeline };
34512
- }
34513
- continue;
34514
- }
34515
- if (first === null) first = { tween, timeline };
34516
- if (kind === "keyframe" && now2 != null) {
34517
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34518
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return { tween, timeline };
34519
- }
34520
- }
34521
- }
34522
- return channelMatch ?? first;
34523
- }
34524
- function readCarriesChannel(read, channels) {
34525
- return read.keyframes.some((kf) => channels.some((c) => kf.properties[c] != null));
34526
- }
34527
- function readRuntimeKeyframes(iframe, selector, compositionId, requireChannels) {
34528
- const timelines = timelinesOf(iframe);
34529
- if (!timelines) return null;
34530
- let targetEl = null;
34531
- try {
34532
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34533
- } catch {
34534
- return null;
34535
- }
34536
- if (!targetEl) return null;
34537
- const tlIds = compositionId ? [compositionId] : Object.keys(timelines).filter((k) => typeof timelines[k]?.getChildren === "function");
34538
- if (tlIds.length === 0) return null;
34539
- let firstRead = null;
34540
- for (const tlId of tlIds) {
34541
- const timeline = timelines[tlId];
34542
- if (!timeline?.getChildren) continue;
34543
- const now2 = typeof timeline.time === "function" ? timeline.time() : null;
34544
- for (const tween of timeline.getChildren(true)) {
34545
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34546
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34547
- if (isZeroDurationSet(dur)) continue;
34548
- const read = readTween(tween.vars);
34549
- if (!read) continue;
34550
- if (requireChannels && !readCarriesChannel(read, requireChannels)) continue;
34551
- if (firstRead === null) firstRead = read;
34552
- if (now2 != null) {
34553
- const start = typeof tween.startTime === "function" ? tween.startTime() : 0;
34554
- if (now2 >= start - 1e-3 && now2 <= start + dur + 1e-3) return read;
34555
- }
34556
- }
34557
- }
34558
- return firstRead;
34559
- }
34560
- function hasNonHoldTweenForElement(iframe, selector, compositionId, channels) {
34561
- let targetEl = null;
34562
- try {
34563
- targetEl = iframe?.contentDocument?.querySelector(selector) ?? null;
34564
- } catch {
34565
- return false;
34566
- }
34567
- return !!targetEl && hasNonHoldTween(timelinesOf(iframe), targetEl, channels, compositionId);
34568
- }
34569
- function elementHasNonHoldTween(el, channels) {
34570
- const win = el.ownerDocument.defaultView;
34571
- return hasNonHoldTween(win?.__timelines ?? null, el, channels);
34572
- }
34573
- function hasNonHoldTween(timelines, targetEl, channels, compositionId) {
34574
- if (!timelines) return false;
34575
- for (const tlId of compositionId ? [compositionId] : Object.keys(timelines)) {
34576
- for (const tween of timelines[tlId]?.getChildren?.(true) ?? []) {
34577
- if (!tween.vars || !matchesElement(tween, targetEl)) continue;
34578
- const dur = typeof tween.duration === "function" ? tween.duration() : 0;
34579
- if (isZeroDurationSet(dur)) continue;
34580
- if (channels && keyframeVarsCarryChannel(tween.vars, channels)) return true;
34581
- const read = readTween(tween.vars);
34582
- if (read && (!channels || readCarriesChannel(read, channels))) return true;
34583
- }
34584
- }
34585
- return false;
34586
- }
34587
- function toClipRelative(keyframes, tweenStart, tweenDuration, clip) {
34588
- if (!clip || clip.duration <= 0) return keyframes;
34589
- return keyframes.map((kf) => {
34590
- const abs = toAbsoluteTime(tweenStart, tweenDuration, kf.percentage);
34591
- return { ...kf, percentage: Math.round((abs - clip.start) / clip.duration * 1e5) / 1e3 };
34592
- });
34593
- }
34594
- function buildEntry(read, start, duration, clip) {
34595
- return {
34596
- keyframes: toClipRelative(read.keyframes, start, duration, clip),
34597
- tweenStart: start,
34598
- tweenDuration: duration,
34599
- ...read.easeEach ? { easeEach: read.easeEach } : {},
34600
- ...read.arcPath ? { arcPath: read.arcPath } : {}
34601
- };
34602
- }
34603
- function addScanEntry(result, tween, clipById) {
34604
- if (!tween.targets || !tween.vars) return;
34605
- const { start, duration } = tweenTiming(tween);
34606
- if (isZeroDurationSet(duration)) return;
34607
- const read = readTween(tween.vars);
34608
- if (!read) return;
34609
- for (const target of tween.targets()) {
34610
- const id = target.id;
34611
- if (id && !result.has(id)) result.set(id, buildEntry(read, start, duration, clipById?.get(id)));
34612
- }
34613
- }
34614
- function scanAllRuntimeKeyframes(iframe, clipById) {
34615
- const result = /* @__PURE__ */ new Map();
34616
- const timelines = timelinesOf(iframe);
34617
- if (!timelines) return result;
34618
- for (const timeline of Object.values(timelines)) {
34619
- if (!timeline?.getChildren) continue;
34620
- for (const tween of timeline.getChildren(true)) addScanEntry(result, tween, clipById);
34621
- }
34622
- return result;
34623
- }
34624
-
34625
34729
  // src/components/editor/cropResize.ts
34626
34730
  var boxOf = (element) => ({
34627
34731
  width: element.offsetWidth,
@@ -42384,19 +42488,6 @@ function getAnimationsForElement(animations, target, element) {
42384
42488
  }
42385
42489
 
42386
42490
  // src/hooks/gsapRuntimeBridge.ts
42387
- var POSITION_CHANNELS = [
42388
- "x",
42389
- "y",
42390
- "xPercent",
42391
- "yPercent",
42392
- "left",
42393
- "top",
42394
- // GSAP normalizes translateX/Y to x/y at play time, but readTween reads the
42395
- // AUTHORED shape — include them so a hand-authored translateX/Y position tween
42396
- // still counts as a live position tween.
42397
- "translateX",
42398
- "translateY"
42399
- ];
42400
42491
  var POSITION_CHANNEL_SET = new Set(POSITION_CHANNELS);
42401
42492
  var ROTATION_CHANNELS = ["rotation", "rotationX", "rotationY", "rotationZ"];
42402
42493
  var ROTATION_CHANNEL_SET = new Set(ROTATION_CHANNELS);
@@ -45044,12 +45135,13 @@ import { CornersIn, CornersOut, DotsThree, X as X2 } from "@phosphor-icons/react
45044
45135
  import { create as create6 } from "zustand";
45045
45136
  var useDockLayoutStore = create6((set, get) => ({
45046
45137
  controller: null,
45138
+ panels: PANEL_IDS,
45047
45139
  openPanels: new Set(PANEL_IDS),
45048
45140
  visiblePanels: new Set(PANEL_IDS),
45049
45141
  activePanel: null,
45050
45142
  lastActive: {},
45051
45143
  pendingActivation: null,
45052
- attach: (controller) => set({ controller }),
45144
+ attach: (controller, panels = PANEL_IDS) => set({ controller, panels }),
45053
45145
  detach: () => set({ controller: null, lastActive: {} }),
45054
45146
  sync: (snapshot3) => set((state) => {
45055
45147
  const lastActive = { ...state.lastActive };
@@ -45325,6 +45417,8 @@ var MIN_TIMELINE_H = 100;
45325
45417
  var MIN_SIDE_W = 200;
45326
45418
  var MIN_SIDE_W_FLOOR = 120;
45327
45419
  var DEFAULT_TIMELINE_H = 360;
45420
+ var DEFAULT_LEFT = ["compositions", "assets", "code", "catalog"];
45421
+ var DEFAULT_RIGHT = ["design", "layers", "renders", "variables"];
45328
45422
  function defaultSideWidths(viewportWidth) {
45329
45423
  const floor = sideMinimumWidth(viewportWidth);
45330
45424
  const left = Math.max(floor, Math.min(384, Math.round(viewportWidth * 0.257)));
@@ -45380,24 +45474,25 @@ function addRegisteredPanel(api, id, position) {
45380
45474
  ...position ? { position } : {}
45381
45475
  });
45382
45476
  }
45383
- function buildEditLayout(api, viewportWidth) {
45477
+ function addSideColumn(api, ids, panels, direction) {
45478
+ const [first, ...rest] = ids.filter((id) => panels.includes(id));
45479
+ if (!first) return void 0;
45480
+ addRegisteredPanel(api, first, { referencePanel: "preview", direction });
45481
+ for (const id of rest)
45482
+ addRegisteredPanel(api, id, { referencePanel: first, direction: "within" });
45483
+ api.getPanel(first)?.api.setActive();
45484
+ return first;
45485
+ }
45486
+ function buildEditLayout(api, viewportWidth, panels = PANEL_IDS) {
45384
45487
  api.clear();
45385
45488
  const widths = defaultSideWidths(viewportWidth);
45386
45489
  addRegisteredPanel(api, "preview");
45387
45490
  addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" });
45388
- addRegisteredPanel(api, "compositions", { referencePanel: "preview", direction: "left" });
45389
- for (const id of ["assets", "code", "catalog"]) {
45390
- addRegisteredPanel(api, id, { referencePanel: "compositions", direction: "within" });
45391
- }
45392
- addRegisteredPanel(api, "design", { referencePanel: "preview", direction: "right" });
45393
- for (const id of ["layers", "renders", "variables"]) {
45394
- addRegisteredPanel(api, id, { referencePanel: "design", direction: "within" });
45395
- }
45396
- api.getPanel("compositions")?.api.setActive();
45397
- api.getPanel("design")?.api.setActive();
45491
+ const left = addSideColumn(api, DEFAULT_LEFT, panels, "left");
45492
+ const right = addSideColumn(api, DEFAULT_RIGHT, panels, "right");
45398
45493
  applySideMinimums(api, viewportWidth);
45399
- api.getPanel("compositions")?.group.api.setSize({ width: widths.left });
45400
- api.getPanel("design")?.group.api.setSize({ width: widths.right });
45494
+ if (left) api.getPanel(left)?.group.api.setSize({ width: widths.left });
45495
+ if (right) api.getPanel(right)?.group.api.setSize({ width: widths.right });
45401
45496
  api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H });
45402
45497
  }
45403
45498
 
@@ -45442,10 +45537,11 @@ function snapshot(api) {
45442
45537
  groupActivePanels
45443
45538
  };
45444
45539
  }
45445
- function createController(api) {
45540
+ function createController(api, options) {
45446
45541
  const open = (id) => {
45447
45542
  if (api.getPanel(id)) return;
45448
- const { zone, reopen } = PANEL_DEFINITIONS[id];
45543
+ const { zone, reopen: preferred } = PANEL_DEFINITIONS[id];
45544
+ const reopen = zone === "center" || options.panels.includes(preferred.near) ? preferred : { near: "preview", direction: zone };
45449
45545
  const sibling = zone === "center" ? void 0 : panelsInZone(zone).find((other) => other !== id && api.getPanel(other));
45450
45546
  if (sibling) {
45451
45547
  addRegisteredPanel(api, id, { referencePanel: sibling, direction: "within" });
@@ -45467,11 +45563,12 @@ function createController(api) {
45467
45563
  },
45468
45564
  setTitle: (id, title) => api.getPanel(id)?.api.setTitle(title),
45469
45565
  setGroupVisible: (id, visible) => api.getPanel(id)?.group.api.setVisible(visible),
45470
- reset: () => buildEditLayout(api, window.innerWidth)
45566
+ reset: () => buildEditLayout(api, options.dockWidth(), options.panels)
45471
45567
  };
45472
45568
  }
45473
- function restoreOrBuild(api, projectId) {
45474
- const stored = readStudioUiPreferences(void 0, projectId).dockLayout;
45569
+ function restoreOrBuild(api, projectId, options) {
45570
+ const { dockLayout } = readStudioUiPreferences(void 0, projectId, options.storageKey);
45571
+ const stored = parseDockLayout(dockLayout, options.panels);
45475
45572
  if (stored) {
45476
45573
  try {
45477
45574
  api.fromJSON(stored);
@@ -45479,9 +45576,16 @@ function restoreOrBuild(api, projectId) {
45479
45576
  } catch {
45480
45577
  }
45481
45578
  }
45482
- buildEditLayout(api, window.innerWidth);
45579
+ buildEditLayout(api, options.dockWidth(), options.panels);
45483
45580
  }
45484
- function Root({ projectId, children }) {
45581
+ var windowWidth = () => window.innerWidth;
45582
+ function Root({
45583
+ projectId,
45584
+ panels = PANEL_IDS,
45585
+ storageKey,
45586
+ dockWidth = windowWidth,
45587
+ children
45588
+ }) {
45485
45589
  const [slots, setSlots] = useState50({});
45486
45590
  const registerSlot = useCallback70((id, element) => {
45487
45591
  setSlots((prev) => {
@@ -45498,17 +45602,18 @@ function Root({ projectId, children }) {
45498
45602
  const onReady = useCallback70(
45499
45603
  ({ api }) => {
45500
45604
  disposeRef.current();
45501
- restoreOrBuild(api, projectId);
45502
- applySideMinimums(api);
45605
+ const options = { panels, storageKey, dockWidth };
45606
+ restoreOrBuild(api, projectId, options);
45607
+ applySideMinimums(api, dockWidth());
45503
45608
  const root = api.groups[0]?.element.closest(".hf-dock");
45504
45609
  const disposeAccessibility = root ? installDockAccessibility(api, root) : () => {
45505
45610
  };
45506
45611
  const disposeTabFill = root ? installTabFill(api, root) : () => {
45507
45612
  };
45508
- const resizeObserver = new ResizeObserver(() => applySideMinimums(api, window.innerWidth));
45613
+ const resizeObserver = new ResizeObserver(() => applySideMinimums(api, dockWidth()));
45509
45614
  if (root) resizeObserver.observe(root);
45510
45615
  const store = useDockLayoutStore.getState();
45511
- store.attach(createController(api));
45616
+ store.attach(createController(api, options), panels);
45512
45617
  store.sync(snapshot(api));
45513
45618
  const pending2 = store.takePendingActivation();
45514
45619
  if (pending2) api.getPanel(pending2)?.api.setActive();
@@ -45516,7 +45621,7 @@ function Root({ projectId, children }) {
45516
45621
  const persist2 = () => {
45517
45622
  clearTimeout(timer);
45518
45623
  timer = setTimeout(() => {
45519
- writeStudioUiPreferences({ dockLayout: api.toJSON() }, void 0, projectId);
45624
+ writeStudioUiPreferences({ dockLayout: api.toJSON() }, void 0, projectId, storageKey);
45520
45625
  }, PERSIST_DEBOUNCE_MS2);
45521
45626
  };
45522
45627
  const onDockChange = () => {
@@ -45525,11 +45630,11 @@ function Root({ projectId, children }) {
45525
45630
  };
45526
45631
  const subscriptions = [
45527
45632
  api.onDidAddPanel(() => {
45528
- applySideMinimums(api);
45633
+ applySideMinimums(api, dockWidth());
45529
45634
  onDockChange();
45530
45635
  }),
45531
45636
  api.onDidMovePanel(() => {
45532
- applySideMinimums(api);
45637
+ applySideMinimums(api, dockWidth());
45533
45638
  onDockChange();
45534
45639
  }),
45535
45640
  api.onDidRemovePanel(onDockChange),
@@ -45545,7 +45650,7 @@ function Root({ projectId, children }) {
45545
45650
  useDockLayoutStore.getState().detach();
45546
45651
  };
45547
45652
  },
45548
- [projectId]
45653
+ [projectId, panels, storageKey, dockWidth]
45549
45654
  );
45550
45655
  return /* @__PURE__ */ jsxs71(SlotsContext.Provider, { value: { slots, registerSlot }, children: [
45551
45656
  /* @__PURE__ */ jsx101(
@@ -45580,6 +45685,7 @@ function Panel({ id, title, children }) {
45580
45685
  }
45581
45686
  function WindowMenu() {
45582
45687
  const openPanels = useDockLayoutStore((state) => state.openPanels);
45688
+ const panels = useDockLayoutStore((state) => state.panels);
45583
45689
  const togglePanel = useDockLayoutStore((state) => state.togglePanel);
45584
45690
  const resetLayout = useDockLayoutStore((state) => state.resetLayout);
45585
45691
  const [open, setOpen] = useState50(false);
@@ -45610,7 +45716,7 @@ function WindowMenu() {
45610
45716
  role: "menu",
45611
45717
  className: "absolute right-0 top-8 z-50 w-44 rounded-md border border-neutral-800 bg-neutral-900 py-1 shadow-lg",
45612
45718
  children: [
45613
- PANEL_IDS.map((id) => /* @__PURE__ */ jsxs71(
45719
+ panels.map((id) => /* @__PURE__ */ jsxs71(
45614
45720
  "button",
45615
45721
  {
45616
45722
  type: "button",
@@ -47838,7 +47944,7 @@ function createTransformCommitHandlers({
47838
47944
  "This element's position can't be edited here yet \u2014 it is driven by its animation"
47839
47945
  ))
47840
47946
  return;
47841
- const current = readStudioPathOffset(element.element);
47947
+ const current = readMoveOffset(element.element);
47842
47948
  await Promise.resolve(
47843
47949
  onSetManualOffset(element, {
47844
47950
  x: axis === "x" ? parsed : current.x,
@@ -48988,6 +49094,13 @@ function inspectDrainFailures(results) {
48988
49094
  }
48989
49095
  return { firstFailure };
48990
49096
  }
49097
+ function focusedField() {
49098
+ const active = document.activeElement;
49099
+ return active instanceof HTMLElement && active.matches('input, textarea, select, [contenteditable="true"], [role="textbox"]') ? active : null;
49100
+ }
49101
+ function hasStudioPendingEdits() {
49102
+ return pendingEditPromises.size > 0 || focusedField() !== null;
49103
+ }
48991
49104
  function trackStudioPendingEdit(result) {
48992
49105
  if (!result) return void 0;
48993
49106
  const promise = Promise.resolve(result);
@@ -48999,8 +49112,8 @@ function trackStudioPendingEdit(result) {
48999
49112
  return promise;
49000
49113
  }
49001
49114
  async function flushStudioPendingEdits() {
49002
- const active = document.activeElement;
49003
- if (active instanceof HTMLElement && active.matches('input, textarea, select, [contenteditable="true"], [role="textbox"]')) {
49115
+ const active = focusedField();
49116
+ if (active) {
49004
49117
  active.blur();
49005
49118
  await Promise.resolve();
49006
49119
  await waitForPostBlurEffects();
@@ -64921,7 +65034,7 @@ var PropertyPanel = memo36(function PropertyPanel2(props) {
64921
65034
  const sourceLabel = element.id ? `#${element.id}` : element.selector ?? "";
64922
65035
  const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
64923
65036
  const showEditableSections = element.capabilities.canEditStyles && sections.style;
64924
- const manualOffset = readStudioPathOffset(element.element);
65037
+ const manualOffset = readMoveOffset(element.element);
64925
65038
  const manualSize = readStudioBoxSize(element.element);
64926
65039
  const resolvedWidth = manualSize.width > 0 ? manualSize.width : parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width;
64927
65040
  const resolvedHeight = manualSize.height > 0 ? manualSize.height : parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height;
@@ -68027,6 +68140,60 @@ function useCaptionSync(projectId) {
68027
68140
  // src/hooks/usePersistentEditHistory.ts
68028
68141
  import { useCallback as useCallback97, useEffect as useEffect105, useMemo as useMemo65, useRef as useRef124, useState as useState104 } from "react";
68029
68142
  var EMPTY3 = { entries: [], back: null, forward: null };
68143
+ var OWN_ENTRIES_KEPT = 100;
68144
+ function createOwnHistory() {
68145
+ const own = /* @__PURE__ */ new Map();
68146
+ let next = null;
68147
+ let changes = 0;
68148
+ const remember = (id, files) => {
68149
+ const known = own.get(id) ?? {};
68150
+ for (const [path, { before, after }] of Object.entries(files)) {
68151
+ known[path] = { before: known[path]?.before ?? before, after };
68152
+ }
68153
+ own.delete(id);
68154
+ own.set(id, known);
68155
+ if (own.size > OWN_ENTRIES_KEPT) own.delete(own.keys().next().value);
68156
+ };
68157
+ return {
68158
+ remember,
68159
+ overtake: () => {
68160
+ changes += 1;
68161
+ next = null;
68162
+ return changes;
68163
+ },
68164
+ offered: (seen, view) => {
68165
+ if (seen === changes) next = { undo: view.back, redo: view.forward };
68166
+ },
68167
+ changes: () => changes,
68168
+ stepped: (entry2) => {
68169
+ const undone = entry2.undoes ? own.get(entry2.undoes) : void 0;
68170
+ if (!undone) return;
68171
+ const swapped = Object.entries(undone).map(([path, f]) => [
68172
+ path,
68173
+ { before: f.after, after: f.before }
68174
+ ]);
68175
+ remember(entry2.id, Object.fromEntries(swapped));
68176
+ },
68177
+ afterOf: (id) => Object.fromEntries(Object.entries(id && own.get(id) || {}).map(([p, f]) => [p, f.after])),
68178
+ predict: (direction) => {
68179
+ const step = next?.[direction];
68180
+ const files = step ? own.get(step.id) : void 0;
68181
+ const paths = files ? Object.keys(files) : [];
68182
+ if (!files || paths.length !== step.paths.length || !paths.every((p) => step.paths.includes(p)))
68183
+ return null;
68184
+ const restore = paths.map((path) => [
68185
+ path,
68186
+ { previous: files[path].after, restored: files[path].before }
68187
+ ]);
68188
+ return { id: step.id, files: Object.fromEntries(restore) };
68189
+ },
68190
+ clear: () => {
68191
+ own.clear();
68192
+ next = null;
68193
+ changes += 1;
68194
+ }
68195
+ };
68196
+ }
68030
68197
  var DEFAULT_COALESCE_MS = 300;
68031
68198
  function historyUrl(projectId, path = "") {
68032
68199
  return `/api/projects/${encodeURIComponent(projectId)}/history${path}`;
@@ -68045,10 +68212,11 @@ async function post(url, body, headers = {}) {
68045
68212
  return { ok: false, status: response.status, error: reply?.error ?? `HTTP ${response.status}` };
68046
68213
  }
68047
68214
  function claimHeld(reply, label) {
68048
- if (reply.ok) return Boolean(reply.body?.claimed);
68215
+ if (reply.ok)
68216
+ return reply.body?.claimed?.id ?? null;
68049
68217
  if (reply.status !== 404)
68050
68218
  console.error(`"${label}" was not recorded as your edit: ${reply.error}`);
68051
- return false;
68219
+ return null;
68052
68220
  }
68053
68221
  async function overwroteVersions(files) {
68054
68222
  const pairs = await Promise.all(
@@ -68086,38 +68254,47 @@ function usePersistentEditHistory({ projectId }) {
68086
68254
  const heldClaimRef = useRef124(null);
68087
68255
  const projectIdRef = useRef124(projectId);
68088
68256
  projectIdRef.current = projectId;
68257
+ const [own] = useState104(createOwnHistory);
68089
68258
  const refresh = useCallback97(async () => {
68090
68259
  if (!projectId) return;
68260
+ const seen = own.changes();
68091
68261
  const response = await fetch(historyUrl(projectId)).catch(() => null);
68092
68262
  const next = response?.ok ? await response.json() : EMPTY3;
68093
- if (projectIdRef.current === projectId) setView(next);
68094
- }, [projectId]);
68263
+ if (projectIdRef.current !== projectId) return;
68264
+ setView(next);
68265
+ own.offered(seen, next);
68266
+ }, [projectId, own]);
68095
68267
  useEffect105(() => {
68096
68268
  setView(EMPTY3);
68097
68269
  setLoaded(false);
68098
68270
  heldClaimRef.current = null;
68271
+ own.clear();
68099
68272
  void refresh().finally(() => setLoaded(true));
68100
- }, [refresh]);
68273
+ }, [refresh, own]);
68101
68274
  const recordEdit = useCallback97(
68102
68275
  async ({ label, coalesceKey, coalesceMs, files }) => {
68103
68276
  if (!projectId) return;
68104
68277
  const paths = Object.keys(files);
68278
+ own.overtake();
68105
68279
  const reply = await post(historyUrl(projectId, "/claim"), {
68106
68280
  label,
68107
68281
  paths,
68108
68282
  overwrote: await overwroteVersions(files),
68109
68283
  ...coalesceKey && { coalesceKey, idleMs: coalesceMs ?? DEFAULT_COALESCE_MS }
68110
68284
  });
68111
- heldClaimRef.current = claimHeld(reply, label) && coalesceKey ? { paths, at: Date.now() } : null;
68285
+ const claimed = claimHeld(reply, label);
68286
+ if (claimed) own.remember(claimed, files);
68287
+ heldClaimRef.current = claimed && coalesceKey ? { paths, at: Date.now() } : null;
68112
68288
  void refresh();
68113
68289
  },
68114
- [projectId, refresh]
68290
+ [projectId, refresh, own]
68115
68291
  );
68116
68292
  const step = useCallback97(
68117
68293
  async (direction, callbacks) => {
68118
68294
  if (!projectId) return { ok: false, reason: "empty" };
68119
68295
  const next = direction === "undo" ? view.back : view.forward;
68120
68296
  const paths = [.../* @__PURE__ */ new Set([...next?.paths ?? [], ...heldClaimRef.current?.paths ?? []])];
68297
+ own.overtake();
68121
68298
  const run = async () => {
68122
68299
  const previous = await readAll(paths, callbacks.readFile);
68123
68300
  const posted = await post(
@@ -68135,18 +68312,28 @@ function usePersistentEditHistory({ projectId }) {
68135
68312
  return { ok: false, reason: "content-mismatch", paths: files };
68136
68313
  }
68137
68314
  if (!reply.entry) return { ok: false, reason: "empty" };
68315
+ own.stepped(reply.entry);
68138
68316
  const changed = reply.entry.files.map((file) => file.path);
68139
68317
  return {
68140
68318
  ok: true,
68141
68319
  label: reply.entry.label,
68320
+ undoes: reply.entry.undoes,
68142
68321
  paths: changed,
68143
- files: await restoredFiles(changed, previous, callbacks.readFile)
68322
+ files: await restoredFiles(
68323
+ changed,
68324
+ { ...own.afterOf(reply.entry.undoes), ...previous },
68325
+ callbacks.readFile
68326
+ )
68144
68327
  };
68145
68328
  };
68146
68329
  return callbacks.serialize ? callbacks.serialize(paths, run) : run();
68147
68330
  },
68148
- [projectId, view, refresh]
68331
+ [projectId, view, refresh, own]
68149
68332
  );
68333
+ const noteOutsideChange = useCallback97(() => {
68334
+ own.overtake();
68335
+ void refresh();
68336
+ }, [own, refresh]);
68150
68337
  const undo = useCallback97((callbacks) => step("undo", callbacks), [step]);
68151
68338
  const redo = useCallback97((callbacks) => step("redo", callbacks), [step]);
68152
68339
  const state = useMemo65(() => {
@@ -68168,7 +68355,9 @@ function usePersistentEditHistory({ projectId }) {
68168
68355
  state,
68169
68356
  recordEdit,
68170
68357
  undo,
68171
- redo
68358
+ redo,
68359
+ predict: own.predict,
68360
+ noteOutsideChange
68172
68361
  };
68173
68362
  }
68174
68363
 
@@ -68934,6 +69123,7 @@ function useFileManager({
68934
69123
  import { useCallback as useCallback103, useRef as useRef129, useState as useState107 } from "react";
68935
69124
 
68936
69125
  // src/utils/gsapUndoRestore.ts
69126
+ import { isCompositionTemplate as isCompositionTemplate2 } from "@hyperframes/parsers/hf-ids";
68937
69127
  function elementIdentityKey(el) {
68938
69128
  const hfId = el.getAttribute("data-hf-id");
68939
69129
  if (hfId) return `hf:${hfId}`;
@@ -68942,9 +69132,9 @@ function elementIdentityKey(el) {
68942
69132
  return null;
68943
69133
  }
68944
69134
  var IDENTITY_SELECTOR = "[id], [data-hf-id]";
68945
- function identityElementMap(doc) {
69135
+ function identityElementMap(root) {
68946
69136
  const map = /* @__PURE__ */ new Map();
68947
- for (const el of doc.querySelectorAll(IDENTITY_SELECTOR)) {
69137
+ for (const el of root.querySelectorAll(IDENTITY_SELECTOR)) {
68948
69138
  const key2 = elementIdentityKey(el);
68949
69139
  if (!key2) continue;
68950
69140
  if (map.has(key2)) return null;
@@ -68952,8 +69142,18 @@ function identityElementMap(doc) {
68952
69142
  }
68953
69143
  return map;
68954
69144
  }
68955
- function normalizeSoftResidual(doc) {
68956
- for (const el of doc.querySelectorAll(IDENTITY_SELECTOR)) {
69145
+ function parseRestoreSource(html2) {
69146
+ const doc = new DOMParser().parseFromString(html2, "text/html");
69147
+ for (const template of Array.from(doc.querySelectorAll("template")).filter(
69148
+ isCompositionTemplate2
69149
+ )) {
69150
+ template.replaceWith(template.content);
69151
+ }
69152
+ return doc;
69153
+ }
69154
+ function normalizeSoftResidual(root) {
69155
+ const self = root.matches(IDENTITY_SELECTOR) ? [root] : [];
69156
+ for (const el of [...self, ...root.querySelectorAll(IDENTITY_SELECTOR)]) {
68957
69157
  const id = el.getAttribute("id");
68958
69158
  const hfId = el.getAttribute("data-hf-id");
68959
69159
  for (const name of [...el.getAttributeNames()]) {
@@ -68962,7 +69162,7 @@ function normalizeSoftResidual(doc) {
68962
69162
  if (id) el.setAttribute("id", id);
68963
69163
  if (hfId) el.setAttribute("data-hf-id", hfId);
68964
69164
  }
68965
- for (const script of findGsapScriptElements(doc)) script.textContent = "";
69165
+ for (const script of findGsapScriptElements(root)) script.textContent = "";
68966
69166
  }
68967
69167
  function attributesEqual(a, b) {
68968
69168
  const aNames = a.getAttributeNames();
@@ -68972,15 +69172,7 @@ function attributesEqual(a, b) {
68972
69172
  }
68973
69173
  return true;
68974
69174
  }
68975
- function diffSoftReloadableRestore(previous, restored) {
68976
- let prevDoc;
68977
- let nextDoc;
68978
- try {
68979
- prevDoc = new DOMParser().parseFromString(previous, "text/html");
68980
- nextDoc = new DOMParser().parseFromString(restored, "text/html");
68981
- } catch {
68982
- return null;
68983
- }
69175
+ function diffRestoreDocs(prevDoc, nextDoc) {
68984
69176
  const prevByKey = identityElementMap(prevDoc);
68985
69177
  const nextByKey = identityElementMap(nextDoc);
68986
69178
  if (!prevByKey || !nextByKey) return null;
@@ -68991,10 +69183,12 @@ function diffSoftReloadableRestore(previous, restored) {
68991
69183
  if (!prevEl || prevEl.tagName !== nextEl.tagName) return null;
68992
69184
  if (!attributesEqual(prevEl, nextEl)) changedElementKeys.push(key2);
68993
69185
  }
68994
- normalizeSoftResidual(prevDoc);
68995
- normalizeSoftResidual(nextDoc);
68996
- if (prevDoc.documentElement.outerHTML !== nextDoc.documentElement.outerHTML) return null;
68997
- return { changedElementKeys };
69186
+ const [prevRest, nextRest] = [prevDoc, nextDoc].map((doc) => {
69187
+ const root = doc.documentElement.cloneNode(true);
69188
+ normalizeSoftResidual(root);
69189
+ return root.outerHTML;
69190
+ });
69191
+ return prevRest === nextRest ? changedElementKeys : null;
68998
69192
  }
68999
69193
  function syncElementAttributes(target, source) {
69000
69194
  for (const name of [...target.getAttributeNames()]) {
@@ -69013,16 +69207,84 @@ function syncStaleEditMarks(doc, restored) {
69013
69207
  }
69014
69208
  }
69015
69209
  }
69016
- function readGsapScriptTexts(html2) {
69017
- const doc = new DOMParser().parseFromString(html2, "text/html");
69018
- return findGsapScriptElements(doc).map((script) => script.textContent ?? "");
69019
- }
69020
- function hasAmbiguousGsapScriptChange(previous, restored) {
69021
- const previousScripts = readGsapScriptTexts(previous);
69022
- const restoredScripts = readGsapScriptTexts(restored);
69210
+ function hasAmbiguousGsapScriptChange(previousScripts, restoredScripts) {
69023
69211
  if (previousScripts.length <= 1 && restoredScripts.length <= 1) return false;
69024
69212
  return previousScripts.length !== restoredScripts.length || previousScripts.some((script, index) => script !== restoredScripts[index]);
69025
69213
  }
69214
+ function restoreScripted(prevDoc, nextDoc, isActive) {
69215
+ const [before, after] = [prevDoc, nextDoc].map(
69216
+ (doc) => findGsapScriptElements(doc).map((script) => script.textContent ?? "")
69217
+ );
69218
+ const scripted = before.length > 0 || after.length > 0;
69219
+ if (scripted && !isActive) return null;
69220
+ return hasAmbiguousGsapScriptChange(before, after) ? null : scripted;
69221
+ }
69222
+ function liveScopes(doc, path, isActive) {
69223
+ if (isActive) return [doc];
69224
+ return Array.from(doc.querySelectorAll(`[data-composition-file="${CSS.escape(path)}"]`));
69225
+ }
69226
+ function scopeTargets(scope, keys, restoredByKey) {
69227
+ const liveByKey = identityElementMap(scope);
69228
+ if (!liveByKey) return null;
69229
+ const targets = [];
69230
+ for (const key2 of keys) {
69231
+ const live = liveByKey.get(key2);
69232
+ const restored = restoredByKey.get(key2);
69233
+ if (!live || !restored || live.hasAttribute("data-hf-inner-root")) return null;
69234
+ targets.push({ live, restored });
69235
+ }
69236
+ return targets;
69237
+ }
69238
+ function fileTargets(doc, path, isActive, file) {
69239
+ const prevDoc = parseRestoreSource(file.previous);
69240
+ const nextDoc = parseRestoreSource(file.restored);
69241
+ const scripted = restoreScripted(prevDoc, nextDoc, isActive);
69242
+ if (scripted === null) return null;
69243
+ const keys = diffRestoreDocs(prevDoc, nextDoc);
69244
+ const restoredByKey = identityElementMap(nextDoc);
69245
+ if (!keys || !restoredByKey) return null;
69246
+ const found = liveScopes(doc, path, isActive).map(
69247
+ (scope) => scopeTargets(scope, keys, restoredByKey)
69248
+ );
69249
+ if (!found.length || found.includes(null)) return null;
69250
+ return { targets: found.flat(), scripted };
69251
+ }
69252
+ function planRestoreTargets(doc, activeDocPath, files) {
69253
+ const plan = { targets: [], scripted: false };
69254
+ for (const [path, file] of Object.entries(files)) {
69255
+ const found = fileTargets(doc, path, path === activeDocPath, file);
69256
+ if (!found) return null;
69257
+ plan.targets.push(...found.targets);
69258
+ plan.scripted ||= found.scripted;
69259
+ }
69260
+ return plan;
69261
+ }
69262
+ function finalizeInPlace(iframe, win, currentTime) {
69263
+ if (applySoftReloadFinalization(iframe, currentTime)) return true;
69264
+ try {
69265
+ win.__player?.seek?.(currentTime);
69266
+ win.__hfStudioManualEditsApply?.();
69267
+ return true;
69268
+ } catch {
69269
+ return false;
69270
+ }
69271
+ }
69272
+ function showRestoreInPlace(iframe, activeCompPath, files, currentTime) {
69273
+ const doc = iframe?.contentDocument;
69274
+ const win = iframe?.contentWindow;
69275
+ const plan = doc && win ? planRestoreTargets(doc, activeCompPath ?? "index.html", files) : null;
69276
+ if (!iframe || !win || !plan || plan.scripted) return null;
69277
+ const before = plan.targets.map(({ live }) => [live, live.cloneNode(false)]);
69278
+ for (const { live, restored } of plan.targets) syncElementAttributes(live, restored);
69279
+ const putBack = (time) => {
69280
+ if (iframe.contentDocument !== doc) return false;
69281
+ for (const [live, attributes] of before) syncElementAttributes(live, attributes);
69282
+ return finalizeInPlace(iframe, win, time);
69283
+ };
69284
+ if (finalizeInPlace(iframe, win, currentTime)) return putBack;
69285
+ putBack(currentTime);
69286
+ return null;
69287
+ }
69026
69288
  function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, reload) {
69027
69289
  const activeDocPath = activeCompPath ?? "index.html";
69028
69290
  const paths = files ? Object.keys(files) : [];
@@ -69030,51 +69292,23 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
69030
69292
  markScenesStale(iframe, paths);
69031
69293
  reload();
69032
69294
  };
69033
- if (!iframe || !files || paths.length !== 1 || paths[0] !== activeDocPath) {
69034
- reloadPreview();
69035
- return "full";
69036
- }
69037
- const doc = iframe.contentDocument;
69038
- const win = iframe.contentWindow;
69039
- if (!doc || !win) {
69040
- reloadPreview();
69041
- return "full";
69042
- }
69043
- const { previous, restored } = files[activeDocPath];
69044
- const diff = diffSoftReloadableRestore(previous, restored);
69045
- if (!diff) {
69046
- reloadPreview();
69047
- return "full";
69048
- }
69049
- if (hasAmbiguousGsapScriptChange(previous, restored)) {
69050
- reloadPreview();
69051
- return "full";
69052
- }
69053
- const liveByKey = identityElementMap(doc);
69054
- const restoredByKey = identityElementMap(new DOMParser().parseFromString(restored, "text/html"));
69055
- if (!liveByKey || !restoredByKey) {
69295
+ const doc = iframe?.contentDocument;
69296
+ const win = iframe?.contentWindow;
69297
+ const plan = doc && files && win ? planRestoreTargets(doc, activeDocPath, files) : null;
69298
+ if (!iframe || !doc || !files || !win || !plan) {
69056
69299
  reloadPreview();
69057
69300
  return "full";
69058
69301
  }
69059
- const changedTargets = [];
69060
- for (const key2 of diff.changedElementKeys) {
69061
- const liveEl = liveByKey.get(key2);
69062
- const restoredEl = restoredByKey.get(key2);
69063
- if (!liveEl || !restoredEl) {
69064
- reloadPreview();
69065
- return "full";
69066
- }
69067
- changedTargets.push({ live: liveEl, restored: restoredEl });
69068
- }
69069
- for (const target of changedTargets) syncElementAttributes(target.live, target.restored);
69070
- const restoredScript = extractGsapScriptText(restored);
69071
- const previousScript = extractGsapScriptText(previous);
69302
+ for (const target of plan.targets) syncElementAttributes(target.live, target.restored);
69303
+ const active = files[activeDocPath];
69304
+ const restoredScript = active ? extractGsapScriptText(active.restored) : null;
69305
+ const previousScript = active ? extractGsapScriptText(active.previous) : null;
69072
69306
  if (restoredScript && restoredScript !== previousScript) {
69073
- syncStaleEditMarks(doc, restored);
69307
+ syncStaleEditMarks(doc, active.restored);
69074
69308
  const result = applySoftReload(iframe, restoredScript, {
69075
69309
  onAsyncFailure: reloadPreview,
69076
69310
  currentTimeOverride: currentTime,
69077
- authoredHtml: restored
69311
+ authoredHtml: active.restored
69078
69312
  });
69079
69313
  if (result === "cannot-soft-reload") {
69080
69314
  reloadPreview();
@@ -69082,15 +69316,9 @@ function applyUndoRestoreToPreview(iframe, activeCompPath, files, currentTime, r
69082
69316
  }
69083
69317
  return "soft";
69084
69318
  }
69085
- if (applySoftReloadFinalization(iframe, currentTime)) return "soft";
69086
- try {
69087
- win.__player?.seek?.(currentTime);
69088
- win.__hfStudioManualEditsApply?.();
69089
- } catch {
69090
- reloadPreview();
69091
- return "full";
69092
- }
69093
- return "soft";
69319
+ if (finalizeInPlace(iframe, win, currentTime)) return "soft";
69320
+ reloadPreview();
69321
+ return "full";
69094
69322
  }
69095
69323
 
69096
69324
  // src/player/lib/automationStoreSync.ts
@@ -69274,6 +69502,20 @@ function usePreviewPersistence({
69274
69502
  },
69275
69503
  [previewIframeRef, activeCompPathRef, reloadPreview]
69276
69504
  );
69505
+ const showHistoryRestoreNow = useCallback103(
69506
+ (files) => {
69507
+ if (!domEditSaveQueueRef.current?.isIdle() || hasStudioPendingEdits()) return null;
69508
+ const iframe = previewIframeRef.current;
69509
+ const now2 = () => usePlayerStore.getState().currentTime;
69510
+ const putBack = showRestoreInPlace(iframe, activeCompPathRef.current, files, now2());
69511
+ if (!putBack) return null;
69512
+ return () => {
69513
+ if (putBack(now2())) syncStoredAutomationFromPreview(iframe?.contentDocument ?? null);
69514
+ else void syncHistoryPreviewAfterApply({ paths: Object.keys(files) });
69515
+ };
69516
+ },
69517
+ [previewIframeRef, activeCompPathRef, syncHistoryPreviewAfterApply]
69518
+ );
69277
69519
  useMountEffect(() => {
69278
69520
  void clearLegacyStudioMotionFile(_readOptionalProjectFile, _writeProjectFile);
69279
69521
  });
@@ -69288,7 +69530,8 @@ function usePreviewPersistence({
69288
69530
  resetDomEditSaveQueueBreaker,
69289
69531
  applyCurrentStudioManualEditsToPreview,
69290
69532
  applyStudioManualEditsToPreview,
69291
- syncHistoryPreviewAfterApply
69533
+ syncHistoryPreviewAfterApply,
69534
+ showHistoryRestoreNow
69292
69535
  };
69293
69536
  }
69294
69537
 
@@ -73716,10 +73959,6 @@ function canCommitInlineTextSelection(selection, element) {
73716
73959
  if (selection.isCompositionHost || selection.isInsideLockedComposition) return false;
73717
73960
  return canEditElementTextInline(element);
73718
73961
  }
73719
- function ownsCurrentPreviewElement(selection, element, document2) {
73720
- if (!document2 || !element.isConnected) return false;
73721
- return element === selection.element && element.ownerDocument === document2;
73722
- }
73723
73962
  async function resyncDomTextSelectionFromPreview(doc, selection, activeCompPath, buildDomSelectionFromTarget, applyDomSelection) {
73724
73963
  if (!doc) return;
73725
73964
  const refreshed = findElementForSelection(doc, selection, activeCompPath);
@@ -73742,6 +73981,8 @@ function useDomEditTextCommits({
73742
73981
  }) {
73743
73982
  const latestReadOnlyPreviewRef = useRef142(readOnlyPreview);
73744
73983
  latestReadOnlyPreviewRef.current = readOnlyPreview;
73984
+ const latestSelectionRef = useRef142(domEditSelection);
73985
+ latestSelectionRef.current = domEditSelection;
73745
73986
  const domTextCommitVersionRef = useRef142(/* @__PURE__ */ new Map());
73746
73987
  const domStyleCommitVersionRef = useRef142(/* @__PURE__ */ new Map());
73747
73988
  const {
@@ -73853,18 +74094,32 @@ function useDomEditTextCommits({
73853
74094
  );
73854
74095
  const handleDomRichTextCommit = useCallback123(
73855
74096
  async ({ element, html: html2, previousHtml }) => {
73856
- if (!domEditSelection) return;
73857
- if (latestReadOnlyPreviewRef.current) {
74097
+ const putBack = () => {
73858
74098
  if (element.isConnected && element.innerHTML === html2) element.innerHTML = previousHtml;
73859
- return;
74099
+ };
74100
+ if (latestReadOnlyPreviewRef.current) return putBack();
74101
+ const refuse = (reason) => {
74102
+ console.error("[Studio] text edit not saved:", reason, element);
74103
+ showToast(`Couldn't save the text edit: ${reason}`, "error");
74104
+ putBack();
74105
+ };
74106
+ const doc = previewIframeRef.current?.contentDocument;
74107
+ if (!doc || !element.isConnected || element.ownerDocument !== doc) {
74108
+ return refuse("the text's element is gone from the preview");
74109
+ }
74110
+ const selection = await buildDomSelectionFromTarget(element, {
74111
+ exactTarget: true,
74112
+ skipSourceProbe: true
74113
+ });
74114
+ if (selection?.element !== element) {
74115
+ return refuse("this text was not found in the composition's source");
74116
+ }
74117
+ if (!canCommitInlineTextSelection(selection, element)) {
74118
+ return refuse("this text can't be edited in place");
73860
74119
  }
73861
- if (!canCommitInlineTextSelection(domEditSelection, element)) return;
73862
- const iframe = previewIframeRef.current;
73863
- const doc = iframe?.contentDocument;
73864
- if (!ownsCurrentPreviewElement(domEditSelection, element, doc)) return;
73865
74120
  const isLatestTextCommit = bumpDomEditCommitMapVersion(
73866
74121
  domTextCommitVersionRef.current,
73867
- getDomEditTargetKey(domEditSelection)
74122
+ getDomEditTargetKey(selection)
73868
74123
  );
73869
74124
  const operations = [buildDomEditRichTextPatchOperation(html2)];
73870
74125
  let appliedHtml = "";
@@ -73876,7 +74131,7 @@ function useDomEditTextCommits({
73876
74131
  appliedHtml = element.innerHTML;
73877
74132
  },
73878
74133
  persist: async () => {
73879
- await persistDomEditOperations(domEditSelection, operations, {
74134
+ await persistDomEditOperations(selection, operations, {
73880
74135
  label: "Edit text",
73881
74136
  skipRefresh: true,
73882
74137
  shouldSave: isLatestTextCommit
@@ -73888,11 +74143,12 @@ function useDomEditTextCommits({
73888
74143
  element.innerHTML = previousHtml;
73889
74144
  }
73890
74145
  },
73891
- onError: (error) => reportDomEditPersistFailure(domEditSelection, operations, error, showToast),
73892
- shouldResync: isLatestTextCommit,
74146
+ onError: (error) => reportDomEditPersistFailure(selection, operations, error, showToast),
74147
+ // Re-select only what is still selected: the selection may have moved on, or a host cleared it.
74148
+ shouldResync: () => isLatestTextCommit() && latestSelectionRef.current?.element === element,
73893
74149
  resync: () => resyncDomTextSelectionFromPreview(
73894
74150
  doc,
73895
- domEditSelection,
74151
+ selection,
73896
74152
  activeCompPath,
73897
74153
  buildDomSelectionFromTarget,
73898
74154
  applyDomSelection
@@ -73904,7 +74160,6 @@ function useDomEditTextCommits({
73904
74160
  activeCompPath,
73905
74161
  applyDomSelection,
73906
74162
  buildDomSelectionFromTarget,
73907
- domEditSelection,
73908
74163
  persistDomEditOperations,
73909
74164
  previewIframeRef,
73910
74165
  showToast
@@ -74122,9 +74377,29 @@ function settleGsapDraftAtGestureStart(el) {
74122
74377
  const y = Number.parseFloat(el.getAttribute("data-hf-drag-gsap-base-y") ?? "");
74123
74378
  if (gsap3 && Number.isFinite(x) && Number.isFinite(y)) gsap3.set(el, { x, y });
74124
74379
  }
74380
+ var plainMoveCounter = 0;
74381
+ function stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey) {
74382
+ const style = selection.element.style;
74383
+ const before = style.getPropertyValue("translate");
74384
+ const patch = translatePatch(next);
74385
+ style.setProperty("translate", patch.value);
74386
+ const rollback = () => {
74387
+ if (style.getPropertyValue("translate") === patch.value) style.setProperty("translate", before);
74388
+ };
74389
+ const key2 = coalesceKey ?? `move:${++plainMoveCounter}`;
74390
+ const options = { label: "Move layer", coalesceKey: key2, coalesceMs: Number.POSITIVE_INFINITY };
74391
+ const save = () => commitPositionPatchToHtml(selection, [patch], options).catch((error) => {
74392
+ rollback();
74393
+ throw error;
74394
+ });
74395
+ return { save, rollback };
74396
+ }
74125
74397
  function stageElementOffset({ commitPositionPatchToHtml, showToast, readOnlyPreview }, selection, next, coalesceKey) {
74126
74398
  const el = selection.element;
74127
74399
  if (readOnlyPreview) return { save: () => Promise.resolve(), rollback: () => void 0 };
74400
+ if (!gsapWritesPosition(el)) {
74401
+ return stagePlainTranslate(commitPositionPatchToHtml, selection, next, coalesceKey);
74402
+ }
74128
74403
  const previous = { position: el.style.position, left: el.style.left, top: el.style.top };
74129
74404
  const liftMarker = el.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
74130
74405
  const result = applyElementPositionOffset(el, next);
@@ -77602,6 +77877,8 @@ function useGsapAwareEditing({
77602
77877
  );
77603
77878
  const handleGsapAwarePathOffsetCommit = useCallback138(
77604
77879
  async (selection, next, modifiers) => {
77880
+ if (!gsapWritesPosition(selection.element))
77881
+ return stageElementPositionOffset(selection, next).save();
77605
77882
  if (gsapCommitMutation) {
77606
77883
  try {
77607
77884
  const ownedAnimations = getGsapAnimationsForSelection(selection);
@@ -77667,6 +77944,7 @@ function useGsapAwareEditing({
77667
77944
  const offsetMembers = /* @__PURE__ */ new Set();
77668
77945
  const preflightResults = await Promise.allSettled(
77669
77946
  updates.map(async ({ selection }) => {
77947
+ if (!gsapWritesPosition(selection.element)) return void offsetMembers.add(selection);
77670
77948
  const animations = await makeFetchFallback(selection, { failOnFetchError: true })();
77671
77949
  preflightAnimations.set(selection, animations);
77672
77950
  const outcome = await tryGsapDragIntercept(
@@ -79519,14 +79797,18 @@ function useStudioExternalFileChanges({
79519
79797
  fileManager,
79520
79798
  previewPersistence,
79521
79799
  pendingTimelineEditPathRef,
79522
- reloadPreview
79800
+ reloadPreview,
79801
+ onOutsideChange
79523
79802
  }) {
79524
79803
  const { flushPendingSourceSave, discardPendingSourceSave } = fileManager;
79525
79804
  const { drainPendingDomEditSaves, resetDomEditSaveQueueBreaker } = previewPersistence;
79526
79805
  const bumpThumbnailRevisions = usePlayerStore((state) => state.bumpThumbnailRevisions);
79527
79806
  const onAcceptedPersistedFileChange = useCallback143(
79528
- (_path, affectedCompositions) => bumpThumbnailRevisions(affectedCompositions),
79529
- [bumpThumbnailRevisions]
79807
+ (_path, affectedCompositions) => {
79808
+ onOutsideChange?.();
79809
+ bumpThumbnailRevisions(affectedCompositions);
79810
+ },
79811
+ [bumpThumbnailRevisions, onOutsideChange]
79530
79812
  );
79531
79813
  const drainPendingChanges = useCallback143(async () => {
79532
79814
  const source = await flushPendingSourceSave();
@@ -80093,6 +80375,7 @@ function useEditHistoryActions({
80093
80375
  writeProjectFile,
80094
80376
  showToast,
80095
80377
  syncHistoryPreviewAfterApply,
80378
+ showHistoryRestoreNow,
80096
80379
  waitForPendingDomEditSaves,
80097
80380
  onAfterUndoRedo,
80098
80381
  activeCompPath,
@@ -80109,11 +80392,20 @@ function useEditHistoryActions({
80109
80392
  const apply = useCallback145(
80110
80393
  async (direction) => {
80111
80394
  const noun = direction === "undo" ? "Undo" : "Redo";
80112
- await waitForPendingDomEditSaves();
80113
- const result = await editHistory[direction]({
80114
- readFile: readHistoryFile,
80115
- serialize: serializeHistoryFiles
80116
- });
80395
+ const predicted = editHistory.predict?.(direction) ?? null;
80396
+ const putBack = predicted ? showHistoryRestoreNow?.(predicted.files) ?? null : null;
80397
+ let result = { ok: false, reason: "failed" };
80398
+ let serverSteppedShown = false;
80399
+ try {
80400
+ await waitForPendingDomEditSaves();
80401
+ result = await editHistory[direction]({
80402
+ readFile: readHistoryFile,
80403
+ serialize: serializeHistoryFiles
80404
+ });
80405
+ serverSteppedShown = Boolean(putBack && result.ok && result.label) && result.undoes === predicted?.id;
80406
+ } finally {
80407
+ if (putBack && !serverSteppedShown) putBack();
80408
+ }
80117
80409
  if (!result.ok && result.reason === "content-mismatch") {
80118
80410
  showToast(
80119
80411
  `Can't ${direction}: ${result.paths?.join(", ")} changed since that edit.`,
@@ -80126,7 +80418,8 @@ function useEditHistoryActions({
80126
80418
  return;
80127
80419
  }
80128
80420
  if (result.ok && result.label) {
80129
- const restore = { paths: result.paths, files: result.files };
80421
+ const files = serverSteppedShown ? fromShown(result.files, predicted.files) : result.files;
80422
+ const restore = { paths: result.paths, files };
80130
80423
  onAfterUndoRedo?.(restore);
80131
80424
  if (activeCompPath && result.paths?.includes(activeCompPath)) {
80132
80425
  forceReloadSdkSession?.();
@@ -80140,6 +80433,7 @@ function useEditHistoryActions({
80140
80433
  readHistoryFile,
80141
80434
  showToast,
80142
80435
  syncHistoryPreviewAfterApply,
80436
+ showHistoryRestoreNow,
80143
80437
  waitForPendingDomEditSaves,
80144
80438
  serializeHistoryFiles,
80145
80439
  onAfterUndoRedo,
@@ -80151,6 +80445,15 @@ function useEditHistoryActions({
80151
80445
  const redo = useCallback145(() => apply("redo"), [apply]);
80152
80446
  return useMemo71(() => ({ undo, redo }), [undo, redo]);
80153
80447
  }
80448
+ function fromShown(files, shown2) {
80449
+ if (!files) return files;
80450
+ return Object.fromEntries(
80451
+ Object.entries(files).map(([path, f]) => [
80452
+ path,
80453
+ { previous: shown2[path]?.restored ?? f.previous, restored: f.restored }
80454
+ ])
80455
+ );
80456
+ }
80154
80457
 
80155
80458
  // src/hooks/useAppHotkeys.ts
80156
80459
  function iframeContentWindow(iframe) {
@@ -80166,9 +80469,9 @@ function safeAddListener(t, type, h, capture = false) {
80166
80469
  } catch {
80167
80470
  }
80168
80471
  }
80169
- function safeRemoveListener(t, type, h) {
80472
+ function safeRemoveListener(t, type, h, capture = false) {
80170
80473
  try {
80171
- t?.removeEventListener(type, h);
80474
+ t?.removeEventListener(type, h, capture);
80172
80475
  } catch {
80173
80476
  }
80174
80477
  }
@@ -80195,6 +80498,7 @@ function useAppHotkeys({
80195
80498
  writeProjectFile,
80196
80499
  showToast,
80197
80500
  syncHistoryPreviewAfterApply,
80501
+ showHistoryRestoreNow,
80198
80502
  waitForPendingDomEditSaves,
80199
80503
  handleCopy: handleCopy2,
80200
80504
  handlePaste: handlePaste2,
@@ -80218,6 +80522,7 @@ function useAppHotkeys({
80218
80522
  writeProjectFile,
80219
80523
  showToast,
80220
80524
  syncHistoryPreviewAfterApply,
80525
+ showHistoryRestoreNow,
80221
80526
  waitForPendingDomEditSaves,
80222
80527
  onAfterUndoRedo,
80223
80528
  activeCompPath,
@@ -80274,38 +80579,17 @@ function useAppHotkeys({
80274
80579
  window.addEventListener("keydown", handleAppKeyDown, true);
80275
80580
  return () => window.removeEventListener("keydown", handleAppKeyDown, true);
80276
80581
  }, [handleAppKeyDown]);
80277
- const handleHistoryHotkey = useCallback146((event) => {
80278
- if (!(event.metaKey || event.ctrlKey) || shouldIgnoreHistoryShortcut(event.target)) return;
80279
- handleUndoRedoKey(
80280
- event,
80281
- () => void cbRef.current.handleUndo(),
80282
- () => void cbRef.current.handleRedo()
80283
- );
80284
- }, []);
80285
80582
  const syncPreviewHotkeys = useCallback146(
80286
80583
  (iframe) => {
80287
80584
  previewHistoryCleanupRef.current?.();
80288
80585
  previewHistoryCleanupRef.current = null;
80289
80586
  const win = iframeContentWindow(iframe);
80290
- let doc = null;
80291
- try {
80292
- doc = iframe?.contentDocument ?? null;
80293
- } catch {
80294
- doc = null;
80295
- }
80296
- if (!win && !doc) return;
80297
- const handler = handleHistoryHotkey;
80587
+ if (!win) return;
80298
80588
  const appHandler = handleAppKeyDown;
80299
- safeAddListener(win, "keydown", handler, true);
80300
80589
  safeAddListener(win, "keydown", appHandler, true);
80301
- doc?.addEventListener("keydown", handleHistoryHotkey, true);
80302
- previewHistoryCleanupRef.current = () => {
80303
- safeRemoveListener(win, "keydown", handler);
80304
- safeRemoveListener(win, "keydown", appHandler);
80305
- doc?.removeEventListener("keydown", handleHistoryHotkey, true);
80306
- };
80590
+ previewHistoryCleanupRef.current = () => safeRemoveListener(win, "keydown", appHandler, true);
80307
80591
  },
80308
- [handleAppKeyDown, handleHistoryHotkey]
80592
+ [handleAppKeyDown]
80309
80593
  );
80310
80594
  useEffect121(
80311
80595
  () => () => {
@@ -91691,7 +91975,8 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
91691
91975
  fileManager,
91692
91976
  previewPersistence,
91693
91977
  pendingTimelineEditPathRef,
91694
- reloadPreview
91978
+ reloadPreview,
91979
+ onOutsideChange: editHistory.noteOutsideChange
91695
91980
  });
91696
91981
  const invalidateGsapCacheRef = useRef181(() => {
91697
91982
  });
@@ -91793,6 +92078,7 @@ function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason } = {}) {
91793
92078
  writeProjectFile: fileManager.writeProjectFile,
91794
92079
  showToast,
91795
92080
  syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
92081
+ showHistoryRestoreNow: previewPersistence.showHistoryRestoreNow,
91796
92082
  waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
91797
92083
  handleCopy: handleCopy2,
91798
92084
  handlePaste: handlePaste2,
@@ -92681,6 +92967,7 @@ export {
92681
92967
  ConnectedDomEditOverlay,
92682
92968
  ContextMenu,
92683
92969
  DEFAULT_SHORTCUT_SECTIONS,
92970
+ Dock,
92684
92971
  DomEditOverlay,
92685
92972
  DomEditProvider,
92686
92973
  EditorShell,
@@ -92776,6 +93063,7 @@ export {
92776
93063
  snapEngagedForTravel,
92777
93064
  useClipboard,
92778
93065
  useCompositionStack,
93066
+ useDockLayoutStore,
92779
93067
  useDomEditSelectionContext,
92780
93068
  useDomEditSession,
92781
93069
  useDomGeometryCommit,