@hyperframes/studio 0.8.120 → 0.8.122

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 (54) hide show
  1. package/dist/assets/{hyperframes-player-i4gqkhjz.js → hyperframes-player-DMzBHhbH.js} +1 -1
  2. package/dist/assets/{index-Ce3Lr-Gt.js → index-B-YDp8hN.js} +174 -174
  3. package/dist/assets/{index-uWOhRqxe.js → index-CBRX2eZX.js} +1 -1
  4. package/dist/assets/{index-C7LHDM18.js → index-lyjhSEzi.js} +1 -1
  5. package/dist/{chunk-J6Q572FA.js → chunk-246VB2G3.js} +3 -1
  6. package/dist/chunk-246VB2G3.js.map +1 -0
  7. package/dist/index.d.ts +10 -2
  8. package/dist/index.html +1 -1
  9. package/dist/index.js +416 -301
  10. package/dist/index.js.map +1 -1
  11. package/dist/player.js +1 -1
  12. package/package.json +8 -8
  13. package/src/components/TimelineToolbar.test.tsx +25 -0
  14. package/src/components/TimelineToolbar.tsx +5 -1
  15. package/src/components/editor/MotionPathNode.tsx +2 -1
  16. package/src/components/editor/MotionPathOverlay.layerPress.test.tsx +99 -0
  17. package/src/components/editor/MotionPathOverlay.tsx +6 -0
  18. package/src/components/editor/domEditOverlayStartGesture.blocked.test.ts +57 -0
  19. package/src/components/editor/domEditOverlayStartGesture.ts +1 -1
  20. package/src/components/editor/motionPathGeometry.test.ts +10 -0
  21. package/src/components/editor/motionPathGeometry.ts +8 -5
  22. package/src/components/editor/motionPathLayerNode.test.ts +82 -0
  23. package/src/components/editor/motionPathLayerNode.ts +28 -0
  24. package/src/components/editor/useDomEditNudge.test.tsx +53 -1
  25. package/src/components/editor/useDomEditNudge.ts +2 -2
  26. package/src/components/editor/useMotionPathData.test.tsx +30 -0
  27. package/src/components/editor/useMotionPathData.ts +3 -1
  28. package/src/hooks/gsapValueAtPlayhead.test.ts +46 -3
  29. package/src/hooks/gsapValueAtPlayhead.ts +12 -2
  30. package/src/hooks/linkShortcuts.test.ts +16 -0
  31. package/src/hooks/linkShortcuts.ts +2 -0
  32. package/src/hooks/useAppHotkeys.ts +1 -2
  33. package/src/index.ts +2 -0
  34. package/src/player/components/TimelineClipFades.tsx +1 -0
  35. package/src/player/components/timelineClipDragPreview.test.ts +68 -0
  36. package/src/player/components/timelineClipDragPreview.ts +39 -14
  37. package/src/player/components/timelineLayout.test.ts +24 -1
  38. package/src/player/components/timelineRulerGeometry.ts +24 -3
  39. package/src/player/components/timelineSnapping.test.ts +43 -12
  40. package/src/player/components/timelineSnapping.ts +33 -15
  41. package/src/player/components/useTimelineClipDrag.resize.test.tsx +54 -54
  42. package/src/player/components/useTimelineClipDrag.ts +17 -29
  43. package/src/player/components/useTimelineProviderState.tsx +0 -1
  44. package/src/player/components/useTimelineTicks.ts +6 -3
  45. package/src/player/hooks/timelinePlayerTestHarness.ts +2 -0
  46. package/src/player/hooks/useTimelinePlayer.seek.test.ts +66 -0
  47. package/src/player/hooks/useTimelinePlayer.ts +6 -2
  48. package/src/player/lib/time.ts +3 -0
  49. package/src/player/store/previewFrameStore.ts +48 -0
  50. package/src/player/store/timelineElement.ts +12 -0
  51. package/src/timelineChromeExports.test.tsx +6 -0
  52. package/src/utils/gsapSoftReload.test.ts +50 -0
  53. package/src/utils/gsapSoftReload.ts +22 -2
  54. package/dist/chunk-J6Q572FA.js.map +0 -1
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  Player,
9
9
  STUDIO_PREVIEW_FPS,
10
10
  applyPreviewVariablesToUrl,
11
+ clampToDuration,
11
12
  computePinnedZoomPercent,
12
13
  createTimelineResetState,
13
14
  effectiveThumbnailMode,
