@hyperframes/studio 0.8.48 → 0.8.49

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 (40) hide show
  1. package/dist/assets/hyperframes-player-CqON2Rzo.js +464 -0
  2. package/dist/assets/{index-B3-orIjW.js → index-BGovDksE.js} +1 -1
  3. package/dist/assets/{index-C9gJKpqQ.js → index-Bgmx2h7W.js} +135 -135
  4. package/dist/assets/{index-D_j-0PD3.js → index-mIgSsKqs.js} +1 -1
  5. package/dist/index.d.ts +4 -3
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +443 -303
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/hooks/useBlockHandlers.ts +7 -2
  11. package/src/hooks/useTimelineAssetDropOps.test.tsx +15 -0
  12. package/src/hooks/useTimelineAssetDropOps.ts +4 -2
  13. package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +215 -3
  14. package/src/hooks/useTimelineSelectionPreviewSync.ts +63 -32
  15. package/src/player/components/timelineClipDragCommit.test.ts +114 -0
  16. package/src/player/components/timelineClipDragCommit.ts +18 -49
  17. package/src/player/components/timelineClipDragPreview.test.ts +173 -1
  18. package/src/player/components/timelineClipDragPreview.ts +8 -1
  19. package/src/player/components/timelineCollision.test.ts +23 -0
  20. package/src/player/components/timelineCollision.ts +27 -0
  21. package/src/player/components/timelineDragLanding.ts +108 -0
  22. package/src/player/components/timelineDropReveal.test.ts +34 -0
  23. package/src/player/components/timelineDropReveal.ts +12 -0
  24. package/src/player/components/useTimelineFocusCoordinator.test.tsx +66 -0
  25. package/src/player/components/useTimelineFocusCoordinator.ts +43 -3
  26. package/src/player/components/useTimelineGeometry.test.tsx +117 -0
  27. package/src/player/components/useTimelineGeometry.ts +7 -2
  28. package/src/player/components/useTimelineGeometryFocusOrder.test.tsx +184 -0
  29. package/src/player/components/useTimelineLogicalFocus.ts +1 -0
  30. package/src/player/lib/timelineElementHelpers.ts +8 -0
  31. package/src/player/store/readinessSlice.ts +4 -3
  32. package/src/utils/blockInstaller.test.ts +4 -0
  33. package/src/utils/blockInstaller.ts +9 -3
  34. package/src/utils/retryBudget.ts +20 -0
  35. package/src/utils/sdkCutover.familyGate.test.ts +5 -1
  36. package/src/utils/sdkCutover.ts +7 -4
  37. package/src/utils/sdkCutoverEligibility.test.ts +96 -1
  38. package/src/utils/sdkCutoverEligibility.ts +34 -11
  39. package/src/utils/timelineCompositionInsert.ts +2 -1
  40. package/dist/assets/hyperframes-player-CtVX6jL_.js +0 -464
package/dist/index.js CHANGED
@@ -1195,6 +1195,9 @@ function deriveTimelineStoreKey(params) {
1195
1195
  if (!params.domId && !params.selector) return void 0;
1196
1196
  return buildTimelineElementKey({ id: "", fallbackIndex: 0, ...params });
1197
1197
  }
