fix(studio): harden composition timeline reliability (#2615)

* fix(studio): preserve composition playback continuity

* feat(studio): drag compositions into the timeline

* fix(studio): collapse expanded composition move aliases

* fix(studio): make timeline cuts atomic

* fix(studio): group inspector gesture history

* test(studio): cover masked text selection

* fix(studio): harden composition timeline reliability

* fix(studio): satisfy CI source gates

* fix(studio): harden composition mutation requests
This commit is contained in:
Miguel Ángel
2026-07-17 14:15:30 -04:00
committed by GitHub
parent 2be8a62c00
commit 2b65b4efce
93 changed files with 3925 additions and 758 deletions
@@ -1,5 +1,9 @@
import { useCallback, useState, type RefObject } from "react";
import { TIMELINE_ASSET_MIME, TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
import {
parseTimelineCompositionPayload,
TIMELINE_COMPOSITION_MIME,
} from "../../utils/timelineCompositionDrop";
import { usePlayerStore } from "../store/playerStore";
import { TRACK_H, resolveTimelineAssetDrop } from "./timelineLayout";
import type { TimelineDropCallbacks } from "./timelineCallbacks";
@@ -32,6 +36,11 @@ function applyJsonDropPayload(
}
}
function resolveDropStart(usePointerStart: boolean, pointerStart: number): number {
if (usePointerStart) return pointerStart;
return Math.max(0, usePlayerStore.getState().currentTime);
}
/**
* Dropping an asset/file/block onto the timeline places it at the PLAYHEAD —
* start is the current playhead time, only the track comes from the drop y.
@@ -48,6 +57,7 @@ export function useTimelineAssetDrop({
onFileDrop,
onAssetDrop,
onBlockDrop,
onCompositionDrop,
}: UseTimelineAssetDropOptions) {
const [isDragOver, setIsDragOver] = useState(false);
@@ -56,7 +66,8 @@ export function useTimelineAssetDrop({
const hasFiles = types.includes("Files");
const hasAsset = types.includes(TIMELINE_ASSET_MIME);
const hasBlock = types.includes(TIMELINE_BLOCK_MIME);
if (!hasFiles && !hasAsset && !hasBlock) return;
const hasComposition = types.includes(TIMELINE_COMPOSITION_MIME);
if (!hasFiles && !hasAsset && !hasBlock && !hasComposition) return;
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setIsDragOver(true);
@@ -65,11 +76,10 @@ export function useTimelineAssetDrop({
const clearDropPreview = useCallback(() => setIsDragOver(false), []);
const resolveDropPlacement = useCallback(
(clientX: number, clientY: number): TimelinePlacement => {
(clientX: number, clientY: number, usePointerStart = false): TimelinePlacement => {
const scroll = scrollRef.current;
const rect = scroll?.getBoundingClientRect();
// Track comes from the vertical drop position; start is the playhead.
const { track } = resolveTimelineAssetDrop(
const pointer = resolveTimelineAssetDrop(
{
rectLeft: rect?.left ?? 0,
rectTop: rect?.top ?? 0,
@@ -77,14 +87,17 @@ export function useTimelineAssetDrop({
scrollTop: scroll?.scrollTop ?? 0,
pixelsPerSecond: ppsRef.current,
duration: durationRef.current,
clampStartToDuration: !usePointerStart,
trackHeight: TRACK_H,
trackOrder: trackOrderRef.current,
},
clientX,
clientY,
);
const start = Math.max(0, usePlayerStore.getState().currentTime);
return { start, track };
return {
start: resolveDropStart(usePointerStart, pointer.start),
track: pointer.track,
};
},
[scrollRef, ppsRef, durationRef, trackOrderRef],
);
@@ -93,6 +106,14 @@ export function useTimelineAssetDrop({
(e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const compositionPayload = parseTimelineCompositionPayload(
e.dataTransfer.getData(TIMELINE_COMPOSITION_MIME),
);
if (compositionPayload && onCompositionDrop) {
const placement = resolveDropPlacement(e.clientX, e.clientY, true);
void onCompositionDrop(compositionPayload.sourcePath, placement);
return;
}
const placement = resolveDropPlacement(e.clientX, e.clientY);
if (onFileDrop && e.dataTransfer.files.length > 0) {
@@ -109,7 +130,7 @@ export function useTimelineAssetDrop({
applyJsonDropPayload(blockPayload, (p) => p.name, onBlockDrop, placement);
}
},
[resolveDropPlacement, onFileDrop, onAssetDrop, onBlockDrop],
[resolveDropPlacement, onFileDrop, onAssetDrop, onBlockDrop, onCompositionDrop],
);
return { isDragOver, handleAssetDragOver, handleAssetDrop, clearDropPreview };