Files
hyperframes/packages/studio/src/player/components/timelineDragDrop.ts
T
Miguel Ángel ab08260201 refactor(studio): extract shared timeline components and deduplicate code (#1329)
Extract shared utilities to reduce duplication across timeline components:

- PlayheadIndicator: shared playhead rendering (was duplicated in
  TimelineCanvas and TimelineEditorNotice)
- useContextMenuDismiss: outside-click/Escape dismiss pattern (was
  duplicated in ClipContextMenu and KeyframeDiamondContextMenu)
- TimelineCallbacks: shared callback interfaces for drop and edit
  operations (was duplicated in NLELayout and Timeline props)
- useTimelineZoom: consolidated zoom store selectors
- timelineElementSplit: shared canSplitElement, buildPatchTarget, and
  readFileContent utilities
- gsapParser.test-helpers: shared test utilities for parser specs
2026-06-10 23:45:03 -04:00

92 lines
3.1 KiB
TypeScript

import { useCallback, useState, type RefObject } from "react";
import { TIMELINE_ASSET_MIME, TIMELINE_BLOCK_MIME } from "../../utils/timelineAssetDrop";
import { TRACK_H, resolveTimelineAssetDrop } from "./timelineLayout";
import type { TimelineDropCallbacks } from "./timelineCallbacks";
interface UseTimelineAssetDropOptions extends TimelineDropCallbacks {
scrollRef: RefObject<HTMLDivElement | null>;
ppsRef: RefObject<number>;
durationRef: RefObject<number>;
trackOrderRef: RefObject<number[]>;
}
export function useTimelineAssetDrop({
scrollRef,
ppsRef,
durationRef,
trackOrderRef,
onFileDrop,
onAssetDrop,
onBlockDrop,
}: UseTimelineAssetDropOptions) {
const [isDragOver, setIsDragOver] = useState(false);
const handleAssetDragOver = useCallback((e: React.DragEvent) => {
const hasFiles = e.dataTransfer.files.length > 0;
const types = Array.from(e.dataTransfer.types);
const hasAsset = types.includes(TIMELINE_ASSET_MIME);
const hasBlock = types.includes(TIMELINE_BLOCK_MIME);
if (!hasFiles && !hasAsset && !hasBlock) return;
e.preventDefault();
if (hasAsset || hasBlock) e.dataTransfer.dropEffect = "copy";
setIsDragOver(true);
}, []);
const handleAssetDrop = useCallback(
// fallow-ignore-next-line complexity
(e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const scroll = scrollRef.current;
const rect = scroll?.getBoundingClientRect();
const dropInput = {
rectLeft: rect?.left ?? 0,
rectTop: rect?.top ?? 0,
scrollLeft: scroll?.scrollLeft ?? 0,
scrollTop: scroll?.scrollTop ?? 0,
pixelsPerSecond: ppsRef.current,
duration: durationRef.current,
trackHeight: TRACK_H,
trackOrder: trackOrderRef.current,
};
if (onFileDrop && e.dataTransfer.files.length > 0) {
void onFileDrop(
Array.from(e.dataTransfer.files),
scroll && rect ? resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY) : undefined,
);
return;
}
const assetPayload = e.dataTransfer.getData(TIMELINE_ASSET_MIME);
if (assetPayload && onAssetDrop && scroll && rect) {
try {
const parsed = JSON.parse(assetPayload) as { path?: string };
if (parsed.path)
void onAssetDrop(
parsed.path,
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
);
} catch {
/* ignore malformed drag payloads */
}
return;
}
const blockPayload = e.dataTransfer.getData(TIMELINE_BLOCK_MIME);
if (blockPayload && onBlockDrop && scroll && rect) {
try {
const parsed = JSON.parse(blockPayload) as { name?: string };
if (parsed.name)
void onBlockDrop(
parsed.name,
resolveTimelineAssetDrop(dropInput, e.clientX, e.clientY),
);
} catch {
/* ignore malformed drag payloads */
}
}
},
[onAssetDrop, onBlockDrop, onFileDrop, scrollRef, ppsRef, durationRef, trackOrderRef],
);
return { isDragOver, setIsDragOver, handleAssetDragOver, handleAssetDrop };
}