@@ -36,7 +37,7 @@ import {
36
37
  usePreviewVariablesStore,
37
38
  whenPreviewBooted,
38
39
  writeStudioUiPreferences
39
- } from "./chunk-J6Q572FA.js";
40
+ } from "./chunk-246VB2G3.js";
40
41
  import {
41
42
  Button,
42
43
  ContextMenu,
@@ -2542,6 +2543,11 @@ import { clampPlaybackRate } from "@hyperframes/parsers/media-duration";
2542
2543
 
2543
2544
  // src/player/store/timelineElement.ts
2544
2545
  var authoredOffset = (element) => element.authoredStartIsMasterTime ? 0 : element.parentCompositionStart ?? 0;
2546
+ function savedClipEdges(element, start, resizedDuration) {
2547
+ const savedStart = authoredOffset(element) + roundToCenti(toAuthoredStart(element, start));
2548
+ const duration = resizedDuration === void 0 ? element.duration : roundToCenti(resizedDuration);
2549
+ return { start: savedStart, end: savedStart + duration };
2550
+ }
2545
2551
  function clampToHostStart(element, masterTime) {
2546
2552
  return Math.max(Math.min(authoredOffset(element), element.start), masterTime);
2547
2553
  }
@@ -3405,7 +3411,8 @@ var TIMELINE_SNAP_PX = 8;
3405
3411
  var TYPE_PRIORITY = {
3406
3412
  playhead: 0,
3407
3413
  "clip-edge": 1,
3408
- beat: 2
3414
+ beat: 2,
3415
+ grid: 3
3409
3416
  };
3410
3417
  function collectTimelineSnapTargets(input) {
3411
3418
  const byTime = /* @__PURE__ */ new Map();
@@ -3423,11 +3430,18 @@ function collectTimelineSnapTargets(input) {
3423
3430
  add(el.start, "clip-edge");
3424
3431
  add(el.start + el.duration, "clip-edge");
3425
3432
  }
3426
- if (input.playheadTime != null && input.includePlayhead !== false)
3427
- add(input.playheadTime, "playhead");
3433
+ if (input.playheadTime != null) add(input.playheadTime, "playhead");
3428
3434
  return Array.from(byTime.values()).sort((a, b) => a.time - b.time);
3429
3435
  }
3430
- function snapTimelineTime(time, targets, thresholdSecs) {
3436
+ function nearestGridLine(time, gridStep, thresholdSecs) {
3437
+ if (!(gridStep > 0)) return null;
3438
+ const line = Math.round(time / gridStep) * gridStep;
3439
+ const saved2 = roundToCenti(line);
3440
+ const pixelsPerSecond = TIMELINE_SNAP_PX / thresholdSecs;
3441
+ if (Math.abs(saved2 - line) * pixelsPerSecond >= 1) return null;
3442
+ return Math.abs(saved2 - time) < thresholdSecs ? { time: saved2, type: "grid" } : null;
3443
+ }
3444
+ function snapTimelineTime(time, targets, thresholdSecs, gridStep = 0) {
3431
3445
  let best = null;
3432
3446
  let bestDist = thresholdSecs;
3433
3447
  for (const target of targets) {
@@ -3437,21 +3451,23 @@ function snapTimelineTime(time, targets, thresholdSecs) {
3437
3451
  best = target;
3438
3452
  }
3439
3453
  }
3454
+ best ??= nearestGridLine(time, gridStep, thresholdSecs);
3440
3455
  return best ? { time: best.time, target: best } : { time, target: null };
3441
3456
  }
3442
- function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2) {
3443
- if (targets.length === 0) return { start, snapTime: null, snapType: null };
3457
+ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDuration2, gridStep = 0) {
3458
+ if (targets.length === 0 && !(gridStep > 0)) return { start, snapTime: null, snapType: null };
3444
3459
  const thresholdSecs = TIMELINE_SNAP_PX / Math.max(pixelsPerSecond, 1);
3445
- const startSnap = snapTimelineTime(start, targets, thresholdSecs);
3446
- const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs);
3447
- const startMoved = startSnap.target !== null;
3448
- const endMoved = endSnap.target !== null;
3460
+ const startSnap = snapTimelineTime(start, targets, thresholdSecs, gridStep);
3461
+ const endSnap = snapTimelineTime(start + duration, targets, thresholdSecs, gridStep);
3462
+ const rank = (snap, edgeTime2) => snap.target === null ? Infinity : Math.abs(snap.time - edgeTime2) + (snap.target.type === "grid" ? thresholdSecs : 0);
3463
+ const startRank = rank(startSnap, start);
3464
+ const endRank = rank(endSnap, start + duration);
3449
3465
  let candidate = start;
3450
3466
  let target = null;
3451
- if (startMoved && (!endMoved || Math.abs(startSnap.time - start) <= Math.abs(endSnap.time - (start + duration)))) {
3467
+ if (startRank !== Infinity && startRank <= endRank) {
3452
3468
  candidate = startSnap.time;
3453
3469
  target = startSnap.target;
3454
- } else if (endMoved) {
3470
+ } else if (endRank !== Infinity) {
3455
3471
  candidate = endSnap.time - duration;
3456
3472
  target = endSnap.target;
3457
3473
  }
@@ -3692,6 +3708,165 @@ function useTimelineAssetDrop({
3692
3708
  // src/player/components/useTimelineClipDrag.ts
3693
3709
  import { useRef as useRef12, useState as useState7, useCallback as useCallback9, useMemo as useMemo7, useEffect as useEffect11 } from "react";
3694
3710
 
3711
+ // src/player/components/timelineRulerGeometry.ts
3712
+ function getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate) {
3713
+ const zoomIntervals = [
3714
+ 0.02,
3715
+ 0.05,
3716
+ 0.1,
3717
+ 0.2,
3718
+ 0.5,
3719
+ 1,
3720
+ 2,
3721
+ 5,
3722
+ 10,
3723
+ 15,
3724
+ 30,
3725
+ 60,
3726
+ 120,
3727
+ 300,
3728
+ 600,
3729
+ 900,
3730
+ 1800,
3731
+ 3600
3732
+ ];
3733
+ let interval;
3734
+ if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
3735
+ const targetMajorPx = 88;
3736
+ interval = zoomIntervals.find((candidate) => candidate * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 3600;
3737
+ } else {
3738
+ const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
3739
+ const target = duration / 6;
3740
+ interval = durationIntervals.find((candidate) => candidate >= target) ?? 60;
3741
+ }
3742
+ if (Number.isFinite(frameRate) && (frameRate ?? 0) > 0) {
3743
+ const fps = frameRate ?? 0;
3744
+ return Math.max(1, Math.ceil(interval * fps - 1e-6)) / fps;
3745
+ }
3746
+ return interval;
3747
+ }
3748
+ function getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate) {
3749
+ const pps = Number.isFinite(pixelsPerSecond) ? pixelsPerSecond ?? 0 : 0;
3750
+ if (pps <= 0) return 4;
3751
+ const fps = Number.isFinite(frameRate) ? frameRate ?? 0 : 0;
3752
+ const majorFrames = fps > 0 ? Math.round(majorInterval * fps) : 0;
3753
+ const candidates = fps > 0 ? [4, 5, 3, 2] : [4, 2];
3754
+ for (const parts of candidates) {
3755
+ if (fps > 0 && majorFrames % parts !== 0) continue;
3756
+ if (majorInterval / parts * pps >= 8) return parts;
3757
+ }
3758
+ return 0;
3759
+ }
3760
+ function roundTickValue(time) {
3761
+ return Math.round(time * 1e6) / 1e6;
3762
+ }
3763
+ function isSupportedTickDuration(duration) {
3764
+ return duration > 0 && Number.isFinite(duration) && duration <= 14400;
3765
+ }
3766
+ function getTickRange(duration, range) {
3767
+ return {
3768
+ start: Math.max(0, range?.start ?? 0),
3769
+ end: Math.min(duration, range?.end ?? duration)
3770
+ };
3771
+ }
3772
+ function appendMinorTicks(minor, majorTime, minorInterval, subdivisions, range, maxTicks, majorCount) {
3773
+ for (let part = 1; part < subdivisions && majorCount + minor.length < maxTicks; part++) {
3774
+ const time = majorTime + part * minorInterval;
3775
+ if (time >= range.start - 1e-3 && time <= range.end + 1e-3) {
3776
+ minor.push(roundTickValue(time));
3777
+ }
3778
+ }
3779
+ }
3780
+ function tickSpacing(duration, pixelsPerSecond, frameRate) {
3781
+ const majorInterval = getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate);
3782
+ const subdivisions = getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate);
3783
+ return { majorInterval, subdivisions };
3784
+ }
3785
+ function rulerFrameRate(mode) {
3786
+ return mode === "frame" ? STUDIO_PREVIEW_FPS : void 0;
3787
+ }
3788
+ function getTimelineGridStep(duration, pixelsPerSecond, frameRate) {
3789
+ if (!isSupportedTickDuration(duration)) return 0;
3790
+ const { majorInterval, subdivisions } = tickSpacing(duration, pixelsPerSecond, frameRate);
3791
+ return subdivisions > 0 ? majorInterval / subdivisions : majorInterval;
3792
+ }
3793
+ function generateTicks(duration, pixelsPerSecond, frameRate, range) {
3794
+ if (!isSupportedTickDuration(duration)) return { major: [], minor: [] };
3795
+ const { majorInterval, subdivisions } = tickSpacing(duration, pixelsPerSecond, frameRate);
3796
+ const minorInterval = subdivisions > 0 ? majorInterval / subdivisions : 0;
3797
+ const major = [];
3798
+ const minor = [];
3799
+ const maxTicks = 2e3;
3800
+ const tickRange = getTickRange(duration, range);
3801
+ const firstMajorIndex = Math.max(0, Math.floor(tickRange.start / majorInterval));
3802
+ for (let index = firstMajorIndex; major.length < maxTicks; index++) {
3803
+ const time = index * majorInterval;
3804
+ if (time > tickRange.end + 1e-3) break;
3805
+ if (time >= tickRange.start - 1e-3) major.push(roundTickValue(time));
3806
+ appendMinorTicks(minor, time, minorInterval, subdivisions, tickRange, maxTicks, major.length);
3807
+ }
3808
+ return { major, minor };
3809
+ }
3810
+ function formatTimelineTickLabel(time, duration, majorInterval) {
3811
+ if (!Number.isFinite(time)) return "00:00";
3812
+ const safeTime = Math.max(0, time);
3813
+ if (majorInterval < 0.1) {
3814
+ const totalHundredths = Math.round(safeTime * 100);
3815
+ const wholeSeconds = Math.floor(totalHundredths / 100);
3816
+ const hundredth = totalHundredths % 100;
3817
+ return `${formatTime(wholeSeconds)}.${hundredth.toString().padStart(2, "0")}`;
3818
+ }
3819
+ if (majorInterval < 1) {
3820
+ const totalTenths = Math.round(safeTime * 10);
3821
+ const wholeSeconds = Math.floor(totalTenths / 10);
3822
+ const tenth = totalTenths % 10;
3823
+ return `${formatTime(wholeSeconds)}.${tenth}`;
3824
+ }
3825
+ if (duration >= 3600 || safeTime >= 3600) {
3826
+ const totalSeconds = Math.floor(safeTime);
3827
+ const hours = Math.floor(totalSeconds / 3600);
3828
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
3829
+ const seconds = totalSeconds % 60;
3830
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
3831
+ }
3832
+ return formatTime(safeTime);
3833
+ }
3834
+
3835
+ // src/player/store/previewFrameStore.ts
3836
+ import { create } from "zustand";
3837
+ var usePreviewFrameStore = create(() => ({ time: null }));
3838
+ var setPreviewFrame = (time) => usePreviewFrameStore.setState({ time });
3839
+ function showingPreviewFrame(adapter) {
3840
+ let seeked = false;
3841
+ const time = () => seeked ? adapter.getTime() : usePlayerStore.getState().currentTime;
3842
+ return {
3843
+ play: () => {
3844
+ adapter.seek(time());
3845
+ adapter.play();
3846
+ },
3847
+ pause: () => adapter.pause(),
3848
+ seek: (t2, options) => {
3849
+ seeked = true;
3850
+ adapter.seek(t2, options);
3851
+ },
3852
+ getTime: time,
3853
+ getDuration: () => adapter.getDuration(),
3854
+ isPlaying: () => adapter.isPlaying()
3855
+ };
3856
+ }
3857
+ function transportAdapter(adapter) {
3858
+ const showing = usePreviewFrameStore.getState().time !== null;
3859
+ return adapter && showing && !usePlayerStore.getState().isPlaying ? showingPreviewFrame(adapter) : adapter;
3860
+ }
3861
+ function subscribePreviewFrame(getAdapter) {
3862
+ return usePreviewFrameStore.subscribe((state, prev) => {
3863
+ if (state.time === prev.time || usePlayerStore.getState().isPlaying) return;
3864
+ const adapter = getAdapter();
3865
+ const time = state.time ?? usePlayerStore.getState().currentTime;
3866
+ adapter?.seek(clampToDuration(time, adapter.getDuration()));
3867
+ });
3868
+ }
3869
+
3695
3870
  // src/player/components/timelineAuthoredMoveTarget.ts
