diff --git a/packages/studio/src/player/components/timelineEditing.ts b/packages/studio/src/player/components/timelineEditing.ts index c7293d90f..cba503c42 100644 --- a/packages/studio/src/player/components/timelineEditing.ts +++ b/packages/studio/src/player/components/timelineEditing.ts @@ -107,18 +107,16 @@ export function resolveTimelineMove( Math.max(0, input.maxStart), ); - // Stacking mode: vertical movement writes z-index only. The authored - // data-track-index is preserved even when the pointer crosses rows. + // Stacking mode: the two axes never fight. Horizontal movement writes time + // (nextStart); vertical movement writes z-index. Lane/overlap resolution + // uses the clip's authored time span, NOT the dragged start — otherwise a + // diagonal drag that drifts the clip out of overlap silently flips the + // placement from "restack" to "join lane" and cancels the reorder. if (input.stackingElement) { - const stackingElement = { - ...input.stackingElement, - start: nextStart, - duration: input.duration, - }; const layerMove = input.timelineLayers && input.layerOrder ? resolveTimelineLayerStackingMove({ - element: stackingElement, + element: { ...input.stackingElement, duration: input.duration }, layers: input.timelineLayers, layerOrder: input.layerOrder, trackDeltaRaw, diff --git a/packages/studio/src/player/components/useTimelineClipDrag.test.tsx b/packages/studio/src/player/components/useTimelineClipDrag.test.tsx index 0c71c0ffb..a90c598f3 100644 --- a/packages/studio/src/player/components/useTimelineClipDrag.test.tsx +++ b/packages/studio/src/player/components/useTimelineClipDrag.test.tsx @@ -202,13 +202,17 @@ describe("useTimelineClipDrag", () => { harness.unmount(); }); - it("uses the preview start when resolving whether a dragged clip can join a lane", async () => { + it("resolves lane stacking from the authored time span, independent of horizontal drag", async () => { const front = timelineElement({ id: "front", track: 0, zIndex: 3 }); const back = timelineElement({ id: "back", track: 1, zIndex: 1 }); back.start = 0; front.start = 0; const harness = renderDragHarness([front, back]); + // Drag up one row AND rightward in time. The horizontal drift moves the + // clip out of overlap, but the two axes never fight: the vertical restack + // is resolved from the authored (overlapping) span, so it still inserts + // above the target lane rather than silently joining it. harness.startDrag(back, 1); harness.movePointer(200, -TRACK_H); await harness.dropPointer(); @@ -219,8 +223,8 @@ describe("useTimelineClipDrag", () => { track: 1, stackingReorder: { contextKey: "root", - placement: { type: "onto", layerId: harness.layers[0]!.id }, - zIndexChanges: [{ key: "back", zIndex: 3 }], + placement: { type: "above", layerId: harness.layers[0]!.id }, + zIndexChanges: [{ key: "back", zIndex: 4 }], }, });