@ -163,44 +163,56 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
extractThumbnails ( ) ;
extractThumbnails ( ) ;
} , [ orderedClips ] ) ;
} , [ orderedClips ] ) ;
// Drag and drop for reordering clips
// Pointer-based drag reorder (HTML5 drag doesn't work inside React Flow nodes)
const [ draggedClipId , setDraggedClipId ] = useState < string | null > ( null ) ;
const [ draggedClipId , setDraggedClipId ] = useState < string | null > ( null ) ;
const [ hoverClipId , setHoverClipId ] = useState < string | null > ( 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 ) ;
setDraggedClipId ( edgeId ) ;
e . dataTransfer . effectAllowed = "move" ;
setHoverClipId ( null ) ;
( e . target as HTMLElement ) . setPointerCapture ( e . pointerId ) ;
} , [ ] ) ;
} , [ ] ) ;
const handleDragOver = useCallback ( ( e : React.DragEvent ) = > {
const handlePointerMove = useCallback ( ( e : React.PointerEvent ) = > {
e . preventDefault ( ) ;
if ( ! draggedClipId ) return ;
e . dataTransfer . dropEffect = "move" ;
// Find which clip element the pointer is over
} , [ ] ) ;
const elementsUnder = document . elementsFromPoint ( e . clientX , e . clientY ) ;
for ( const el of elementsUnder ) {
const handleDrop = useCallback (
const clipEl = ( el as HTMLElement ) . closest ( "[data-clip-id]" ) as HTMLElement | null ;
( e : React.DragEvent , targetEdgeId : string ) = > {
if ( clipEl ) {
e . preventDefault ( ) ;
const targetId = clipEl . dataset . clipId ! ;
if ( ! draggedClipId || draggedClipId === targetEdgeId ) {
if ( targetId !== draggedClipId ) {
setDraggedClipId ( null ) ;
setHoverClipId ( targetId ) ;
}
return ;
return ;
}
}
}
setHoverClipId ( null ) ;
} , [ draggedClipId ] ) ;
const currentOrder = [ . . . nodeData . clipOrder ] ;
const handlePointerUp = useCallback ( ( ) = > {
const draggedIndex = currentOrder . indexOf ( draggedClipId ) ;
if ( ! draggedClipId || ! hoverClipId || draggedClipId === hoverClipId ) {
const targetIndex = currentOrder . indexOf ( targetEdgeId ) ;
setDraggedClipId ( null ) ;
setHoverClipId ( null ) ;
return ;
}
if ( draggedIndex === - 1 || targetIndex === - 1 ) {
const currentOrder = [ . . . ( nodeData . clipOrder || [ ] ) ] ;
setDraggedClipId ( null ) ;
const draggedIndex = currentOrder . indexOf ( draggedClipId ) ;
return ;
const targetIndex = currentOrder . indexOf ( hoverClipId ) ;
}
if ( draggedIndex !== - 1 && targetIndex !== - 1 ) {
currentOrder . splice ( draggedIndex , 1 ) ;
currentOrder . splice ( draggedIndex , 1 ) ;
currentOrder . splice ( targetIndex , 0 , draggedClipId ) ;
currentOrder . splice ( targetIndex , 0 , draggedClipId ) ;
updateNodeData ( id , { clipOrder : currentOrder } ) ;
updateNodeData ( id , { clipOrder : currentOrder } ) ;
setDraggedClipId ( null ) ;
}
} ,
[ draggedClipId , nodeData . clipOrder , id , updateNodeData ]
setDraggedClipId ( null ) ;
) ;
setHoverClipId ( null ) ;
} , [ draggedClipId , hoverClipId , nodeData . clipOrder , id , updateNodeData ] ) ;
const handleRemoveClip = useCallback (
const handleRemoveClip = useCallback (
( edgeId : string ) = > {
( edgeId : string ) = > {
@ -404,11 +416,17 @@ export function VideoStitchNode({ id, data, selected }: NodeProps<VideoStitchNod
return (
return (
< div
< div
key = { clip . edgeId }
key = { clip . edgeId }
draggable
data - clip - id = { clip . edgeId }
onDragStart = { ( e ) = > handleDragStart ( e , clip . edgeId ) }
onPointerDown = { ( e ) = > handlePointerDown ( e , clip . edgeId ) }
onDragOver = { handleDragOver }
onPointerMove = { handlePointerMove }
onDrop = { ( e ) = > handleDrop ( e , clip . edgeId ) }
onPointerUp = { handlePointerUp }
className = "nodrag relative w-full aspect-video bg-neutral-800 border border-neutral-600 rounded cursor-move hover:border-neutral-500 transition-colors group"
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 ? (
{ thumbnail ? (
< img
< img