3696
3871
  function canMoveTimelineElement(element) {
3697
3872
  return getTimelineEditCapabilities({
@@ -4019,7 +4194,8 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4019
4194
  pps,
4020
4195
  // Relaxed clamp: allow the snapped start past the content, up to the
4021
4196
  // rendered extent (see dragMaxStart) — the composition grows on commit.
4022
- dragMaxStart + drag.element.duration
4197
+ dragMaxStart + drag.element.duration,
4198
+ ctx3.gridStep
4023
4199
  );
4024
4200
  const group = resolveGroupDrag(drag, ctx3);
4025
4201
  const dragKey = drag.element.key ?? drag.element.id;
@@ -4037,6 +4213,8 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4037
4213
  return { ...drag, started: true };
4038
4214
  }
4039
4215
  const { track: previewTrack, insertRow } = placement;
4216
+ const target = placement.start === snap.start && snap.snapTime !== null && snap.snapType !== null ? { time: snap.snapTime, type: snap.snapType } : null;
4217
+ const guide = guideIfSaved(drag.element, { start: snap.start }, target, pps);
4040
4218
  return {
4041
4219
  ...drag,
4042
4220
  started: true,
@@ -4048,10 +4226,16 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
4048
4226
  // tell a deliberate vertical lane change from a horizontal drag.
4049
4227
  desiredTrack: nextMove.track,
4050
4228
  insertRow,
4051
- snapTime: placement.start === snap.start ? snap.snapTime : null,
4052
- snapType: placement.start === snap.start ? snap.snapType : null
4229
+ snapTime: guide?.time ?? null,
4230
+ snapType: guide?.type ?? null
4053
4231
  };
4054
4232
  }
4233
+ function guideIfSaved(element, clip, target, pps) {
4234
+ if (!target) return null;
4235
+ const saved2 = savedClipEdges(element, clip.start, clip.duration);
4236
+ const off = Math.min(Math.abs(saved2.start - target.time), Math.abs(saved2.end - target.time));
4237
+ return off * pps < 1 ? target : null;
4238
+ }
4055
4239
  var TRIM_END_FRAME_LEAD_S = 1 / STUDIO_PREVIEW_FPS;
