fix(studio): stop horizontal drag from cancelling a vertical restack

Lane/overlap resolution now uses the clip's authored time span instead
of the live dragged start. A diagonal drag that drifts the clip out of
overlap no longer flips the placement from restack to lane-join, so the
two axes never fight.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-08 23:46:29 -04:00
parent ce0ddbccf8
commit db6ccb8082
2 changed files with 13 additions and 11 deletions
@@ -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,
@@ -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 }],
},
});