From 5bc99ce03e3a8e4e7dc7106a3d3ffa7eda0d28d7 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 4 Feb 2026 16:24:47 +1300 Subject: [PATCH] fix: replace HTML5 drag with pointer events for clip reorder HTML5 drag-and-drop doesn't work inside React Flow nodes because React Flow captures pointer events at a higher level. Switch to pointer-event-based reorder with visual feedback (dragged item dims, hover target highlights with blue ring). Co-Authored-By: Claude Opus 4.5 --- src/components/nodes/VideoStitchNode.tsx | 78 +++++++++++++++--------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/components/nodes/VideoStitchNode.tsx b/src/components/nodes/VideoStitchNode.tsx index 7188efac..b7747f2b 100644 --- a/src/components/nodes/VideoStitchNode.tsx +++ b/src/components/nodes/VideoStitchNode.tsx @@ -163,44 +163,56 @@ export function VideoStitchNode({ id, data, selected }: NodeProps(null); + const [hoverClipId, setHoverClipId] = useState(null); - const handleDragStart = useCallback((e: React.DragEvent, edgeId: string) => { + const handlePointerDown = useCallback((e: React.PointerEvent, edgeId: string) => { + // Only left mouse button + if (e.button !== 0) return; + e.stopPropagation(); setDraggedClipId(edgeId); - e.dataTransfer.effectAllowed = "move"; + setHoverClipId(null); + (e.target as HTMLElement).setPointerCapture(e.pointerId); }, []); - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }, []); - - const handleDrop = useCallback( - (e: React.DragEvent, targetEdgeId: string) => { - e.preventDefault(); - if (!draggedClipId || draggedClipId === targetEdgeId) { - setDraggedClipId(null); + const handlePointerMove = useCallback((e: React.PointerEvent) => { + if (!draggedClipId) return; + // Find which clip element the pointer is over + const elementsUnder = document.elementsFromPoint(e.clientX, e.clientY); + for (const el of elementsUnder) { + const clipEl = (el as HTMLElement).closest("[data-clip-id]") as HTMLElement | null; + if (clipEl) { + const targetId = clipEl.dataset.clipId!; + if (targetId !== draggedClipId) { + setHoverClipId(targetId); + } return; } + } + setHoverClipId(null); + }, [draggedClipId]); - const currentOrder = [...nodeData.clipOrder]; - const draggedIndex = currentOrder.indexOf(draggedClipId); - const targetIndex = currentOrder.indexOf(targetEdgeId); + const handlePointerUp = useCallback(() => { + if (!draggedClipId || !hoverClipId || draggedClipId === hoverClipId) { + setDraggedClipId(null); + setHoverClipId(null); + return; + } - if (draggedIndex === -1 || targetIndex === -1) { - setDraggedClipId(null); - return; - } + const currentOrder = [...(nodeData.clipOrder || [])]; + const draggedIndex = currentOrder.indexOf(draggedClipId); + const targetIndex = currentOrder.indexOf(hoverClipId); + if (draggedIndex !== -1 && targetIndex !== -1) { currentOrder.splice(draggedIndex, 1); currentOrder.splice(targetIndex, 0, draggedClipId); - updateNodeData(id, { clipOrder: currentOrder }); - setDraggedClipId(null); - }, - [draggedClipId, nodeData.clipOrder, id, updateNodeData] - ); + } + + setDraggedClipId(null); + setHoverClipId(null); + }, [draggedClipId, hoverClipId, nodeData.clipOrder, id, updateNodeData]); const handleRemoveClip = useCallback( (edgeId: string) => { @@ -404,11 +416,17 @@ export function VideoStitchNode({ id, data, selected }: NodeProps handleDragStart(e, clip.edgeId)} - onDragOver={handleDragOver} - onDrop={(e) => handleDrop(e, clip.edgeId)} - className="nodrag relative w-full aspect-video bg-neutral-800 border border-neutral-600 rounded cursor-move hover:border-neutral-500 transition-colors group" + data-clip-id={clip.edgeId} + onPointerDown={(e) => handlePointerDown(e, clip.edgeId)} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + className={`nodrag relative w-full aspect-video bg-neutral-800 border rounded cursor-move transition-colors group ${ + draggedClipId === clip.edgeId + ? "opacity-50 border-blue-500" + : hoverClipId === clip.edgeId && draggedClipId + ? "border-blue-400 ring-1 ring-blue-400/50" + : "border-neutral-600 hover:border-neutral-500" + }`} > {thumbnail ? (