4056
4240
  function trimPreviewTime(edge, start, duration) {
4057
4241
  return edge === "start" ? start : Math.max(start, start + duration - TRIM_END_FRAME_LEAD_S);
@@ -4085,28 +4269,33 @@ function computeResizePreview(resize2, clientX, ctx3) {
4085
4269
  );
4086
4270
  const trimTargets = buildSnapTargets(
4087
4271
  resize2.element.key ?? resize2.element.id,
4088
- !isMusicTrack(resize2.element),
4089
- false
4272
+ !isMusicTrack(resize2.element)
4090
4273
  );
4274
+ const gridStep = ctx3.gridStep ?? 0;
4091
4275
  let snap = null;
4092
- if (trimTargets.length > 0) {
4276
+ if (trimTargets.length > 0 || gridStep > 0) {
4093
4277
  const snapSecs = TIMELINE_SNAP_PX / Math.max(pps, 1);
4094
4278
  if (resize2.edge === "end") {
4095
4279
  const edgeTime2 = nextResize.start + nextResize.duration;
4096
- const { time: snapped, target } = snapTimelineTime(edgeTime2, trimTargets, snapSecs);
4280
+ const { time: snapped, target } = snapTimelineTime(edgeTime2, trimTargets, snapSecs, gridStep);
4097
4281
  const snappedDuration = Math.round((snapped - nextResize.start) * 1e3) / 1e3;
4098
4282
  if (target && snapped <= maxEnd + 1e-6 && snappedDuration >= resolveTimelineMinDuration() - 1e-6) {
4099
4283
  if (snapped !== edgeTime2) nextResize = { ...nextResize, duration: snappedDuration };
4100
- snap = target;
4284
+ snap = guideIfSaved(resize2.element, nextResize, target, pps);
4101
4285
  }
4102
4286
  } else {
4103
- const { time: snapped, target } = snapTimelineTime(nextResize.start, trimTargets, snapSecs);
4287
+ const { time: snapped, target } = snapTimelineTime(
4288
+ nextResize.start,
4289
+ trimTargets,
4290
+ snapSecs,
4291
+ gridStep
4292
+ );
4104
4293
  const clip = { ...nextResize, playbackRate: resize2.element.playbackRate };
4105
4294
  const delta = snapped - nextResize.start;
4106
4295
  const bounds = clipStartTrimDeltaBounds(clip, minStart, resolveTimelineMinDuration());
4107
4296
  if (target && delta >= bounds.minDelta - 1e-6 && delta <= bounds.maxDelta + 1e-6) {
4108
4297
  if (snapped !== nextResize.start) nextResize = applyClipStartTrimDelta(clip, delta);
4109
- snap = target;
4298
+ snap = guideIfSaved(resize2.element, nextResize, target, pps);
4110
4299
  }
4111
4300
  }
4112
4301
  }
@@ -4137,8 +4326,8 @@ function previewGroupResize(session, next, setResizeState) {
4137
4326
  }
4138
4327
 
4139
4328
  // src/utils/linkedClipPreferences.ts
4140
- import { create } from "zustand";
4141
- var useLinkedClipPreferences = create((set) => ({
4329
+ import { create as create2 } from "zustand";
4330
+ var useLinkedClipPreferences = create2((set) => ({
4142
4331
  linkedSelection: readStudioUiPreferences().linkedSelectionEnabled ?? true,
4143
4332
  setLinkedSelection: (enabled) => {
4144
4333
  writeStudioUiPreferences({ linkedSelectionEnabled: enabled });
@@ -9719,7 +9908,6 @@ function useTimelineClipDrag({
9719
9908
  onResizeElements,
9720
9909
  onBlockedEditAttempt,
9721
9910
  onLinkEdit,
9722
- onSeek,
9723
9911
  setShowPopover,
9724
9912
  setRangeSelectionRef,
9725
9913
  readZIndex,
@@ -9769,23 +9957,27 @@ function useTimelineClipDrag({
9769
9957
  const snapTargetsCacheRef = useRef12(/* @__PURE__ */ new Map());
9770
9958
  const dragAudioTracksRef = useRef12(null);
9771
9959
  const buildSnapTargets = useCallback9(
9772
- (excludeElementKey, includeBeats, includePlayhead = true) => {
9960
+ (excludeElementKey, includeBeats) => {
9773
9961
  if (!snapContextRef.current.enabled) return [];
9774
- const cacheKey = `${excludeElementKey ?? ""}|${includeBeats ? 1 : 0}|${includePlayhead ? 1 : 0}`;
9962
+ const cacheKey = `${excludeElementKey ?? ""}|${includeBeats ? 1 : 0}`;
9775
9963
  const cached5 = snapTargetsCacheRef.current.get(cacheKey);
9776
9964
  if (cached5) return cached5;
9777
9965
  const targets = collectTimelineSnapTargets({
9778
9966
  elements: elementsRef.current,
9779
9967
  playheadTime: usePlayerStore.getState().currentTime,
9780
9968
  beatTimes: includeBeats ? snapContextRef.current.beatTimes : [],
9781
- excludeElementKey,
9782
- includePlayhead
9969
+ excludeElementKey
9783
9970
  });
9784
9971
  snapTargetsCacheRef.current.set(cacheKey, targets);
9785
9972
  return targets;
9786
9973
  },
9787
9974
  []
9788
9975
  );
9976
+ const snapGridStep = useCallback9(() => {
9977
+ if (!snapContextRef.current.enabled) return 0;
9978
+ const frameRate = rulerFrameRate(usePlayerStore.getState().timeDisplayMode);
9979
+ return getTimelineGridStep(durationRef.current, ppsRef.current, frameRate);
9980
+ }, [durationRef, ppsRef]);
9789
9981
  const [draggedClip, setDraggedClipState] = useState7(null);
9790
9982
  const draggedClipRef = useRef12(null);
9791
9983
  const publishDraggedClip = useCallback9((next) => {
@@ -9874,9 +10066,6 @@ function useTimelineClipDrag({
9874
10066
  onResizeElementRef.current = onResizeElement;
9875
10067
  const onResizeElementsRef = useRef12(onResizeElements);
9876
10068
  onResizeElementsRef.current = onResizeElements;
9877
- const onSeekRef = useRef12(onSeek);
9878
- onSeekRef.current = onSeek;
9879
- const trimSeekOriginRef = useRef12(null);
9880
10069
  const readZIndexRef = useRef12(readZIndex);
9881
10070
  readZIndexRef.current = readZIndex;
9882
10071
  const onStackingPatchesRef = useRef12(onStackingPatches);
@@ -9902,7 +10091,8 @@ function useTimelineClipDrag({
9902
10091
  elements: elementsRef.current,
9903
10092
  selectedKeys: gestureSelectedKeysRef.current,
9904
10093
  buildSnapTargets,
9905
- audioTracks: dragAudioTracksRef.current
10094
+ audioTracks: dragAudioTracksRef.current,
10095
+ gridStep: snapGridStep()
9906
10096
  });
9907
10097
  },
9908
10098
  [
@@ -9912,7 +10102,8 @@ function useTimelineClipDrag({
9912
10102
  trackOrderRef,
9913
10103
  trackInsertLayoutRef,
9914
10104
  rowGeometryRef,
9915
- buildSnapTargets
10105
+ buildSnapTargets,
10106
+ snapGridStep
9916
10107
  ]
9917
10108
  );
9918
10109
  const applyResizePointer = useCallback9(
@@ -9922,14 +10113,11 @@ function useTimelineClipDrag({
9922
10113
  pps: ppsRef.current,
9923
10114
  buildSnapTargets,
9924
10115
  elements: elementsRef.current,
9925
- gestureKeys: gestureSelectedKeysRef.current
10116
+ gestureKeys: gestureSelectedKeysRef.current,
10117
+ gridStep: snapGridStep()
9926
10118
  });
9927
- trimSeekOriginRef.current ??= usePlayerStore.getState().currentTime;
9928
10119
  const setResizeState = (v) => {
9929
- onSeekRef.current?.(trimPreviewTime(resize2.edge, v.previewStart, v.previewDuration), {
9930
- keepPlaying: true,
9931
- follow: false
9932
- });
10120
+ setPreviewFrame(trimPreviewTime(resize2.edge, v.previewStart, v.previewDuration));
9933
10121
  publishResizingClip(
9934
10122
  resizingClipRef.current ? { ...resizingClipRef.current, started: true, ...v } : null
9935
10123
  );
@@ -9958,7 +10146,7 @@ function useTimelineClipDrag({
9958
10146
  }
9959
10147
  previewGroupResize(session, next, setResizeState);
9960
10148
  },
9961
- [scrollRef, ppsRef, buildSnapTargets, publishResizingClip]
10149
+ [scrollRef, ppsRef, buildSnapTargets, snapGridStep, publishResizingClip]
9962
10150
  );
9963
10151
  const applyResizePointerRef = useRef12(applyResizePointer);
9964
10152
  applyResizePointerRef.current = applyResizePointer;
@@ -9968,12 +10156,7 @@ function useTimelineClipDrag({
9968
10156
  cancelAnimationFrame(clipDragScrollRaf.current);
9969
10157
  clipDragScrollRaf.current = 0;
9970
10158
  }
9971
- if (trimSeekOriginRef.current != null) {
9972
- if (!usePlayerStore.getState().isPlaying) {
9973
- onSeekRef.current?.(trimSeekOriginRef.current, { keepPlaying: true, follow: false });
9974
- }
9975
- trimSeekOriginRef.current = null;
9976
- }
10159
+ setPreviewFrame(null);
9977
10160
  snapTargetsCacheRef.current.clear();
9978
10161
  dragAudioTracksRef.current = null;
9979
10162
  }, []);
@@ -13152,122 +13335,8 @@ function useTimelineShiftModifier() {
13152
13335
 
13153
13336
  // src/player/components/useTimelineTicks.ts
13154
13337
  import { useMemo as useMemo18 } from "react";
13155
-
13156
- // src/player/components/timelineRulerGeometry.ts
13157
- function getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate) {
13158
- const zoomIntervals = [
13159
- 0.02,
13160
- 0.05,
13161
- 0.1,
13162
- 0.2,
13163
- 0.5,
13164
- 1,
13165
- 2,
13166
- 5,
13167
- 10,
13168
- 15,
13169
- 30,
13170
- 60,
13171
- 120,
13172
- 300,
13173
- 600,
13174
- 900,
13175
- 1800,
13176
- 3600
13177
- ];
13178
- let interval;
13179
- if (Number.isFinite(pixelsPerSecond) && (pixelsPerSecond ?? 0) > 0) {
13180
- const targetMajorPx = 88;
13181
- interval = zoomIntervals.find((candidate) => candidate * (pixelsPerSecond ?? 0) >= targetMajorPx) ?? 3600;
13182
- } else {
13183
- const durationIntervals = [0.25, 0.5, 1, 2, 5, 10, 15, 30, 60];
13184
- const target = duration / 6;
13185
- interval = durationIntervals.find((candidate) => candidate >= target) ?? 60;
13186
- }
13187
- if (Number.isFinite(frameRate) && (frameRate ?? 0) > 0) {
13188
- const fps = frameRate ?? 0;
13189
- return Math.max(1, Math.ceil(interval * fps - 1e-6)) / fps;
13190
- }
13191
- return interval;
13192
- }
13193
- function getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate) {
13194
- const pps = Number.isFinite(pixelsPerSecond) ? pixelsPerSecond ?? 0 : 0;
13195
- if (pps <= 0) return 4;
13196
- const fps = Number.isFinite(frameRate) ? frameRate ?? 0 : 0;
13197
- const majorFrames = fps > 0 ? Math.round(majorInterval * fps) : 0;
13198
- const candidates = fps > 0 ? [4, 5, 3, 2] : [4, 2];
13199
- for (const parts of candidates) {
13200
- if (fps > 0 && majorFrames % parts !== 0) continue;
13201
- if (majorInterval / parts * pps >= 8) return parts;
13202
- }
13203
- return 0;
13204
- }
13205
- function roundTickValue(time) {
13206
- return Math.round(time * 1e6) / 1e6;
13207
- }
13208
- function isSupportedTickDuration(duration) {
13209
- return duration > 0 && Number.isFinite(duration) && duration <= 14400;
13210
- }
13211
- function getTickRange(duration, range) {
13212
- return {
13213
- start: Math.max(0, range?.start ?? 0),
13214
- end: Math.min(duration, range?.end ?? duration)
13215
- };
13216
- }
13217
- function appendMinorTicks(minor, majorTime, minorInterval, subdivisions, range, maxTicks, majorCount) {
13218
- for (let part = 1; part < subdivisions && majorCount + minor.length < maxTicks; part++) {
13219
- const time = majorTime + part * minorInterval;
13220
- if (time >= range.start - 1e-3 && time <= range.end + 1e-3) {
13221
- minor.push(roundTickValue(time));
13222
- }
13223
- }
13224
- }
13225
- function generateTicks(duration, pixelsPerSecond, frameRate, range) {
13226
- if (!isSupportedTickDuration(duration)) return { major: [], minor: [] };
13227
- const majorInterval = getTimelineMajorTickInterval(duration, pixelsPerSecond, frameRate);
13228
- const subdivisions = getMinorSubdivisions(majorInterval, pixelsPerSecond, frameRate);
13229
- const minorInterval = subdivisions > 0 ? majorInterval / subdivisions : 0;
13230
- const major = [];
13231
- const minor = [];
13232
- const maxTicks = 2e3;
13233
- const tickRange = getTickRange(duration, range);
13234
- const firstMajorIndex = Math.max(0, Math.floor(tickRange.start / majorInterval));
13235
- for (let index = firstMajorIndex; major.length < maxTicks; index++) {
13236
- const time = index * majorInterval;
13237
- if (time > tickRange.end + 1e-3) break;
13238
- if (time >= tickRange.start - 1e-3) major.push(roundTickValue(time));
13239
- appendMinorTicks(minor, time, minorInterval, subdivisions, tickRange, maxTicks, major.length);
13240
- }
13241
- return { major, minor };
13242
- }
13243
- function formatTimelineTickLabel(time, duration, majorInterval) {
13244
- if (!Number.isFinite(time)) return "00:00";
13245
- const safeTime = Math.max(0, time);
13246
- if (majorInterval < 0.1) {
13247
- const totalHundredths = Math.round(safeTime * 100);
13248
- const wholeSeconds = Math.floor(totalHundredths / 100);
13249
- const hundredth = totalHundredths % 100;
13250
- return `${formatTime(wholeSeconds)}.${hundredth.toString().padStart(2, "0")}`;
13251
- }
13252
- if (majorInterval < 1) {
13253
- const totalTenths = Math.round(safeTime * 10);
13254
- const wholeSeconds = Math.floor(totalTenths / 10);
13255
- const tenth = totalTenths % 10;
13256
- return `${formatTime(wholeSeconds)}.${tenth}`;
13257
- }
13258
- if (duration >= 3600 || safeTime >= 3600) {
13259
- const totalSeconds = Math.floor(safeTime);
13260
- const hours = Math.floor(totalSeconds / 3600);
13261
- const minutes = Math.floor(totalSeconds % 3600 / 60);
13262
- const seconds = totalSeconds % 60;
13263
- return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
13264
- }
13265
- return formatTime(safeTime);
13266
- }
13267
-
13268
- // src/player/components/useTimelineTicks.ts
13269
13338
  function useTimelineTicks(duration, pixelsPerSecond, timeDisplayMode, renderTimeRange) {
13270
- const frameRate = timeDisplayMode === "frame" ? STUDIO_PREVIEW_FPS : void 0;
13339
+ const frameRate = rulerFrameRate(timeDisplayMode);
13271
13340
  const ticks = useMemo18(
13272
13341
  () => generateTicks(duration, pixelsPerSecond, frameRate, renderTimeRange),
13273
13342
  [duration, frameRate, pixelsPerSecond, renderTimeRange]
@@ -14206,7 +14275,6 @@ function useTimelineProviderState({
14206
14275
  onResizeElements: pinnedOnResizeElements,
14207
14276
  onBlockedEditAttempt: oneShotEdits.onBlockedEditAttempt,
14208
14277
  onLinkEdit: oneShotEdits.onLinkEdit,
14209
- onSeek,
14210
14278
  setShowPopover,
14211
14279
  setRangeSelectionRef,
14212
14280
  readZIndex: zSyncEnabled ? readClipZIndex : void 0,
@@ -14997,7 +15065,8 @@ var DEFAULT_FADE_SECONDS = 0.5;
14997
15065
  var SNAP_LABEL = {
14998
15066
  playhead: "playhead",
14999
15067
  "clip-edge": "clip edge",
15000
- beat: "beat"
15068
+ beat: "beat",
15069
+ grid: "grid line"
15001
15070
  };
15002
15071
  var ClipFadesContext = createContext4(null);
15003
15072
  function useClipFadeDraft(el) {
@@ -16348,8 +16417,8 @@ import { HF_AUDIO_FX_ATTR } from "@hyperframes/core/audio-fx";
16348
16417
  import { HF_COLOR_GRADING_ATTR } from "@hyperframes/core/color-grading";
16349
16418
 
16350
16419
  // src/player/store/previewIframeStore.ts
16351
- import { create as create2 } from "zustand";
16352
- var usePreviewIframeStore = create2((set) => ({
16420
+ import { create as create3 } from "zustand";
16421
+ var usePreviewIframeStore = create3((set) => ({
16353
16422
  iframe: null,
16354
16423
  // Return the same state reference on a no-op set so zustand skips the
16355
16424
  // notify (Object.is check in setState) instead of merging a new object
@@ -17061,7 +17130,10 @@ function fileDocs(ownFile, written, nestedFiles) {
17061
17130
  }
17062
17131
 
17063
17132
  // src/utils/gsapSoftReload.ts
17133
+ var pendingReadyRetry = null;
17064
17134
  function ensureMotionPathPluginLoaded(iframe) {
17135
+ if (pendingReadyRetry) window.removeEventListener("message", pendingReadyRetry);
17136
+ pendingReadyRetry = null;
17065
17137
  if (!iframe?.contentWindow || !iframe.contentDocument) return;
17066
17138
  const win = iframe.contentWindow;
17067
17139
  const doc = iframe.contentDocument;
@@ -17072,7 +17144,19 @@ function ensureMotionPathPluginLoaded(iframe) {
17072
17144
  }
17073
17145
  return;
17074
17146
  }
17075
- if (!win.gsap?.registerPlugin) return;
17147
+ if (!win.gsap?.registerPlugin) {
17148
+ if (win.__playerReady) return;
17149
+ const retry = (event) => {
17150
+ const data = event.data;
17151
+ if (event.source !== win || data?.source !== "hf-preview" || data.type !== "ready") return;
17152
+ window.removeEventListener("message", retry);
17153
+ pendingReadyRetry = null;
17154
+ if (win.gsap?.registerPlugin) ensureMotionPathPluginLoaded(iframe);
17155
+ };
17156
+ pendingReadyRetry = retry;
17157
+ window.addEventListener("message", retry);
17158
+ return;
17159
+ }
17076
17160
  if (win.__hfMotionPathPluginLoading) return;
17077
17161
  try {
17078
17162
  win.__hfMotionPathPluginLoading = true;
@@ -18062,8 +18146,8 @@ async function persistElementAttribute({
18062
18146
  }
18063
18147
 
18064
18148
  // src/components/editor/cropPresetStore.ts
18065
- import { create as create3 } from "zustand";
18066
- var useCropPresetBarStore = create3((set) => ({
18149
+ import { create as create4 } from "zustand";
18150
+ var useCropPresetBarStore = create4((set) => ({
18067
18151
  openFor: null,
18068
18152
  open: (target) => set({ openFor: target }),
18069
18153
  close: () => set({ openFor: null })
@@ -24330,8 +24414,8 @@ async function normalizeClipsLoudness(projectId, elements, writers) {
24330
24414
  }
24331
24415
 
24332
24416
  // src/player/components/audioGainDialogStore.ts
24333
- import { create as create4 } from "zustand";
24334
- var useAudioGainDialogStore = create4((set) => ({
24417
+ import { create as create5 } from "zustand";
24418
+ var useAudioGainDialogStore = create5((set) => ({
24335
24419
  targetKeys: null,
24336
24420
  open: (keys) => set({ targetKeys: keys }),
24337
24421
  close: () => set({ targetKeys: null })
@@ -27332,7 +27416,7 @@ import { useRef as useRef48, useCallback as useCallback42, useEffect as useEffec
27332
27416
  import { useRef as useRef45, useCallback as useCallback38 } from "react";
27333
27417
 
27334
27418
  // src/captions/store.ts
27335
- import { create as create5 } from "zustand";
27419
+ import { create as create6 } from "zustand";
27336
27420
  var nextSplitId = 0;
27337
27421
  var HISTORY_CAP = 50;
27338
27422
  var HISTORY_COALESCE_MS = 800;
@@ -27362,7 +27446,7 @@ function pushHistory(state, key2) {
27362
27446
  const past = [...state.past, state.model].slice(-HISTORY_CAP);
27363
27447
  return { past, future: [] };
27364
27448
  }
27365
- var useCaptionStore = create5((set, get) => ({
27449
+ var useCaptionStore = create6((set, get) => ({
27366
27450
  ...initialState,
27367
27451
  // Basic
27368
27452
  setEditMode: (active) => set({ isEditMode: active }),
@@ -30830,10 +30914,11 @@ function useTimelinePlayer({
30830
30914
  const iframe = overrideIframe !== void 0 ? overrideIframe : iframeRef.current;
30831
30915
  const win = iframe?.contentWindow;
30832
30916
  if (!iframe || !win) return null;
30833
- return resolvePlaybackAdapter(iframe, win, {
30917
+ const adapter = resolvePlaybackAdapter(iframe, win, {
30834
30918
  cache: staticSeekAdapterRef,
30835
30919
  warned: staticSeekWarnedRef
30836
30920
  });
30921
+ return transportAdapter(adapter);
30837
30922
  } catch {
30838
30923
  return null;
30839
30924
  }
@@ -30977,7 +31062,7 @@ function useTimelinePlayer({
30977
31062
  return false;
30978
31063
  }
30979
31064
  const duration = Math.max(0, adapter.getDuration());
30980
- const nextTime = Math.max(0, duration > 0 ? Math.min(duration, time) : time);
31065
+ const nextTime = clampToDuration(time, duration);
30981
31066
  const keepPlaying = options?.keepPlaying === true;
30982
31067
  const shouldResumeAfterSeek = shouldResumeForwardPlaybackAfterSeek({
30983
31068
  keepPlaying,
@@ -31038,6 +31123,7 @@ function useTimelinePlayer({
31038
31123
  }
31039
31124
  });
31040
31125
  }, [seek, play, pause]);
31126
+ useEffect35(() => subscribePreviewFrame(getAdapter), [getAdapter]);
31041
31127
  const { playbackKeyDownRef, playbackKeyUpRef, attachIframeShortcutListeners, togglePlay } = usePlaybackKeyboard({
31042
31128
  iframeRef,
31043
31129
  shuttleDirectionRef,
@@ -31333,8 +31419,8 @@ function useCompositionStack({
31333
31419
  }
31334
31420
 
31335
31421
  // src/utils/assetPreviewStore.ts
31336
- import { create as create6 } from "zustand";
31337
- var useAssetPreviewStore = create6((set) => ({
31422
+ import { create as create7 } from "zustand";
31423
+ var useAssetPreviewStore = create7((set) => ({
31338
31424
  previewAsset: null,
31339
31425
  previewProjectId: null,
31340
31426
  setPreviewAsset: (asset, projectId) => set({ previewAsset: asset, previewProjectId: projectId }),
@@ -31758,9 +31844,9 @@ function resolvePreviewVisibleRegion(input) {
31758
31844
  }
31759
31845
 
31760
31846
  // src/components/editor/previewGuidesStore.ts
31761
- import { create as create7 } from "zustand";
31847
+ import { create as create8 } from "zustand";
31762
31848
  var RULER_GUTTER_PX = 16;
31763
- var usePreviewGuidesStore = create7((set, get) => {
31849
+ var usePreviewGuidesStore = create8((set, get) => {
31764
31850
  const stored = readStudioUiPreferences();
31765
31851
  return {
31766
31852
  rulerVisible: stored.rulerVisible ?? false,
@@ -33303,8 +33389,8 @@ import {
33303
33389
  } from "@hyperframes/core/audio-gain";
33304
33390
 
33305
33391
  // src/utils/audioMeterVisibility.ts
33306
- import { create as create8 } from "zustand";
33307
- var useAudioMetersVisible = create8((set) => ({
33392
+ import { create as create9 } from "zustand";
33393
+ var useAudioMetersVisible = create9((set) => ({
33308
33394
  visible: readStudioUiPreferences().audioMetersVisible ?? false,
33309
33395
  setVisible: (visible) => {
33310
33396
  writeStudioUiPreferences({ audioMetersVisible: visible });
@@ -37894,7 +37980,6 @@ function startGesture(kind, e, opts, options) {
37894
37980
  let pathOffsetMember;
37895
37981
  if (kind === "drag") {
37896
37982
  opts.onManualDragStartRef.current?.();
37897
- opts.rafPausedRef.current = true;
37898
37983
  const result = createManualOffsetDragMember({
37899
37984
  key: selectionCacheKey(sel),
37900
37985
  selection: sel,
@@ -37904,6 +37989,7 @@ function startGesture(kind, e, opts, options) {
37904
37989
  });
37905
37990
  if (!result.ok) {
37906
37991
  opts.onBlockedMoveRef.current(result.selection, result.reason);
37992
+ e.preventDefault();
37907
37993
  return false;
37908
37994
  }
37909
37995
  pathOffsetMember = result.member;
@@ -38691,6 +38777,7 @@ function useDomEditNudge(params) {
38691
38777
  const isGroup = groupItems.length > 1;
38692
38778
  const targets = isGroup ? resolveGroupNudgeTargets(groupItems) : resolveSingleNudgeTarget(p.selectionRef.current, p.overlayRectRef.current);
38693
38779
  if (!targets) return null;
38780
+ p.onManualDragStartRef.current?.();
38694
38781
  const members = [];
38695
38782
  for (const target of targets) {
38696
38783
  const result = createManualOffsetDragMember({ ...target, gesture: "nudge" });
@@ -38702,7 +38789,6 @@ function useDomEditNudge(params) {
38702
38789
  members.push(result.member);
38703
38790
  }
38704
38791
  if (members.length === 0) return null;
38705
- p.onManualDragStartRef.current?.();
38706
38792
  const edit = beginStudioPendingEdit(manualOffsetMoveRevert(members));
38707
38793
  return { members, isGroup, accum: { x: 0, y: 0 }, timer: null, edit };
38708
38794
  };
@@ -45100,7 +45186,7 @@ function nearestPointOnPath(px, py, nodes) {
45100
45186
  }
45101
45187
  return best;
45102
45188
  }
45103
- function buildMotionPathGeometry(read) {
45189
+ function buildMotionPathGeometry(read, base2 = { x: 0, y: 0 }) {
45104
45190
  if (!read) return null;
45105
45191
  const isArc = Boolean(read.arcPath);
45106
45192
  const nodes = [];
@@ -45112,8 +45198,8 @@ function buildMotionPathGeometry(read) {
45112
45198
  if (tweenHasX && !finite(kf.properties.x)) return;
45113
45199
  if (tweenHasY && !finite(kf.properties.y)) return;
45114
45200
  nodes.push({
45115
- x: tweenHasX ? kf.properties.x : 0,
45116
- y: tweenHasY ? kf.properties.y : 0,
45201
+ x: tweenHasX ? kf.properties.x : base2.x,
45202
+ y: tweenHasY ? kf.properties.y : base2.y,
45117
45203
  ref: isArc ? { type: "waypoint", index: i } : { type: "keyframe", pct: kf.percentage }
45118
45204
  });
45119
45205
  });
@@ -45139,6 +45225,97 @@ function editableAnimationId(animations, kind, sel) {
45139
45225
  return a?.id ?? null;
45140
45226
  }
45141
45227
 
45228
+ // src/components/editor/motionPathLayerNode.ts
45229
+ var LAYER_BOX = '[data-dom-edit-selection-box="true"]';
45230
+ var dotRadius = (r, selected) => selected ? r * 1.5 : r;
45231
+ function pressBelongsToLayer(e, point, pressed, dotR, live) {
45232
+ if (live && Math.abs(pressed.x - live.x) < 0.5 && Math.abs(pressed.y - live.y) < 0.5) return true;
45233
+ const hits = e.currentTarget.ownerDocument.elementsFromPoint(e.clientX, e.clientY);
45234
+ const inBox = hits.some((el) => el.closest(LAYER_BOX));
45235
+ return inBox && Math.hypot(point.x - pressed.ax, point.y - pressed.ay) > dotR;
45236
+ }
45237
+ function pressSelectedLayer(e) {
45238
+ const box = e.currentTarget.ownerDocument.querySelector(LAYER_BOX);
45239
+ if (!box || getComputedStyle(box).pointerEvents === "none") return false;
45240
+ return !box.dispatchEvent(new PointerEvent("pointerdown", e.nativeEvent));
45241
+ }
45242
+
45243
+ // src/hooks/gsapPositionDetection.ts
45244
+ function readGsapPositionFromIframe(iframe, elementSelector) {
45245
+ const gsap3 = getIframeGsap(iframe);
45246
+ if (!gsap3) return null;
45247
+ const element = queryIframeElement(iframe, elementSelector);
45248
+ if (!element) return null;
45249
+ const x = Number(gsap3.getProperty(element, "x")) || 0;
45250
+ const y = Number(gsap3.getProperty(element, "y")) || 0;
45251
+ return { x, y };
45252
+ }
45253
+ function animHasPosition(anim) {
45254
+ if (anim.keyframes?.keyframes.some((kf) => "x" in kf.properties || "y" in kf.properties))
45255
+ return true;
45256
+ if (anim.method === "fromTo") {
45257
+ const from = anim.fromProperties;
45258
+ return "x" in anim.properties || "y" in anim.properties || !!(from && ("x" in from || "y" in from));
45259
+ }
45260
+ return "x" in anim.properties || "y" in anim.properties;
45261
+ }
45262
+ function findGsapPositionAnimation(animations, selector) {
45263
+ if (animations.length === 0) return null;
45264
+ const currentTime = usePlayerStore.getState().currentTime;
45265
+ const scored = animations.filter(animHasPosition).map((a) => {
45266
+ let score = 0;
45267
+ if (a.keyframes) score += 5;
45268
+ if (selector && a.targetSelector === selector) score += 8;
45269
+ else if (a.targetSelector.includes(",")) score -= 5;
45270
+ return { anim: a, score: score + playheadProximity(a, currentTime) };
45271
+ });
45272
+ scored.sort((a, b) => b.score - a.score);
45273
+ return scored[0]?.anim ?? null;
45274
+ }
45275
+ function playheadProximity(a, currentTime) {
45276
+ const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
45277
+ const dur = a.duration ?? 0;
45278
+ if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
45279
+ return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
45280
+ }
45281
+ function statesValueAt(anim, time) {
45282
+ const duration = resolveTweenDuration(anim);
45283
+ if (!(duration > 0)) return false;
45284
+ const pct = (time - (resolveTweenStart(anim) ?? 0)) / duration * 100;
45285
+ const near3 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
45286
+ if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near3(kf.percentage));
45287
+ if (anim.method === "from") return near3(0);
45288
+ return near3(100) || anim.method === "fromTo" && near3(0);
45289
+ }
45290
+ function pickClosestToPlayhead(anims) {
45291
+ if (anims.length <= 1) return anims[0] ?? null;
45292
+ const ct = usePlayerStore.getState().currentTime;
45293
+ return anims.reduce((best, a) => {
45294
+ const dist = playheadDistance(a, ct);
45295
+ const bestDist = playheadDistance(best, ct);
45296
+ if (dist < bestDist) return a;
45297
+ const stated = dist === 0 && bestDist === 0 ? statedValueWinner(a, best, ct) : null;
45298
+ if (stated) return stated;
45299
+ if (dist === bestDist && keyframeCount(a) > keyframeCount(best)) return a;
45300
+ return best;
45301
+ });
45302
+ }
45303
+ function playheadDistance(anim, time) {
45304
+ const s = resolveTweenStart(anim) ?? 0;
45305
+ const e = s + resolveTweenDuration(anim);
45306
+ return time >= s && time <= e ? 0 : Math.min(Math.abs(time - s), Math.abs(time - e));
45307
+ }
45308
+ function statedValueWinner(a, b, time) {
45309
+ const [mine, theirs] = [statesValueAt(a, time), statesValueAt(b, time)];
45310
+ if (mine !== theirs) return mine ? a : b;
45311
+ const [s, bS] = [resolveTweenStart(a) ?? 0, resolveTweenStart(b) ?? 0];
45312
+ if (mine && s !== bS) return s > bS ? a : b;
45313
+ return null;
45314
+ }
45315
+ function keyframeCount(anim) {
45316
+ return anim.keyframes?.keyframes.length ?? 0;
45317
+ }
45318
+
45142
45319
  // src/components/editor/MotionPathNode.tsx
45143
45320
  import { jsx as jsx103, jsxs as jsxs74 } from "react/jsx-runtime";
45144
45321
  var ACCENT = "var(--hf-accent, #3CE6AC)";
@@ -45147,7 +45324,7 @@ function MotionPathNode(props) {
45147
45324
  const bx = cx + r * 1.8;
45148
45325
  const by = cy - r * 1.8;
45149
45326
  const k = r * 0.55;
45150
- const side = (selected ? r * 1.5 : r) * 1.414;
45327
+ const side = dotRadius(r, selected) * 1.414;
45151
45328
  return /* @__PURE__ */ jsxs74("g", { onPointerEnter: props.onEnter, onPointerLeave: props.onLeave, children: [
45152
45329
  /* @__PURE__ */ jsx103(
45153
45330
  "rect",
@@ -45394,7 +45571,8 @@ function useMotionPathData(iframeRef, selector) {
45394
45571
  }
45395
45572
  const recompute = () => {
45396
45573
  const read = readRuntimeKeyframes(iframeRef.current, selector, void 0, ["x", "y"]);
45397
- const next = buildMotionPathGeometry(read);
45574
+ const base2 = read ? readGsapPositionFromIframe(iframeRef.current, selector) : null;
45575
+ const next = buildMotionPathGeometry(read, base2 ?? void 0);
45398
45576
  setGeometry(
45399
45577
  (prev) => prev?.points === next?.points && prev?.kind === next?.kind ? prev : next
45400
45578
  );
@@ -45564,6 +45742,10 @@ var MotionPathOverlay = memo29(function MotionPathOverlay2({
45564
45742
  if (!interactive) return;
45565
45743
  if (e.button !== 0) return;
45566
45744
  e.stopPropagation();
45745
+ const live = readGsapPositionFromIframe(iframeRef.current, selectorFor(selection) ?? "");
45746
+ const dotR = dotRadius(nodeR, ref.type === "keyframe" && ref.pct === activeKeyframePct);
45747
+ if (pressBelongsToLayer(e, clientToComp(e), abs[index], dotR, live) && pressSelectedLayer(e))
45748
+ return;
45567
45749
  e.target.setPointerCapture(e.pointerId);
45568
45750
  dragRef.current = {
45569
45751
  index,
@@ -46532,10 +46714,10 @@ function normalize(edit) {
46532
46714
  const { anim } = edit;
46533
46715
  const data = anim.keyframes;
46534
46716
  if (data) {
46535
- if (!isLinear(anim.ease) || !isLinear(data.ease)) return { reason: "eased-keyframes" };
46536
46717
  if (data.format === "simple-array") return { reason: "simple-array-keyframes" };
46537
46718
  const arrayStep = data.format === "object-array";
46538
46719
  return {
46720
+ runEase: data.ease ?? anim.ease,
46539
46721
  keyframes: data.keyframes.map((kf) => ({
46540
46722
  ...kf,
46541
46723
  properties: { ...kf.properties },
@@ -46573,8 +46755,9 @@ function valueAt(keyframes, prop, side) {
46573
46755
  const kf = side === "first" ? carrying[0] : carrying.at(-1);
46574
46756
  return kf ? { percentage: kf.percentage, value: kf.properties[prop] } : null;
46575
46757
  }
46758
+ var keyframeAt = (keyframes, percentage) => keyframes.find((kf) => Math.abs(kf.percentage - percentage) <= KEYFRAME_PCT_MATCH);
46576
46759
  function upsert(keyframes, percentage, properties, ease) {
46577
- const hit = keyframes.find((kf) => Math.abs(kf.percentage - percentage) <= KEYFRAME_PCT_MATCH);
46760
+ const hit = keyframeAt(keyframes, percentage);
46578
46761
  if (hit) {
46579
46762
  Object.assign(hit.properties, properties);
46580
46763
  return hit;
@@ -46622,11 +46805,15 @@ function planValueAtPlayhead(edit) {
46622
46805
  let position = start;
46623
46806
  let span = duration;
46624
46807
  const pct = "percentage" in edit.at ? edit.at.percentage : roundPct2((edit.at.time - start) / duration * 100);
46808
+ const eased = !isLinear(norm.runEase);
46625
46809
  if (pct >= -KEYFRAME_PCT_MATCH && pct <= 100 + KEYFRAME_PCT_MATCH) {
46626
46810
  const at = Math.min(100, Math.max(0, pct));
46811
+ const hit = keyframeAt(keyframes, at);
46812
+ if (eased && hit?.percentage !== 0 && hit?.percentage !== 100) return refuse("eased-keyframes");
46627
46813
  const next = [...keyframes].sort((a, b) => a.percentage - b.percentage).find((kf) => kf.percentage > at + KEYFRAME_PCT_MATCH);
46628
46814
  upsert(keyframes, at, { ...values }, next?.ease);
46629
46815
  } else {
46816
+ if (eased) return refuse("eased-keyframes");
46630
46817
  const time = edit.at.time;
46631
46818
  const before = time < start;
46632
46819
  const held = heldEnds(norm, before ? "start" : "end", edit.implicitEndValue, backfilled);
@@ -46654,6 +46841,7 @@ function planValueAtPlayhead(edit) {
46654
46841
  position: roundTo3(position),
46655
46842
  duration: roundTo3(span),
46656
46843
  keyframes,
46844
+ ...norm.runEase ? { ease: norm.runEase } : {},
46657
46845
  ...norm.easeEach ? { easeEach: norm.easeEach } : {}
46658
46846
  },
46659
46847
  added: keyframes.length > authored
@@ -46785,82 +46973,6 @@ async function commitGsapPositionFromDrag(selection, anim, studioOffset, gsapPos
46785
46973
  });
46786
46974
  }
46787
46975
 
46788
- // src/hooks/gsapPositionDetection.ts
46789
- function readGsapPositionFromIframe(iframe, elementSelector) {
46790
- const gsap3 = getIframeGsap(iframe);
46791
- if (!gsap3) return null;
46792
- const element = queryIframeElement(iframe, elementSelector);
46793
- if (!element) return null;
46794
- const x = Number(gsap3.getProperty(element, "x")) || 0;
46795
- const y = Number(gsap3.getProperty(element, "y")) || 0;
46796
- return { x, y };
46797
- }
46798
- function animHasPosition(anim) {
46799
- if (anim.keyframes?.keyframes.some((kf) => "x" in kf.properties || "y" in kf.properties))
46800
- return true;
46801
- if (anim.method === "fromTo") {
46802
- const from = anim.fromProperties;
46803
- return "x" in anim.properties || "y" in anim.properties || !!(from && ("x" in from || "y" in from));
46804
- }
46805
- return "x" in anim.properties || "y" in anim.properties;
46806
- }
46807
- function findGsapPositionAnimation(animations, selector) {
46808
- if (animations.length === 0) return null;
46809
- const currentTime = usePlayerStore.getState().currentTime;
46810
- const scored = animations.filter(animHasPosition).map((a) => {
46811
- let score = 0;
46812
- if (a.keyframes) score += 5;
46813
- if (selector && a.targetSelector === selector) score += 8;
46814
- else if (a.targetSelector.includes(",")) score -= 5;
46815
- return { anim: a, score: score + playheadProximity(a, currentTime) };
46816
- });
46817
- scored.sort((a, b) => b.score - a.score);
46818
- return scored[0]?.anim ?? null;
46819
- }
46820
- function playheadProximity(a, currentTime) {
46821
- const pos = a.resolvedStart ?? (typeof a.position === "number" ? a.position : 0);
46822
- const dur = a.duration ?? 0;
46823
- if (currentTime >= pos - 0.05 && currentTime <= pos + dur + 0.05) return 50;
46824
- return -Math.round(Math.min(Math.abs(currentTime - pos), Math.abs(currentTime - pos - dur)) * 5);
46825
- }
46826
- function statesValueAt(anim, time) {
46827
- const duration = resolveTweenDuration(anim);
46828
- if (!(duration > 0)) return false;
46829
- const pct = (time - (resolveTweenStart(anim) ?? 0)) / duration * 100;
46830
- const near3 = (keyframe) => Math.abs(keyframe - pct) <= KEYFRAME_PCT_MATCH;
46831
- if (anim.keyframes) return anim.keyframes.keyframes.some((kf) => near3(kf.percentage));
46832
- if (anim.method === "from") return near3(0);
46833
- return near3(100) || anim.method === "fromTo" && near3(0);
46834
- }
46835
- function pickClosestToPlayhead(anims) {
46836
- if (anims.length <= 1) return anims[0] ?? null;
46837
- const ct = usePlayerStore.getState().currentTime;
46838
- return anims.reduce((best, a) => {
46839
- const dist = playheadDistance(a, ct);
46840
- const bestDist = playheadDistance(best, ct);
46841
- if (dist < bestDist) return a;
46842
- const stated = dist === 0 && bestDist === 0 ? statedValueWinner(a, best, ct) : null;
46843
- if (stated) return stated;
46844
- if (dist === bestDist && keyframeCount(a) > keyframeCount(best)) return a;
46845
- return best;
46846
- });
46847
- }
46848
- function playheadDistance(anim, time) {
46849
- const s = resolveTweenStart(anim) ?? 0;
46850
- const e = s + resolveTweenDuration(anim);
46851
- return time >= s && time <= e ? 0 : Math.min(Math.abs(time - s), Math.abs(time - e));
46852
- }
46853
- function statedValueWinner(a, b, time) {
46854
- const [mine, theirs] = [statesValueAt(a, time), statesValueAt(b, time)];
46855
- if (mine !== theirs) return mine ? a : b;
46856
- const [s, bS] = [resolveTweenStart(a) ?? 0, resolveTweenStart(b) ?? 0];
46857
- if (mine && s !== bS) return s > bS ? a : b;
46858
- return null;
46859
- }
46860
- function keyframeCount(anim) {
46861
- return anim.keyframes?.keyframes.length ?? 0;
46862
- }
46863
-
46864
46976
  // src/hooks/gsapElementMatch.ts
46865
46977
  function getAnimationsForElement(animations, target, element) {
46866
46978
  const matchers = /* @__PURE__ */ new Set();
@@ -49544,8 +49656,8 @@ import { useCallback as useCallback71, useSyncExternalStore as useSyncExternalSt
49544
49656
  import { CornersIn, CornersOut, DotsThree, X as X2 } from "@phosphor-icons/react";
49545
49657
 
49546
49658
  // src/components/dock/dockLayoutStore.ts
49547
- import { create as create9 } from "zustand";
49548
- var useDockLayoutStore = create9((set, get) => ({
49659
+ import { create as create10 } from "zustand";
49660
+ var useDockLayoutStore = create10((set, get) => ({
49549
49661
  controller: null,
49550
49662
  panels: PANEL_IDS,
49551
49663
  openPanels: new Set(PANEL_IDS),
@@ -50380,6 +50492,37 @@ function EditorShellBody({
50380
50492
  );
50381
50493
  }
50382
50494
 
50495
+ // src/hooks/linkShortcuts.ts
50496
+ var isDeleteKey = (event) => event.key === "Backspace" || event.key === "Delete";
50497
+ var hasMod = (event) => event.metaKey || event.ctrlKey;
50498
+ var SHORTCUTS = [
50499
+ ["\u2318L", (e) => hasMod(e) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "l"],
50500
+ ["\u2325\u21E7D", (e) => !hasMod(e) && e.altKey && e.shiftKey && e.code === "KeyD"],
50501
+ ["\u2325\u232B", (e) => !hasMod(e) && e.altKey && !e.shiftKey && isDeleteKey(e)]
50502
+ ];
50503
+ function shortcutOf(event) {
50504
+ return SHORTCUTS.find(([, matches]) => matches(event))?.[0] ?? null;
50505
+ }
50506
+ function dispatchLinkShortcut(event, cb) {
50507
+ if (isTypingTarget(event.target)) return false;
50508
+ const shortcut = shortcutOf(event);
50509
+ if (!shortcut) return false;
50510
+ const { elements, selectedElementId, selectedElementIds } = usePlayerStore.getState();
50511
+ const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
50512
+ if (!element) return false;
50513
+ const item = resolveLinkMenuItems({
50514
+ element,
50515
+ elements,
50516
+ selectedKeys: selectedElementIds,
50517
+ onLinkEdit: cb.handleLinkEdit,
50518
+ onDeleteElementOnly: cb.handleTimelineElementDeleteOnly
50519
+ }).find((candidate) => candidate.shortcut === shortcut);
50520
+ if (!item) return false;
50521
+ event.preventDefault();
50522
+ item.run();
50523
+ return true;
50524
+ }
50525
+
50383
50526
  // src/player/components/ImageThumbnail.tsx
50384
50527
  import { memo as memo34, useMemo as useMemo48 } from "react";
50385
50528
 
@@ -85076,36 +85219,6 @@ function useBlockHandlers({
85076
85219
  // src/hooks/useAppHotkeys.ts
85077
85220
  import { useCallback as useCallback154, useEffect as useEffect128, useRef as useRef158 } from "react";
85078
85221
 
85079
- // src/hooks/linkShortcuts.ts
85080
- var isDeleteKey = (event) => event.key === "Backspace" || event.key === "Delete";
85081
- var hasMod = (event) => event.metaKey || event.ctrlKey;
85082
- var SHORTCUTS = [
85083
- ["\u2318L", (e) => hasMod(e) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "l"],
85084
- ["\u2325\u21E7D", (e) => !hasMod(e) && e.altKey && e.shiftKey && e.code === "KeyD"],
85085
- ["\u2325\u232B", (e) => !hasMod(e) && e.altKey && !e.shiftKey && isDeleteKey(e)]
85086
- ];
85087
- function shortcutOf(event) {
85088
- return SHORTCUTS.find(([, matches]) => matches(event))?.[0] ?? null;
85089
- }
85090
- function dispatchLinkShortcut(event, cb) {
85091
- const shortcut = shortcutOf(event);
85092
- if (!shortcut) return false;
85093
- const { elements, selectedElementId, selectedElementIds } = usePlayerStore.getState();
85094
- const element = elements.find((el) => (el.key ?? el.id) === selectedElementId);
85095
- if (!element) return false;
85096
- const item = resolveLinkMenuItems({
85097
- element,
85098
- elements,
85099
- selectedKeys: selectedElementIds,
85100
- onLinkEdit: cb.handleLinkEdit,
85101
- onDeleteElementOnly: cb.handleTimelineElementDeleteOnly
85102
- }).find((candidate) => candidate.shortcut === shortcut);
85103
- if (!item) return false;
85104
- event.preventDefault();
85105
- item.run();
85106
- return true;
85107
- }
85108
-
85109
85222
  // src/hooks/appHotkeysDispatch.ts
85110
85223
  function handleUndoRedoKey(event, onUndo, onRedo) {
85111
85224
  const key2 = event.key.toLowerCase();
@@ -85516,7 +85629,7 @@ function useAppHotkeys({
85516
85629
  const handleAppKeyDown = useCallback154((event) => {
85517
85630
  const cb = cbRef.current;
85518
85631
  const key2 = event.key.toLowerCase();
85519
- if (!isTypingTarget(event.target) && dispatchLinkShortcut(event, cb)) return;
85632
+ if (dispatchLinkShortcut(event, cb)) return;
85520
85633
  if (event.metaKey || event.ctrlKey) {
85521
85634
  dispatchModifierKey(event, key2, cb);
85522
85635
  return;
@@ -96558,6 +96671,7 @@ function useKeyframeToggle(session) {
96558
96671
  var TimelineToolbar = memo60(function TimelineToolbar2({
96559
96672
  domEditSession,
96560
96673
  onSplitElement,
96674
+ splitShortcut = SPLIT_SHORTCUT_HINT,
96561
96675
  history: history2,
96562
96676
  showHistory = true,
96563
96677
  showSelectAroundPlayhead = true,
@@ -96725,7 +96839,7 @@ var TimelineToolbar = memo60(function TimelineToolbar2({
96725
96839
  return /* @__PURE__ */ jsx243(
96726
96840
  Tooltip,
96727
96841
  {
96728
- label: canSplit ? "Split at playhead (S)" : splittable ? "Move the playhead inside the clip to split" : "Select a clip to split",
96842
+ label: canSplit ? `Split at playhead (${splitShortcut})` : splittable ? "Move the playhead inside the clip to split" : "Select a clip to split",
96729
96843
  children: /* @__PURE__ */ jsx243(
96730
96844
  "button",
96731
96845
  {
@@ -98306,6 +98420,7 @@ export {
98306
98420
  cn,
98307
98421
  collectSnapContext,
98308
98422
  collectStudioLookScene,
98423
+ dispatchLinkShortcut,
98309
98424
  displayTrackOrder,
98310
98425
  extractSnapTargets,
98311
98426
  fadeHandleBoxes,