1198
+ function deriveTimelineStoreKeyForDomId(domId, sourceFile) {
1199
+ return deriveTimelineStoreKey({ domId, sourceFile });
1200
+ }
1198
1201
  function getTimelineDomNodes(doc) {
1199
1202
  const rootComp = doc.querySelector("[data-composition-id]");
1200
1203
  return Array.from(doc.querySelectorAll("[data-start]")).filter(
@@ -18623,6 +18626,71 @@ function snapMoveToTargets(start, duration, targets, pixelsPerSecond, timelineDu
18623
18626
  return { start: clamped, snapTime: target?.time ?? null, snapType: target?.type ?? null };
18624
18627
  }
18625
18628
 
18629
+ // src/player/components/timelineZones.ts
18630
+ function classifyZone(el) {
18631
+ return isAudioTimelineElement(el) ? "audio" : "visual";
18632
+ }
18633
+ function isMainTrackElement(el) {
18634
+ return el.track === 0 && classifyZone(el) === "visual" && el.expandedParentStart == null;
18635
+ }
18636
+ var keyOf = (el) => el.key ?? el.id;
18637
+ var EPS = 1e-6;
18638
+ function overlaps2(a, b) {
18639
+ return a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
18640
+ }
18641
+ function byStableId(a, b) {
18642
+ const ka = keyOf(a);
18643
+ const kb = keyOf(b);
18644
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
18645
+ }
18646
+ function packTrackLanes(clips, base2, laneOf) {
18647
+ const ordered = [...clips].sort(byStableId);
18648
+ const lanes = [];
18649
+ for (const el of ordered) {
18650
+ let sub = lanes.findIndex((occ) => occ.every((o) => !overlaps2(o, el)));
18651
+ if (sub === -1) {
18652
+ sub = lanes.length;
18653
+ lanes.push([]);
18654
+ }
18655
+ lanes[sub].push(el);
18656
+ laneOf.set(keyOf(el), base2 + sub);
18657
+ }
18658
+ return Math.max(1, lanes.length);
18659
+ }
18660
+ function packZoneLanes(clips, base2, laneOf) {
18661
+ const byTrack = /* @__PURE__ */ new Map();
18662
+ for (const el of clips) {
18663
+ const list = byTrack.get(el.track);
18664
+ if (list) list.push(el);
18665
+ else byTrack.set(el.track, [el]);
18666
+ }
18667
+ let used = 0;
18668
+ for (const track2 of [...byTrack.keys()].sort((a, b) => a - b)) {
18669
+ used += packTrackLanes(byTrack.get(track2), base2 + used, laneOf);
18670
+ }
18671
+ return used;
18672
+ }
18673
+ function normalizeToZones(elements) {
18674
+ if (elements.length === 0) return elements;
18675
+ const laneOf = /* @__PURE__ */ new Map();
18676
+ const visual = [];
18677
+ const audio = [];
18678
+ for (const el of elements) {
18679
+ (classifyZone(el) === "audio" ? audio : visual).push(el);
18680
+ }
18681
+ let nextLane = 0;
18682
+ nextLane += packZoneLanes(visual, nextLane, laneOf);
18683
+ packZoneLanes(audio, nextLane, laneOf);
18684
+ let changed = false;
18685
+ const remapped = elements.map((el) => {
18686
+ const lane = laneOf.get(keyOf(el));
18687
+ if (lane == null || lane === el.track) return el;
18688
+ changed = true;
18689
+ return { ...el, track: lane, authoredTrack: el.authoredTrack ?? el.track };
18690
+ });
18691
+ return changed ? remapped : elements;
18692
+ }
18693
+
18626
18694
  // src/player/components/timelineCollision.ts
18627
18695
  function clampTrackToZone(targetTrack, trackOrder, audioRow, isAudio) {
18628
18696
  if (audioRow < 0) return targetTrack;
@@ -18675,6 +18743,21 @@ function resolveZoneDropPlacement(input) {
18675
18743
  }
18676
18744
  return { track: placement.track, insertRow: null };
18677
18745
  }
18746
+ function landsOnMainTrack(landingTrack, isAudio) {
18747
+ return isMainTrackElement({
18748
+ id: "",
18749
+ tag: isAudio ? "audio" : "video",
18750
+ start: 0,
18751
+ duration: 0,
18752
+ track: landingTrack
18753
+ });
18754
+ }
18755
+ function resolveMainTrackDropStart(others, originTrack, landingTrack, isAudio, desiredStart) {
18756
+ if (originTrack === landingTrack || !landsOnMainTrack(landingTrack, isAudio)) {
18757
+ return desiredStart;
18758
+ }
18759
+ return others.some(isMainTrackElement) ? desiredStart : 0;
18760
+ }
18678
18761
  function resolveInsertRow(rowFloat, trackCount, band = INSERT_BOUNDARY_BAND) {
18679
18762
  if (trackCount === 0) return 0;
18680
18763
  if (rowFloat <= 0) return 0;
@@ -18720,6 +18803,197 @@ function resolvePlacement({
18720
18803
  return { track: desiredTrack, needsInsert: true };
18721
18804
  }
18722
18805
 
18806
+ // src/player/components/timelineAuthoredMoveTarget.ts
18807
+ function canMoveTimelineElement(element) {
18808
+ return getTimelineEditCapabilities({
18809
+ tag: element.tag,
18810
+ kind: element.kind,
18811
+ duration: element.duration,
18812
+ domId: element.domId,
18813
+ hfId: element.hfId,
18814
+ selector: element.selector,
18815
+ compositionSrc: element.compositionSrc,
18816
+ playbackStart: element.playbackStart,
18817
+ playbackStartAttr: element.playbackStartAttr,
18818
+ sourceDuration: element.sourceDuration,
18819
+ timingSource: element.timingSource,
18820
+ timelineLocked: element.timelineLocked
18821
+ }).canMove;
18822
+ }
18823
+ function resolveExpandedHostAlias(drag, deps) {
18824
+ const selectedKeys = deps.selectedKeys;
18825
+ if (!selectedKeys) return null;
18826
+ const collapsedKeys = new Set(selectedKeys);
18827
+ const candidates = deps.elements.includes(drag.element) ? deps.elements : [...deps.elements, drag.element];
18828
+ for (const element of candidates) {
18829
+ const hostKey2 = element.expandedHostKey;
18830
+ const childKey2 = getTimelineElementIdentity(element);
18831
+ if (hostKey2 && collapsedKeys.has(hostKey2) && collapsedKeys.has(childKey2)) {
18832
+ collapsedKeys.delete(childKey2);
18833
+ }
18834
+ }
18835
+ const hostKey = drag.element.expandedHostKey;
18836
+ const childKey = getTimelineElementIdentity(drag.element);
18837
+ if (!hostKey || collapsedKeys.has(childKey)) {
18838
+ if (collapsedKeys.size === selectedKeys.size) return null;
18839
+ return { drag, selectedKeys: collapsedKeys };
18840
+ }
18841
+ const host = deps.elements.find((element) => getTimelineElementIdentity(element) === hostKey);
18842
+ if (!host || !canMoveTimelineElement(host)) return null;
18843
+ const delta = drag.previewStart - drag.element.start;
18844
+ const mapTrack = (track2) => track2 === drag.element.track ? host.track : track2;
18845
+ return {
18846
+ drag: {
18847
+ ...drag,
18848
+ element: host,
18849
+ previewStart: Math.max(0, Math.round((host.start + delta) * 1e3) / 1e3),
18850
+ previewTrack: mapTrack(drag.previewTrack) ?? host.track,
18851
+ desiredTrack: mapTrack(drag.desiredTrack)
18852
+ },
18853
+ selectedKeys: collapsedKeys
18854
+ };
18855
+ }
18856
+
18857
+ // src/player/components/timelineAuthoredTrack.ts
18858
+ var keyOf2 = (element) => element.key ?? element.id;
18859
+ var sameSourceFile = (a, b) => (a.sourceFile ?? null) === (b.sourceFile ?? null);
18860
+ function authoredTrackForLane(lane, elements, dragged) {
18861
+ const dragKey = keyOf2(dragged);
18862
+ const peers = elements.filter((element) => {
18863
+ return keyOf2(element) !== dragKey && sameSourceFile(element, dragged);
18864
+ });
18865
+ const occupant = peers.find((element) => element.track === lane);
18866
+ if (occupant) return occupant.authoredTrack ?? occupant.track;
18867
+ let nearest = null;
18868
+ for (const peer of peers) {
18869
+ if (!nearest || Math.abs(peer.track - lane) < Math.abs(nearest.track - lane)) nearest = peer;
18870
+ }
18871
+ if (!nearest) return lane;
18872
+ return Math.round((nearest.authoredTrack ?? nearest.track) + (lane - nearest.track));
18873
+ }
18874
+
18875
+ // src/player/components/timelineGaps.ts
18876
+ var TRACK_GAP_EPSILON_S = 1e-3;
18877
+ var keyOf3 = (e) => e.key ?? e.id;
18878
+ function laneGapFloor(elements) {
18879
+ return Math.max(0, ...elements.map((e) => e.expandedParentStart ?? 0));
18880
+ }
18881
+ var round3 = (v) => Math.round(v * 1e3) / 1e3;
18882
+ var endOf = (e) => e.start + e.duration;
18883
+ function sortedLaneClips(elements) {
18884
+ return [...elements].sort((a, b) => a.start - b.start || keyOf3(a).localeCompare(keyOf3(b)));
18885
+ }
18886
+ function resolveTrackGapAt(elements, time, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
18887
+ const clips = sortedLaneClips(elements);
18888
+ const occupied = clips.some((c) => time >= c.start - epsilon && time < endOf(c) - epsilon);
18889
+ if (occupied) return null;
18890
+ const following = clips.filter((c) => c.start > time - epsilon);
18891
+ if (following.length === 0) return null;
18892
+ const gapEnd = following[0].start;
18893
+ const gapStart = Math.max(
18894
+ floor,
18895
+ ...clips.filter((c) => c.start <= time - epsilon).map((c) => endOf(c))
18896
+ );
18897
+ if (gapEnd - gapStart <= epsilon) return null;
18898
+ return { gapStart, gapEnd, followingKeys: following.map(keyOf3) };
18899
+ }
18900
+ function resolveAllGapIntervals(elements, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
18901
+ const gaps = [];
18902
+ let cursor = floor;
18903
+ for (const clip of sortedLaneClips(elements)) {
18904
+ if (clip.start - cursor > epsilon) gaps.push({ start: cursor, end: clip.start });
18905
+ cursor = Math.max(cursor, endOf(clip));
18906
+ }
18907
+ return gaps;
18908
+ }
18909
+ function resolveAllTrackGaps(elements, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
18910
+ const shifts = [];
18911
+ let cursor = floor;
18912
+ for (const clip of sortedLaneClips(elements)) {
18913
+ const newStart = round3(cursor);
18914
+ if (Math.abs(newStart - clip.start) > epsilon) {
18915
+ shifts.push({ key: keyOf3(clip), newStart });
18916
+ }
18917
+ cursor += clip.duration;
18918
+ }
18919
+ return shifts;
18920
+ }
18921
+ function resolveLaneEmptyIntervals(elements, end, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
18922
+ const gaps = resolveAllGapIntervals(elements, epsilon, floor);
18923
+ const maxEnd = Math.max(floor, ...elements.map(endOf));
18924
+ if (end - maxEnd > epsilon) gaps.push({ start: maxEnd, end });
18925
+ return gaps;
18926
+ }
18927
+ function resolveCloseGapShifts(elements, gap) {
18928
+ const width = gap.gapEnd - gap.gapStart;
18929
+ const followSet = new Set(gap.followingKeys);
18930
+ return sortedLaneClips(elements).filter((c) => followSet.has(keyOf3(c))).map((c) => ({ key: keyOf3(c), newStart: Math.max(gap.gapStart, round3(c.start - width)) }));
18931
+ }
18932
+
18933
+ // src/player/components/timelineDragLanding.ts
18934
+ var keyOf4 = (e) => e.key ?? e.id;
18935
+ function insertTrackValue(trackOrder, insertRow) {
18936
+ if (trackOrder.length === 0) return 0;
18937
+ if (insertRow <= 0) return trackOrder[0] - 0.5;
18938
+ if (insertRow >= trackOrder.length) return trackOrder[trackOrder.length - 1] + 0.5;
18939
+ return (trackOrder[insertRow - 1] + trackOrder[insertRow]) / 2;
18940
+ }
18941
+ function layoutAfterTrackInsert(element, previewStart, insertRow, multi, deps) {
18942
+ const { elements, trackOrder } = deps;
18943
+ const editKey = keyOf4(element);
18944
+ if (element.expandedParentStart != null) return null;
18945
+ const targetTrack = insertTrackValue(trackOrder, insertRow);
18946
+ const writableZone = classifyZone(element);
18947
+ const writable = (src) => sameSourceFile(src, element) && classifyZone(src) === writableZone && src.expandedParentStart == null;
18948
+ const topologyOrder = [...new Set(elements.filter(writable).map((e) => e.track))].sort(
18949
+ (a, b) => a - b
18950
+ );
18951
+ const topologyInsertRow = topologyOrder.filter((track2) => track2 < targetTrack).length;
18952
+ const topologyTargetTrack = insertTrackValue(topologyOrder, topologyInsertRow);
18953
+ const normalized = normalizeToZones(
18954
+ elements.filter(writable).map((e) => {
18955
+ if (keyOf4(e) === editKey) {
18956
+ return { ...e, start: previewStart, track: topologyTargetTrack };
18957
+ }
18958
+ if (multi?.keys.has(keyOf4(e))) return { ...e, start: multi.movedStart(e) };
18959
+ return e;
18960
+ })
18961
+ );
18962
+ return { normalized, targetTrack, writable };
18963
+ }
18964
+ function resolveDragLandingStart(drag, deps) {
18965
+ const alias = resolveExpandedHostAlias(drag, deps);
18966
+ const target = alias?.drag ?? drag;
18967
+ const keys = alias?.selectedKeys ?? deps.selectedKeys;
18968
+ const element = target.element;
18969
+ const key2 = keyOf4(element);
18970
+ if (keys && keys.size > 1 && keys.has(key2)) return drag.previewStart;
18971
+ let landingTrack = target.previewTrack;
18972
+ let renumbered = /* @__PURE__ */ new Map();
18973
+ if (target.insertRow != null) {
18974
+ const layout = layoutAfterTrackInsert(
18975
+ element,
18976
+ target.previewStart,
18977
+ target.insertRow,
18978
+ null,
18979
+ deps
18980
+ );
18981
+ if (layout) {
18982
+ renumbered = new Map(layout.normalized.map((n) => [keyOf4(n), n.track]));
18983
+ landingTrack = renumbered.get(key2) ?? layout.targetTrack;
18984
+ }
18985
+ }
18986
+ const others = deps.elements.filter((e) => keyOf4(e) !== key2).map((e) => renumbered.has(keyOf4(e)) ? { ...e, track: renumbered.get(keyOf4(e)) } : e);
18987
+ const snapped = resolveMainTrackDropStart(
18988
+ others,
18989
+ element.track,
18990
+ landingTrack,
18991
+ isAudioTimelineElement(element),
18992
+ target.previewStart
18993
+ );
18994
+ return snapped === target.previewStart ? drag.previewStart : round3(drag.previewStart + snapped - target.previewStart);
18995
+ }
18996
+
18723
18997
  // src/player/components/timelineClipDragPreview.ts
18724
18998
  function getTimelineDragOverlayPosition(drag, scroll) {
18725
18999
  if (!drag.started || !scroll) return null;
@@ -18818,12 +19092,18 @@ function computeDragPreview(drag, clientX, clientY, ctx3) {
18818
19092
  nextMove.track,
18819
19093
  ctx3
18820
19094
  );
19095
+ const placed = { ...drag, previewStart, previewTrack, insertRow };
19096
+ const snappedStart = resolveDragLandingStart(placed, {
19097
+ elements,
19098
+ trackOrder,
19099
+ selectedKeys
19100
+ });
18821
19101
  return {
18822
19102
  ...drag,
18823
19103
  started: true,
18824
19104
  pointerClientX: clientX,
18825
19105
  pointerClientY: clientY,
18826
- previewStart,
19106
+ previewStart: snappedStart,
18827
19107
  previewTrack,
18828
19108
  // The lane the POINTER aims at (pre-collision): the commit reads it to tell a
18829
19109
  // deliberate vertical lane change from a horizontal drag merely bumped sideways.
@@ -19179,71 +19459,6 @@ var TimelineCanvas = memo13(function TimelineCanvas2(props) {
19179
19459
  // src/player/components/useTimelineClipDrag.ts
19180
19460
  import { useRef as useRef29, useState as useState17, useCallback as useCallback19, useMemo as useMemo16, useEffect as useEffect21 } from "react";
19181
19461
 
19182
- // src/player/components/timelineZones.ts
19183
- function classifyZone(el) {
19184
- return isAudioTimelineElement(el) ? "audio" : "visual";
19185
- }
19186
- function isMainTrackElement(el) {
19187
- return el.track === 0 && classifyZone(el) === "visual" && el.expandedParentStart == null;
19188
- }
19189
- var keyOf = (el) => el.key ?? el.id;
19190
- var EPS = 1e-6;
19191
- function overlaps2(a, b) {
19192
- return a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
19193
- }
19194
- function byStableId(a, b) {
19195
- const ka = keyOf(a);
19196
- const kb = keyOf(b);
19197
- return ka < kb ? -1 : ka > kb ? 1 : 0;
19198
- }
19199
- function packTrackLanes(clips, base2, laneOf) {
19200
- const ordered = [...clips].sort(byStableId);
19201
- const lanes = [];
19202
- for (const el of ordered) {
19203
- let sub = lanes.findIndex((occ) => occ.every((o) => !overlaps2(o, el)));
19204
- if (sub === -1) {
19205
- sub = lanes.length;
19206
- lanes.push([]);
19207
- }
19208
- lanes[sub].push(el);
19209
- laneOf.set(keyOf(el), base2 + sub);
19210
- }
19211
- return Math.max(1, lanes.length);
19212
- }
19213
- function packZoneLanes(clips, base2, laneOf) {
19214
- const byTrack = /* @__PURE__ */ new Map();
19215
- for (const el of clips) {
19216
- const list = byTrack.get(el.track);
19217
- if (list) list.push(el);
19218
- else byTrack.set(el.track, [el]);
19219
- }
19220
- let used = 0;
19221
- for (const track2 of [...byTrack.keys()].sort((a, b) => a - b)) {
19222
- used += packTrackLanes(byTrack.get(track2), base2 + used, laneOf);
19223
- }
19224
- return used;
19225
- }
19226
- function normalizeToZones(elements) {
19227
- if (elements.length === 0) return elements;
19228
- const laneOf = /* @__PURE__ */ new Map();
19229
- const visual = [];
19230
- const audio = [];
19231
- for (const el of elements) {
19232
- (classifyZone(el) === "audio" ? audio : visual).push(el);
19233
- }
19234
- let nextLane = 0;
19235
- nextLane += packZoneLanes(visual, nextLane, laneOf);
19236
- packZoneLanes(audio, nextLane, laneOf);
19237
- let changed = false;
19238
- const remapped = elements.map((el) => {
19239
- const lane = laneOf.get(keyOf(el));
19240
- if (lane == null || lane === el.track) return el;
19241
- changed = true;
19242
- return { ...el, track: lane, authoredTrack: el.authoredTrack ?? el.track };
19243
- });
19244
- return changed ? remapped : elements;
19245
- }
19246
-
19247
19462
  // src/player/components/timelineStackingSync.ts
19248
19463
  var EPS2 = 1e-6;
19249
19464
  var paintScopeKey = (el) => JSON.stringify([el.sourceFile ?? null, el.stackingContextId ?? null]);
@@ -19359,57 +19574,6 @@ function computeStackingPatches(elements, editedKeys) {
19359
19574
  return patches;
19360
19575
  }
19361
19576
 
19362
- // src/player/components/timelineAuthoredMoveTarget.ts
19363
- function canMoveTimelineElement(element) {
19364
- return getTimelineEditCapabilities({
19365
- tag: element.tag,
19366
- kind: element.kind,
19367
- duration: element.duration,
19368
- domId: element.domId,
19369
- hfId: element.hfId,
19370
- selector: element.selector,
19371
- compositionSrc: element.compositionSrc,
19372
- playbackStart: element.playbackStart,
19373
- playbackStartAttr: element.playbackStartAttr,
19374
- sourceDuration: element.sourceDuration,
19375
- timingSource: element.timingSource,
19376
- timelineLocked: element.timelineLocked
19377
- }).canMove;
19378
- }
19379
- function resolveExpandedHostAlias(drag, deps) {
19380
- const selectedKeys = deps.selectedKeys;
19381
- if (!selectedKeys) return null;
19382
- const collapsedKeys = new Set(selectedKeys);
19383
- const candidates = deps.elements.includes(drag.element) ? deps.elements : [...deps.elements, drag.element];
19384
- for (const element of candidates) {
19385
- const hostKey2 = element.expandedHostKey;
19386
- const childKey2 = getTimelineElementIdentity(element);
19387
- if (hostKey2 && collapsedKeys.has(hostKey2) && collapsedKeys.has(childKey2)) {
19388
- collapsedKeys.delete(childKey2);
19389
- }
19390
- }
19391
- const hostKey = drag.element.expandedHostKey;
19392
- const childKey = getTimelineElementIdentity(drag.element);
19393
- if (!hostKey || collapsedKeys.has(childKey)) {
19394
- if (collapsedKeys.size === selectedKeys.size) return null;
19395
- return { drag, selectedKeys: collapsedKeys };
19396
- }
19397
- const host = deps.elements.find((element) => getTimelineElementIdentity(element) === hostKey);
19398
- if (!host || !canMoveTimelineElement(host)) return null;
19399
- const delta = drag.previewStart - drag.element.start;
19400
- const mapTrack = (track2) => track2 === drag.element.track ? host.track : track2;
19401
- return {
19402
- drag: {
19403
- ...drag,
19404
- element: host,
19405
- previewStart: Math.max(0, Math.round((host.start + delta) * 1e3) / 1e3),
19406
- previewTrack: mapTrack(drag.previewTrack) ?? host.track,
19407
- desiredTrack: mapTrack(drag.desiredTrack)
19408
- },
19409
- selectedKeys: collapsedKeys
19410
- };
19411
- }
19412
-
19413
19577
  // src/player/components/timelineOptimisticRevision.ts
19414
19578
  var revisionsByUpdater = /* @__PURE__ */ new WeakMap();
19415
19579
  function beginTimelineOptimisticGesture(updateElement, keys) {
@@ -19485,27 +19649,9 @@ function refreshAfterDurableLaneMove(pending2, deps) {
19485
19649
  });
19486
19650
  }
19487
19651
 
19488
- // src/player/components/timelineAuthoredTrack.ts
19489
- var keyOf2 = (element) => element.key ?? element.id;
19490
- var sameSourceFile = (a, b) => (a.sourceFile ?? null) === (b.sourceFile ?? null);
19491
- function authoredTrackForLane(lane, elements, dragged) {
19492
- const dragKey = keyOf2(dragged);
19493
- const peers = elements.filter((element) => {
19494
- return keyOf2(element) !== dragKey && sameSourceFile(element, dragged);
19495
- });
19496
- const occupant = peers.find((element) => element.track === lane);
19497
- if (occupant) return occupant.authoredTrack ?? occupant.track;
19498
- let nearest = null;
19499
- for (const peer of peers) {
19500
- if (!nearest || Math.abs(peer.track - lane) < Math.abs(nearest.track - lane)) nearest = peer;
19501
- }
19502
- if (!nearest) return lane;
19503
- return Math.round((nearest.authoredTrack ?? nearest.track) + (lane - nearest.track));
19504
- }
19505
-
19506
19652
  // src/player/components/timelineClipDragCommit.ts
19507
- var keyOf3 = (e) => e.key ?? e.id;
19508
- var round3 = (v) => Math.round(v * 1e3) / 1e3;
19653
+ var keyOf5 = (e) => e.key ?? e.id;
19654
+ var round32 = (v) => Math.round(v * 1e3) / 1e3;
19509
19655
  var laneChangeGestureSeq = 0;
19510
19656
  function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coalesceMs) {
19511
19657
  if (edits.length === 0) return Promise.resolve(true);
@@ -19516,19 +19662,19 @@ function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coales
19516
19662
  );
19517
19663
  }
19518
19664
  const prev = edits.map((e) => ({
19519
- key: keyOf3(e.element),
19665
+ key: keyOf5(e.element),
19520
19666
  start: e.element.start,
19521
19667
  track: e.element.track,
19522
19668
  authoredTrack: e.element.authoredTrack
19523
19669
  }));
19524
19670
  const revision = beginTimelineOptimisticGesture(
19525
19671
  updateElement,
19526
- edits.map((edit) => keyOf3(edit.element))
19672
+ edits.map((edit) => keyOf5(edit.element))
19527
19673
  );
19528
19674
  const applyEdit = (e) => {
19529
19675
  const writtenTrack = e.persistTrack ?? (e.updates.track !== e.element.track ? e.updates.track : void 0);
19530
19676
  updateElement(
19531
- keyOf3(e.element),
19677
+ keyOf5(e.element),
19532
19678
  writtenTrack == null ? e.updates : { ...e.updates, authoredTrack: writtenTrack }
19533
19679
  );
19534
19680
  };
@@ -19540,7 +19686,7 @@ function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coales
19540
19686
  return Promise.resolve(persisted).then(
19541
19687
  () => {
19542
19688
  for (const e of edits) {
19543
- const key2 = keyOf3(e.element);
19689
+ const key2 = keyOf5(e.element);
19544
19690
  if (isLatestTimelineOptimisticGesture(updateElement, revision, key2)) applyEdit(e);
19545
19691
  }
19546
19692
  return true;
@@ -19556,34 +19702,29 @@ function persistMoveEdits(edits, deps, coalesceKey, operation = "timing", coales
19556
19702
  }
19557
19703
  );
19558
19704
  }
19559
- function insertTrackValue(trackOrder, insertRow) {
19560
- if (trackOrder.length === 0) return 0;
19561
- if (insertRow <= 0) return trackOrder[0] - 0.5;
19562
- if (insertRow >= trackOrder.length) return trackOrder[trackOrder.length - 1] + 0.5;
19563
- return (trackOrder[insertRow - 1] + trackOrder[insertRow]) / 2;
19564
- }
19565
19705
  function resolveMultiSelection(drag, deps) {
19566
19706
  const { elements, selectedKeys } = deps;
19567
- const dragKey = keyOf3(drag.element);
19707
+ const dragKey = keyOf5(drag.element);
19568
19708
  if (!selectedKeys || selectedKeys.size <= 1 || !selectedKeys.has(dragKey)) return null;
19569
19709
  const keys = new Set(
19570
19710
  [...selectedKeys].filter((k) => {
19571
- const el = elements.find((e) => keyOf3(e) === k);
19711
+ const el = elements.find((e) => keyOf5(e) === k);
19572
19712
  return el ? canMoveTimelineElement(el) : false;
19573
19713
  })
19574
19714
  );
19575
19715
  const delta = drag.previewStart - drag.element.start;
19576
- const movedStart = (e) => keyOf3(e) === dragKey ? drag.previewStart : Math.max(0, round3(e.start + delta));
19716
+ const movedStart = (e) => keyOf5(e) === dragKey ? drag.previewStart : Math.max(0, round32(e.start + delta));
19577
19717
  return { keys, movedStart };
19578
19718
  }
19579
- function commitDraggedClipMove(drag, deps) {
19719
+ function commitDraggedClipMove(rawDrag, deps) {
19720
+ const drag = { ...rawDrag, previewStart: resolveDragLandingStart(rawDrag, deps) };
19580
19721
  const hostAlias = resolveExpandedHostAlias(drag, deps);
19581
19722
  if (hostAlias) {
19582
19723
  commitDraggedClipMove(hostAlias.drag, { ...deps, selectedKeys: hostAlias.selectedKeys });
19583
19724
  return;
19584
19725
  }
19585
19726
  const { elements, updateElement, onMoveElement } = deps;
19586
- const dragKey = keyOf3(drag.element);
19727
+ const dragKey = keyOf5(drag.element);
19587
19728
  const isInsert = drag.insertRow != null;
19588
19729
  const laneChanged = drag.previewTrack !== drag.element.track;
19589
19730
  const aimTrack = drag.desiredTrack ?? drag.previewTrack;
@@ -19593,7 +19734,7 @@ function commitDraggedClipMove(drag, deps) {
19593
19734
  const delta = drag.previewStart - drag.element.start;
19594
19735
  if (delta === 0) return;
19595
19736
  if (multi) {
19596
- const edits2 = elements.filter((e) => multi.keys.has(keyOf3(e))).map((e) => ({
19737
+ const edits2 = elements.filter((e) => multi.keys.has(keyOf5(e))).map((e) => ({
19597
19738
  element: e,
19598
19739
  updates: { start: multi.movedStart(e), track: e.track }
19599
19740
  })).filter((e) => e.updates.start !== e.element.start);
@@ -19625,14 +19766,14 @@ function commitDraggedClipMove(drag, deps) {
19625
19766
  const edits = [dragEdit];
19626
19767
  if (multi) {
19627
19768
  for (const e of elements) {
19628
- if (keyOf3(e) === dragKey || !multi.keys.has(keyOf3(e))) continue;
19769
+ if (keyOf5(e) === dragKey || !multi.keys.has(keyOf5(e))) continue;
19629
19770
  const start = multi.movedStart(e);
19630
19771
  if (start !== e.start) edits.push({ element: e, updates: { start, track: e.track } });
19631
19772
  }
19632
19773
  }
19633
19774
  const candidate = elements.map((e) => {
19634
- if (keyOf3(e) === dragKey) return { ...e, start: drag.previewStart, track: drag.previewTrack };
19635
- if (multi?.keys.has(keyOf3(e))) return { ...e, start: multi.movedStart(e) };
19775
+ if (keyOf5(e) === dragKey) return { ...e, start: drag.previewStart, track: drag.previewTrack };
19776
+ if (multi?.keys.has(keyOf5(e))) return { ...e, start: multi.movedStart(e) };
19636
19777
  return e;
19637
19778
  });
19638
19779
  const multiKeys = multi ? multi.keys : null;
@@ -19660,45 +19801,30 @@ function commitDraggedClipMove(drag, deps) {
19660
19801
  ).catch(() => void 0);
19661
19802
  }
19662
19803
  function buildTrackInsertEdits(element, previewStart, insertRow, multi, deps) {
19663
- const { elements, trackOrder } = deps;
19664
- const editKey = keyOf3(element);
19665
- if (element.expandedParentStart != null) return null;
19666
- const targetTrack = insertTrackValue(trackOrder, insertRow);
19667
- const candidate = elements.map((e) => {
19668
- if (keyOf3(e) === editKey) return { ...e, start: previewStart, track: targetTrack };
19669
- if (multi?.keys.has(keyOf3(e))) return { ...e, start: multi.movedStart(e) };
19670
- return e;
19671
- });
19672
- const writableZone = classifyZone(element);
19673
- const writable = (src) => sameSourceFile(src, element) && classifyZone(src) === writableZone && src.expandedParentStart == null;
19674
- const topologyOrder = [...new Set(elements.filter(writable).map((e) => e.track))].sort(
19675
- (a, b) => a - b
19676
- );
19677
- const topologyInsertRow = topologyOrder.filter((track2) => track2 < targetTrack).length;
19678
- const topologyTargetTrack = insertTrackValue(topologyOrder, topologyInsertRow);
19679
- const normalized = normalizeToZones(
19680
- elements.filter(writable).map((e) => {
19681
- if (keyOf3(e) === editKey) {
19682
- return { ...e, start: previewStart, track: topologyTargetTrack };
19683
- }
19684
- if (multi?.keys.has(keyOf3(e))) return { ...e, start: multi.movedStart(e) };
19685
- return e;
19686
- })
19687
- );
19688
- const bySrc = new Map(elements.map((e) => [keyOf3(e), e]));
19804
+ const { elements } = deps;
19805
+ const editKey = keyOf5(element);
19806
+ const layout = layoutAfterTrackInsert(element, previewStart, insertRow, multi, deps);
19807
+ if (!layout) return null;
19808
+ const { normalized, targetTrack, writable } = layout;
19809
+ const bySrc = new Map(elements.map((e) => [keyOf5(e), e]));
19689
19810
  for (const norm of normalized) {
19690
- const src = bySrc.get(keyOf3(norm));
19811
+ const src = bySrc.get(keyOf5(norm));
19691
19812
  if (src && writable(src) && !canMoveTimelineElement(src) && norm.track !== (src.authoredTrack ?? src.track)) {
19692
19813
  console.warn(
19693
- `[Timeline] Track insert refused: locked clip ${keyOf3(src)} would need renumbering`
19814
+ `[Timeline] Track insert refused: locked clip ${keyOf5(src)} would need renumbering`
19694
19815
  );
19695
19816
  return null;
19696
19817
  }
19697
19818
  }
19819
+ const candidate = elements.map((e) => {
19820
+ if (keyOf5(e) === editKey) return { ...e, start: previewStart, track: targetTrack };
19821
+ if (multi?.keys.has(keyOf5(e))) return { ...e, start: multi.movedStart(e) };
19822
+ return e;
19823
+ });
19698
19824
  const edits = [];
19699
19825
  if (multi) {
19700
19826
  for (const src of elements) {
19701
- const srcKey = keyOf3(src);
19827
+ const srcKey = keyOf5(src);
19702
19828
  if (srcKey !== editKey && multi.keys.has(srcKey) && !writable(src) && canMoveTimelineElement(src)) {
19703
19829
  edits.push({
19704
19830
  element: src,
@@ -19709,15 +19835,18 @@ function buildTrackInsertEdits(element, previewStart, insertRow, multi, deps) {
19709
19835
  }
19710
19836
  }
19711
19837
  for (const norm of normalized) {
19712
- const src = bySrc.get(keyOf3(norm));
19838
+ const src = bySrc.get(keyOf5(norm));
19713
19839
  if (!src || !canMoveTimelineElement(src)) continue;
19714
- const start = keyOf3(norm) === editKey || multi?.keys.has(keyOf3(norm)) ? multi?.movedStart(src) ?? previewStart : src.start;
19840
+ const normKey = keyOf5(norm);
19841
+ let start = src.start;
19842
+ if (normKey === editKey) start = previewStart;
19843
+ else if (multi?.keys.has(normKey)) start = multi.movedStart(src);
19715
19844
  edits.push({ element: src, updates: { start, track: norm.track } });
19716
19845
  }
19717
19846
  return { candidate, edits };
19718
19847
  }
19719
19848
  function commitTrackInsert(drag, deps, multi) {
19720
- const dragKey = keyOf3(drag.element);
19849
+ const dragKey = keyOf5(drag.element);
19721
19850
  const built = buildTrackInsertEdits(
19722
19851
  drag.element,
19723
19852
  drag.previewStart,
@@ -19779,7 +19908,7 @@ function syncStackingForEdit(candidate, dragKey, currentLane, aimedLane, multiKe
19779
19908
  if (!readZIndex || !onStackingPatches) return Promise.resolve();
19780
19909
  if (aimedLane === currentLane) return Promise.resolve();
19781
19910
  const stackingEls = candidate.map((el, domIndex) => ({
19782
- key: keyOf3(el),
19911
+ key: keyOf5(el),
19783
19912
  start: el.start,
19784
19913
  duration: el.duration,
19785
19914
  track: el.track,
@@ -21721,9 +21850,10 @@ function useTimelineGeometry({
21721
21850
  useEffect24(() => {
21722
21851
  if (zoomMode !== "manual" || isDragging.current) return;
21723
21852
  const el = scrollRef.current;
21724
- const target = lastScrollLeftRef.current;
21725
- if (!el || target <= 0) return;
21853
+ if (!el) return;
21726
21854
  const raf = requestAnimationFrame(() => {
21855
+ const target = lastScrollLeftRef.current;
21856
+ if (target <= 0) return;
21727
21857
  const max = Math.max(0, el.scrollWidth - el.clientWidth);
21728
21858
  const next = Math.min(target, max);
21729
21859
  if (Math.abs(el.scrollLeft - next) > 0.5) el.scrollLeft = next;
@@ -21918,69 +22048,11 @@ function useResolvedTimelineEditCallbacks(overrides) {
21918
22048
  // src/player/components/useTrackGapMenu.ts
21919
22049
  import { useCallback as useCallback24, useEffect as useEffect27, useMemo as useMemo21, useState as useState21 } from "react";
21920
22050
 
21921
- // src/player/components/timelineGaps.ts
21922
- var TRACK_GAP_EPSILON_S = 1e-3;
21923
- var keyOf4 = (e) => e.key ?? e.id;
21924
- function laneGapFloor(elements) {
21925
- return Math.max(0, ...elements.map((e) => e.expandedParentStart ?? 0));
21926
- }
21927
- var round32 = (v) => Math.round(v * 1e3) / 1e3;
21928
- var endOf = (e) => e.start + e.duration;
21929
- function sortedLaneClips(elements) {
21930
- return [...elements].sort((a, b) => a.start - b.start || keyOf4(a).localeCompare(keyOf4(b)));
21931
- }
21932
- function resolveTrackGapAt(elements, time, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
21933
- const clips = sortedLaneClips(elements);
21934
- const occupied = clips.some((c) => time >= c.start - epsilon && time < endOf(c) - epsilon);
21935
- if (occupied) return null;
21936
- const following = clips.filter((c) => c.start > time - epsilon);
21937
- if (following.length === 0) return null;
21938
- const gapEnd = following[0].start;
21939
- const gapStart = Math.max(
21940
- floor,
21941
- ...clips.filter((c) => c.start <= time - epsilon).map((c) => endOf(c))
21942
- );
21943
- if (gapEnd - gapStart <= epsilon) return null;
21944
- return { gapStart, gapEnd, followingKeys: following.map(keyOf4) };
21945
- }
21946
- function resolveAllGapIntervals(elements, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
21947
- const gaps = [];
21948
- let cursor = floor;
21949
- for (const clip of sortedLaneClips(elements)) {
21950
- if (clip.start - cursor > epsilon) gaps.push({ start: cursor, end: clip.start });
21951
- cursor = Math.max(cursor, endOf(clip));
21952
- }
21953
- return gaps;
21954
- }
21955
- function resolveAllTrackGaps(elements, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
21956
- const shifts = [];
21957
- let cursor = floor;
21958
- for (const clip of sortedLaneClips(elements)) {
21959
- const newStart = round32(cursor);
21960
- if (Math.abs(newStart - clip.start) > epsilon) {
21961
- shifts.push({ key: keyOf4(clip), newStart });
21962
- }
21963
- cursor += clip.duration;
21964
- }
21965
- return shifts;
21966
- }
21967
- function resolveLaneEmptyIntervals(elements, end, epsilon = TRACK_GAP_EPSILON_S, floor = 0) {
21968
- const gaps = resolveAllGapIntervals(elements, epsilon, floor);
21969
- const maxEnd = Math.max(floor, ...elements.map(endOf));
21970
- if (end - maxEnd > epsilon) gaps.push({ start: maxEnd, end });
21971
- return gaps;
21972
- }
21973
- function resolveCloseGapShifts(elements, gap) {
21974
- const width = gap.gapEnd - gap.gapStart;
21975
- const followSet = new Set(gap.followingKeys);
21976
- return sortedLaneClips(elements).filter((c) => followSet.has(keyOf4(c))).map((c) => ({ key: keyOf4(c), newStart: Math.max(gap.gapStart, round32(c.start - width)) }));
21977
- }
21978
-
21979
22051
  // src/player/components/timelineGapCommit.ts
21980
- var keyOf5 = (e) => e.key ?? e.id;
22052
+ var keyOf6 = (e) => e.key ?? e.id;
21981
22053
  var gapCloseGestureSeq = 0;
21982
22054
  function resolveShiftedElements(elements, shifts) {
21983
- const byKey = new Map(elements.map((e) => [keyOf5(e), e]));
22055
+ const byKey = new Map(elements.map((e) => [keyOf6(e), e]));
21984
22056
  return shifts.flatMap((s) => {
21985
22057
  const element = byKey.get(s.key);
21986
22058
  return element ? [{ element, start: s.newStart }] : [];
@@ -22023,9 +22095,9 @@ function resolveMainTrackDeleteRippleShifts(survivingElements, deletedElements,
22023
22095
  for (const survivor of survivors) {
22024
22096
  const width = deletedMainTrack.filter((d) => d.start < survivor.start).reduce((sum, d) => sum + d.duration, 0);
22025
22097
  if (width <= 0) continue;
22026
- const newStart = round32(Math.max(laneGapFloor(survivors), survivor.start - width));
22098
+ const newStart = round3(Math.max(laneGapFloor(survivors), survivor.start - width));
22027
22099
  if (Math.abs(newStart - survivor.start) > TRACK_GAP_EPSILON_S) {
22028
- shifts.push({ key: keyOf5(survivor), newStart });
22100
+ shifts.push({ key: keyOf6(survivor), newStart });
22029
22101
  }
22030
22102
  }
22031
22103
  if (shifts.length === 0 || !canShiftTrackGapClips(survivors, shifts)) return null;
@@ -22608,6 +22680,14 @@ function computeRevealScroll(input) {
22608
22680
  };
22609
22681
  }
22610
22682
 
22683
+ // src/utils/retryBudget.ts
22684
+ function recordRetryAttempt(ref, id, maxRetries) {
22685
+ const previous = ref.current;
22686
+ const count = previous.id === id ? previous.count + 1 : 1;
22687
+ ref.current = { id, count };
22688
+ return count <= maxRetries;
22689
+ }
22690
+
22611
22691
  // src/player/components/useTimelineFocusCoordinator.ts
22612
22692
  function isCurrentRequest(request, projectId, sessionEpoch) {
22613
22693
  return request !== null && request.projectId === projectId && request.sessionEpoch === sessionEpoch;
@@ -22629,7 +22709,7 @@ function focusElement(container, targetId) {
22629
22709
  });
22630
22710
  return true;
22631
22711
  }
22632
- function scrollToTarget(container, resolution, elements, rowGeometry, pixelsPerSecond, contentOrigin, allowHorizontal) {
22712
+ function scrollToTarget(container, resolution, elements, rowGeometry, pixelsPerSecond, contentOrigin, allowHorizontal, lastScrollLeftRef) {
22633
22713
  const rowIndex = rowGeometry.getRowIndex(resolution.row.physicalTrackKey);
22634
22714
  if (rowIndex < 0) return false;
22635
22715
  const elementId2 = resolution.target.kind === "row" ? resolution.row.elementId : resolution.target.elementId;
@@ -22651,10 +22731,15 @@ function scrollToTarget(container, resolution, elements, rowGeometry, pixelsPerS
22651
22731
  stickyTop: RULER_H,
22652
22732
  allowHorizontal: allowHorizontal && left !== null
22653
22733
  });
22654
- if (target.left !== null) container.scrollLeft = target.left;
22734
+ if (target.left !== null) {
22735
+ container.scrollLeft = target.left;
22736
+ lastScrollLeftRef.current = target.left;
22737
+ }
22655
22738
  if (target.top !== null) container.scrollTop = target.top;
22656
22739
  return target.left !== null || target.top !== null;
22657
22740
  }
22741
+ var MAX_UNRESOLVED_FOCUS_RETRIES = 5;
22742
+ var UNRESOLVED_FOCUS_TIMEOUT_MS = 4e3;
22658
22743
  function useTimelineFocusCoordinator({
22659
22744
  scrollRef,
22660
22745
  logicalRows,
@@ -22666,12 +22751,14 @@ function useTimelineFocusCoordinator({
22666
22751
  viewportVersion,
22667
22752
  projectId,
22668
22753
  sessionEpoch,
22669
- syncScrollViewport
22754
+ syncScrollViewport,
22755
+ lastScrollLeftRef
22670
22756
  }) {
22671
22757
  const request = usePlayerStore((state) => state.timelineFocus);
22672
22758
  const previousRowsRef = useRef37(logicalRows);
22673
22759
  const resolvedRef = useRef37(null);
22674
22760
  const appliedRef = useRef37(null);
22761
+ const unresolvedFocusAttemptsRef = useRef37({ id: -1, count: 0 });
22675
22762
  const resolution = useMemo26(() => {
22676
22763
  if (isCurrentRequest(request, projectId, sessionEpoch)) {
22677
22764
  if (resolvedRef.current?.nonce !== request.nonce) {
@@ -22701,8 +22788,23 @@ function useTimelineFocusCoordinator({
22701
22788
  useEffect29(() => {
22702
22789
  if (!isCurrentRequest(request, projectId, sessionEpoch)) return;
22703
22790
  if (!resolution) {
22704
- usePlayerStore.getState().clearTimelineFocus(request.nonce);
22705
- return;
22791
+ const withinBudget = recordRetryAttempt(
22792
+ unresolvedFocusAttemptsRef,
22793
+ request.nonce,
22794
+ MAX_UNRESOLVED_FOCUS_RETRIES
22795
+ );
22796
+ if (!withinBudget) {
22797
+ usePlayerStore.getState().clearTimelineFocus(request.nonce);
22798
+ return;
22799
+ }
22800
+ const nonce = request.nonce;
22801
+ const timeout = setTimeout(() => {
22802
+ usePlayerStore.getState().clearTimelineFocus(nonce);
22803
+ }, UNRESOLVED_FOCUS_TIMEOUT_MS);
22804
+ return () => clearTimeout(timeout);
22805
+ }
22806
+ if (unresolvedFocusAttemptsRef.current.count !== 0) {
22807
+ unresolvedFocusAttemptsRef.current = { id: -1, count: 0 };
22706
22808
  }
22707
22809
  if (resolution.target.id !== request.id) {
22708
22810
  usePlayerStore.getState().requestTimelineFocus(resolution.target.id);
@@ -22720,7 +22822,8 @@ function useTimelineFocusCoordinator({
22720
22822
  rowGeometry,
22721
22823
  pixelsPerSecond,
22722
22824
  contentOrigin,
22723
- allowHorizontal
22825
+ allowHorizontal,
22826
+ lastScrollLeftRef
22724
22827
  )) {
22725
22828
  syncScrollViewport(container);
22726
22829
  }
@@ -22730,6 +22833,7 @@ function useTimelineFocusCoordinator({
22730
22833
  allowHorizontal,
22731
22834
  contentOrigin,
22732
22835
  elements,
22836
+ lastScrollLeftRef,
22733
22837
  pixelsPerSecond,
22734
22838
  projectId,
22735
22839
  request,
@@ -23037,7 +23141,8 @@ function useTimelineLogicalFocus(input) {
23037
23141
  viewportVersion: input.viewport,
23038
23142
  projectId,
23039
23143
  sessionEpoch: input.sessionEpoch,
23040
- syncScrollViewport: input.syncScrollViewport
23144
+ syncScrollViewport: input.syncScrollViewport,
23145
+ lastScrollLeftRef: input.lastScrollLeftRef
23041
23146
  });
23042
23147
  const rows = useTimelineRowVirtualization({
23043
23148
  scrollRef: input.scrollRef,
@@ -38885,9 +38990,14 @@ function isResolverDisagreement(session, hfId) {
38885
38990
  return false;
38886
38991
  }
38887
38992
  }
38888
- function shouldUseSdkCutover(flagEnabled, hasSession, hfId, ops) {
38889
- return flagEnabled && hasSession && !!hfId && ops.length > 0 && ops.every((o) => CUTOVER_OP_TYPES.has(o.type)) && // SDK edit ops target only the element hfId; child-scoped patch ops need the server path.
38890
- !hasChildScopedOp(ops) && !ops.some(mapsToReservedAttr) && !hasUnsafeHtmlAttributeOp(ops);
38993
+ function sdkCutoverIneligibleReason(hfId, ops) {
38994
+ if (!hfId) return "target_unaddressable";
38995
+ if (ops.length === 0) return "no_ops";
38996
+ if (!ops.every((o) => CUTOVER_OP_TYPES.has(o.type))) return "unsupported_op_type";
38997
+ if (hasChildScopedOp(ops)) return "child_scoped_op";
38998
+ if (ops.some(mapsToReservedAttr)) return "reserved_attribute";
38999
+ if (hasUnsafeHtmlAttributeOp(ops)) return "unsafe_html_attribute";
39000
+ return null;
38891
39001
  }
38892
39002
 
38893
39003
  // src/utils/sdkEditTransaction.ts
@@ -39097,11 +39207,11 @@ function gsapReadSource(deps, targetPath) {
39097
39207
  return read ? () => read(targetPath) : void 0;
39098
39208
  }
39099
39209
  async function sdkCutoverPersist(selection, ops, originalContent, targetPath, sdkSession, deps, options) {
39100
- if (!shouldUseSdkCutover(sdkFamilyEnabled("dom"), !!sdkSession, selection.hfId, ops))
39101
- return declinedCutover("ineligible_operation", "dom");
39210
+ if (!sdkFamilyEnabled("dom")) return declinedCutover("feature_disabled", "dom");
39102
39211
  if (!sdkSession) return declinedCutover("session_unavailable", "dom");
39103
39212
  const hfId = selection.hfId;
39104
- if (!hfId) return declinedCutover("target_unaddressable", "dom");
39213
+ const ineligible = sdkCutoverIneligibleReason(hfId, ops);
39214
+ if (ineligible || !hfId) return declinedCutover(ineligible ?? "target_unaddressable", "dom");
39105
39215
  const target = sdkSession.getElement(hfId);
39106
39216
  if (!target)
39107
39217
  return declinedCutover("target_not_found", "dom", isResolverDisagreement(sdkSession, hfId));
@@ -39676,12 +39786,12 @@ function useElementLifecycleOps({
39676
39786
  import { useCallback as useCallback55, useRef as useRef66 } from "react";
39677
39787
 
39678
39788
  // src/player/components/timelineZMirror.ts
39679
- var keyOf6 = (el) => el.key ?? el.id;
39789
+ var keyOf7 = (el) => el.key ?? el.id;
39680
39790
  function expandedChildAllowedLanes(element, elements) {
39681
39791
  if (element.expandedParentStart == null) return null;
39682
- const selfKey = keyOf6(element);
39792
+ const selfKey = keyOf7(element);
39683
39793
  return new Set(
39684
- elements.filter((el) => keyOf6(el) !== selfKey && sameSourceFile(el, element)).map((el) => el.track)
39794
+ elements.filter((el) => keyOf7(el) !== selfKey && sameSourceFile(el, element)).map((el) => el.track)
39685
39795
  );
39686
39796
  }
39687
39797
  function displayTrackOrder(elements) {
@@ -39691,12 +39801,12 @@ function resolveZMirrorLaneMove(input) {
39691
39801
  const { action, element, elements } = input;
39692
39802
  if (classifyZone(element) === "audio") return null;
39693
39803
  if (!(element.duration > 0)) return null;
39694
- const selfKey = keyOf6(element);
39804
+ const selfKey = keyOf7(element);
39695
39805
  const start = element.start;
39696
39806
  const end = element.start + element.duration;
39697
39807
  const up = action === "bring-forward" || action === "bring-to-front";
39698
39808
  const overlapSet = elements.filter(
39699
- (el) => keyOf6(el) !== selfKey && classifyZone(el) === "visual" && samePaintScope(el, element) && timeRangesOverlap(start, end, el.start, el.start + el.duration)
39809
+ (el) => keyOf7(el) !== selfKey && classifyZone(el) === "visual" && samePaintScope(el, element) && timeRangesOverlap(start, end, el.start, el.start + el.duration)
39700
39810
  );
39701
39811
  const referenceLane = resolveReferenceLane(input, overlapSet, up);
39702
39812
  if (referenceLane == null) return null;
@@ -39732,13 +39842,13 @@ function resolveRepositionLaneMove(input) {
39732
39842
  const { element, elements, desiredOrderKeys } = input;
39733
39843
  if (classifyZone(element) === "audio") return null;
39734
39844
  if (!(element.duration > 0)) return null;
39735
- const selfKey = keyOf6(element);
39845
+ const selfKey = keyOf7(element);
39736
39846
  const selfIdx = desiredOrderKeys.indexOf(selfKey);
39737
39847
  if (selfIdx === -1) return null;
39738
39848
  const clipLaneForKey = (key2) => {
39739
39849
  if (key2 == null) return null;
39740
- const el = elements.find((candidate) => keyOf6(candidate) === key2);
39741
- return el && classifyZone(el) === "visual" && el.duration > 0 && samePaintScope(el, element) && keyOf6(el) !== selfKey ? el.track : null;
39850
+ const el = elements.find((candidate) => keyOf7(candidate) === key2);
39851
+ return el && classifyZone(el) === "visual" && el.duration > 0 && samePaintScope(el, element) && keyOf7(el) !== selfKey ? el.track : null;
39742
39852
  };
39743
39853
  let aboveLane = null;
39744
39854
  for (let i = selfIdx + 1; i < desiredOrderKeys.length && aboveLane == null; i++) {
@@ -39817,7 +39927,7 @@ function resolveReferenceLane(input, overlapSet, up) {
39817
39927
  }
39818
39928
  function crossedNeighborLane({ elements, crossedKey }) {
39819
39929
  if (crossedKey == null) return null;
39820
- const crossed = elements.find((el) => keyOf6(el) === crossedKey);
39930
+ const crossed = elements.find((el) => keyOf7(el) === crossedKey);
39821
39931
  return crossed && classifyZone(crossed) === "visual" ? crossed.track : null;
39822
39932
  }
39823
39933
 
@@ -41789,6 +41899,7 @@ var CaptionTimeline = memo27(function CaptionTimeline2({
41789
41899
 
41790
41900
  // src/hooks/useTimelineSelectionPreviewSync.ts
41791
41901
  import { useEffect as useEffect49, useMemo as useMemo36, useRef as useRef68 } from "react";
41902
+ var MAX_UNRESOLVED_SYNC_RETRIES = 3;
41792
41903
  function orderSelectedIds(ids, anchor2) {
41793
41904
  const ordered = [...ids];
41794
41905
  if (!anchor2 || !ids.has(anchor2)) return ordered;
@@ -41806,6 +41917,22 @@ function selectionIdsMatch(currentIds, selectedIds, currentAnchor, wantedAnchor)
41806
41917
  function anchorIsOutsideSelection(anchor2, selectedIds) {
41807
41918
  return anchor2 !== null && !selectedIds.includes(anchor2);
41808
41919
  }
41920
+ async function resolveSelectionsForIds(ids, timelineElements, buildDomSelectionForTimelineElement) {
41921
+ const elements = ids.map((id) => timelineElements.find((item) => (item.key ?? item.id) === id)).filter((element) => Boolean(element));
41922
+ const resolved2 = await Promise.all(
41923
+ elements.map((element) => buildDomSelectionForTimelineElement(element))
41924
+ );
41925
+ return resolved2.filter((selection) => Boolean(selection));
41926
+ }
41927
+ function applyResolvedSelections(selections, applyDomSelection, applyMarqueeSelection) {
41928
+ if (selections.length === 0) {
41929
+ applyDomSelection(null, { revealPanel: false });
41930
+ } else if (selections.length === 1) {
41931
+ applyDomSelection(selections[0]);
41932
+ } else {
41933
+ applyMarqueeSelection(selections, false);
41934
+ }
41935
+ }
41809
41936
  function useTimelineSelectionPreviewSync({
41810
41937
  selectedElementId,
41811
41938
  selectedElementIds,
@@ -41827,6 +41954,7 @@ function useTimelineSelectionPreviewSync({
41827
41954
  const domEditGroupSelectionsRef = useRef68(domEditGroupSelections);
41828
41955
  const lastSyncedSelectedKeyRef = useRef68("");
41829
41956
  const missingSelectionKeyRef = useRef68("");
41957
+ const unresolvedAttemptsRef = useRef68({ id: "", count: 0 });
41830
41958
  domEditSelectionRef.current = domEditSelection;
41831
41959
  domEditGroupSelectionsRef.current = domEditGroupSelections;
41832
41960
  useEffect49(() => {
@@ -41841,6 +41969,7 @@ function useTimelineSelectionPreviewSync({
41841
41969
  const currentAnchor = currentDomEditSelection ? resolveTimelineIdForSelection(currentDomEditSelection, timelineElements, activeCompPath) : null;
41842
41970
  if (selectedIds.length === 0) {
41843
41971
  missingSelectionKeyRef.current = "";
41972
+ unresolvedAttemptsRef.current = { id: "", count: 0 };
41844
41973
  logSelect("timeline-empty", {
41845
41974
  had: currentIds.length,
41846
41975
  previousKey: previousSelectedKey.length > 0,
@@ -41862,36 +41991,30 @@ function useTimelineSelectionPreviewSync({
41862
41991
  onSelectionNotFound();
41863
41992
  };
41864
41993
  const syncSelection = async () => {
41865
- const selections = [];
41866
- let resolvableCount = 0;
41867
- for (const id of selectedIds) {
41868
- const element = timelineElements.find((item) => (item.key ?? item.id) === id);
41869
- if (!element) continue;
41870
- resolvableCount += 1;
41871
- const selection = await buildDomSelectionForTimelineElement(element);
41872
- if (selection) selections.push(selection);
41873
- }
41994
+ const selections = await resolveSelectionsForIds(
41995
+ selectedIds,
41996
+ timelineElements,
41997
+ buildDomSelectionForTimelineElement
41998
+ );
41874
41999
  if (cancelled) return;
41875
- if (selections.length < resolvableCount) {
41876
- warnSelectionMissingOnce();
41877
- if (anchorIsOutsideSelection(currentAnchor, selectedIds)) {
41878
- applyDomSelection(null, { revealPanel: false, announce: false });
42000
+ if (selections.length < selectedIds.length) {
42001
+ if (recordRetryAttempt(unresolvedAttemptsRef, selectedKey, MAX_UNRESOLVED_SYNC_RETRIES)) {
42002
+ if (anchorIsOutsideSelection(currentAnchor, selectedIds)) {
42003
+ applyDomSelection(null, { revealPanel: false, announce: false });
42004
+ }
42005
+ return;
41879
42006
  }
41880
- return;
42007
+ warnSelectionMissingOnce();
42008
+ } else {
42009
+ unresolvedAttemptsRef.current = { id: "", count: 0 };
42010
+ missingSelectionKeyRef.current = "";
41881
42011
  }
41882
- missingSelectionKeyRef.current = "";
41883
42012
  logSelect("timeline-sync", {
41884
42013
  wanted: selectedIds.length,
41885
42014
  had: currentIds.length,
41886
42015
  resolved: selections.length
41887
42016
  });
41888
- if (selections.length === 0) {
41889
- applyDomSelection(null, { revealPanel: false });
41890
- } else if (selections.length === 1) {
41891
- applyDomSelection(selections[0]);
41892
- } else {
41893
- applyMarqueeSelection(selections, false);
41894
- }
42017
+ applyResolvedSelections(selections, applyDomSelection, applyMarqueeSelection);
41895
42018
  };
41896
42019
  void syncSelection();
41897
42020
  return () => {
@@ -66615,7 +66738,7 @@ async function commitTimelineCompositionInsertion(input) {
66615
66738
  await input.writeFile(input.targetPath, result.before, result.after);
66616
66739
  throw error;
66617
66740
  }
66618
- input.selectHost(`${input.targetPath}#${result.hostId}`);
66741
+ input.selectHost(deriveTimelineStoreKeyForDomId(result.hostId, input.targetPath));
66619
66742
  try {
66620
66743
  input.resync?.();
66621
66744
  } catch (error) {
@@ -66629,6 +66752,13 @@ async function commitTimelineCompositionInsertion(input) {
66629
66752
  });
66630
66753
  }
66631
66754
 
66755
+ // src/player/components/timelineDropReveal.ts
66756
+ function selectAndRevealTimelineElement(key2) {
66757
+ const store = usePlayerStore.getState();
66758
+ store.setSelectedElementId(key2);
66759
+ store.requestTimelineFocus(timelineClipFocusId(key2));
66760
+ }
66761
+
66632
66762
  // src/hooks/useTimelineAssetDropOps.ts
66633
66763
  function useTimelineAssetDropOps({
66634
66764
  projectIdRef,
@@ -66699,6 +66829,7 @@ function useTimelineAssetDropOps({
66699
66829
  writeFile: writeProjectFile,
66700
66830
  recordEdit
66701
66831
  });
66832
+ selectAndRevealTimelineElement(deriveTimelineStoreKeyForDomId(newId, targetPath));
66702
66833
  forceReloadSdkSession?.();
66703
66834
  reloadPreview();
66704
66835
  } catch (error) {
@@ -66768,7 +66899,7 @@ function useTimelineAssetDropOps({
66768
66899
  writeFile: writeProjectFile,
66769
66900
  recordEdit,
66770
66901
  observeVersion: observeProjectFileVersion,
66771
- selectHost: (key2) => usePlayerStore.getState().setSelectedElementId(key2),
66902
+ selectHost: selectAndRevealTimelineElement,
66772
66903
  resync: forceReloadSdkSession,
66773
66904
  refresh: reloadPreview
66774
66905
  });
@@ -75724,8 +75855,9 @@ async function addBlockToProject(opts) {
75724
75855
  currentTime: opts.currentTime ?? 0
75725
75856
  });
75726
75857
  const { width, height } = resolveTimelineAssetCompositionSize(originalContent);
75858
+ const hostId = buildUniqueCompositionId(block.name, collectHtmlIds(originalContent));
75727
75859
  const subComposition = buildSubCompositionHtml({
75728
- id: buildUniqueCompositionId(block.name, collectHtmlIds(originalContent)),
75860
+ id: hostId,
75729
75861
  compositionFile,
75730
75862
  start,
75731
75863
  duration,
@@ -75751,7 +75883,11 @@ async function addBlockToProject(opts) {
75751
75883
  });
75752
75884
  await refreshFileTree();
75753
75885
  reloadPreview();
75754
- return { block, compositionPath: compositionFile };
75886
+ return {
75887
+ block,
75888
+ compositionPath: compositionFile,
75889
+ hostKey: deriveTimelineStoreKeyForDomId(hostId, targetPath)
75890
+ };
75755
75891
  } catch (error) {
75756
75892
  const message = error instanceof Error ? error.message : "Failed to add block";
75757
75893
  showToast(message);
@@ -75851,7 +75987,9 @@ function useBlockHandlers({
75851
75987
  previewIframe: previewIframeRef.current,
75852
75988
  currentTime: usePlayerStore.getState().currentTime
75853
75989
  })
75854
- );
75990
+ ).then((result) => {
75991
+ if (result) selectAndRevealTimelineElement(result.hostKey);
75992
+ });
75855
75993
  },
75856
75994
  [projectId, blockCtx, previewIframeRef, runBlockInstall]
75857
75995
  );
@@ -75887,7 +76025,9 @@ function useBlockHandlers({
75887
76025
  previewIframe: previewIframeRef.current,
75888
76026
  currentTime: usePlayerStore.getState().currentTime
75889
76027
  })
75890
- );
76028
+ ).then((result) => {
76029
+ if (result) selectAndRevealTimelineElement(result.hostKey);
76030
+ });
75891
76031
  },
75892
76032
  [projectId, blockCtx, previewIframeRef, runBlockInstall]
75893
76033
  );