import React, { createContext, useContext, useState, useRef, useEffect, useCallback } from 'react';
const DndContext = createContext(null);
export function DndProvider({ children, onDragStart, onDragEnd }) {
const [active, setActive] = useState(null);
const [over, setOver] = useState(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const draggablesRef = useRef(new Map());
const droppablesRef = useRef(new Map());
const stateRef = useRef({ active, over, onDragEnd });
useEffect(() => {
stateRef.current = { active, over, onDragEnd };
});
const findDroppableUnderPoint = (x, y) => {
const currentActiveId = stateRef.current.active?.id;
const droppables = Array.from(droppablesRef.current.entries()).reverse();
for (const [id, droppable] of droppables) {
const { element, data } = droppable;
if (element && currentActiveId !== id) {
const rect = element.getBoundingClientRect();
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
return { id, data };
}
}
}
return null;
};
const handleMouseMove = React.useCallback((event) => {
setMousePosition({ x: event.clientX, y: event.clientY });
const over = findDroppableUnderPoint(event.clientX, event.clientY);
setOver(over);
}, []);
const handleMouseUp = React.useCallback(() => {
const { active, over, onDragEnd } = stateRef.current;
if (onDragEnd && active) {
onDragEnd({ active, over });
}
setActive(null);
setOver(null);
setDragOffset({ x: 0, y: 0 });
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}, [handleMouseMove]);
const handleMouseDown = (event, draggableId, data) => {
const draggableElement = draggablesRef.current.get(draggableId)?.element;
if (!draggableElement) return;
const rect = draggableElement.getBoundingClientRect();
setActive({ id: draggableId, data });
setDragOffset({
x: event.clientX - rect.left,
y: event.clientY - rect.top,
});
setMousePosition({ x: event.clientX, y: event.clientY });
if (onDragStart) {
onDragStart({ active: { id: draggableId, data } });
}
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
event.preventDefault();
};
const registerDraggable = (id, element, data) => {
draggablesRef.current.set(id, { element, data });
};
const unregisterDraggable = (id) => {
draggablesRef.current.delete(id);
};
const registerDroppable = (id, element, data) => {
droppablesRef.current.set(id, { element, data });
};
const unregisterDroppable = (id) => {
droppablesRef.current.delete(id);
};
const contextValue = {
active,
over,
mousePosition,
dragOffset,
handleMouseDown,
registerDraggable,
unregisterDraggable,
registerDroppable,
unregisterDroppable,
};
return (
<DndContext.Provider value={contextValue}>
{children}
</DndContext.Provider>
);
}
export function useDraggable({ id, data }) {
const context = useContext(DndContext);
if (!context) throw new Error('useDraggable must be used within a DndProvider');
const nodeRef = useRef(null);
useEffect(() => {
if (nodeRef.current) {
context.registerDraggable(id, nodeRef.current, data);
}
return () => context.unregisterDraggable(id);
}, [id, data, context]);
const isDragging = context.active?.id === id;
const transform = isDragging ? {
x: context.mousePosition.x - context.dragOffset.x,
y: context.mousePosition.y - context.dragOffset.y,
} : null;
return {
setNodeRef: (node) => {
nodeRef.current = node;
},
attributes: {
'data-draggable-id': id,
},
listeners: {
onMouseDown: (event) => {
context.handleMouseDown(event, id, data);
},
},
isDragging,
transform,
};
}
export function useDroppable({ id, data }) {
const context = useContext(DndContext);
if (!context) throw new Error('useDroppable must be used within a DndProvider');
const nodeRef = useRef(null);
useEffect(() => {
if (nodeRef.current) {
context.registerDroppable(id, nodeRef.current, data);
}
return () => context.unregisterDroppable(id);
}, [id, data, context]);
const isOver = context.over?.id === id;
return {
setNodeRef: (node) => {
nodeRef.current = node;
},
isOver,
};
}
export function useSortable({ id, data }) {
const draggable = useDraggable({ id, data });
const droppable = useDroppable({ id, data });
return {
...draggable,
isOver: droppable.isOver,
setNodeRef: (node) => {
draggable.setNodeRef(node);
droppable.setNodeRef(node);
},
};
}
function SortableItem({ item, index }) {
const {
attributes,
listeners,
setNodeRef,
transform,
isDragging,
isOver,
} = useSortable({
id: item.id,
data: { item, index },
});
const placeholderStyle = {
visibility: isDragging ? 'hidden' : 'visible',
border: '1px solid #ddd',
borderTop: isOver ? '2px solid #2196f3' : '1px solid #ddd',
padding: '12px',
margin: '4px 0',
borderRadius: '4px',
backgroundColor: 'white',
transition: 'border-top-color 0.2s ease',
};
const dragOverlayStyle = {
position: 'fixed',
top: transform?.y ?? 0,
left: transform?.x ?? 0,
width: '580px',
zIndex: 1000,
pointerEvents: 'none',
boxShadow: '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)',
cursor: 'grabbing',
userSelect: 'none',
padding: '12px',
margin: '4px 0',
borderRadius: '4px',
backgroundColor: 'white',
border: '1px solid #ddd',
};
return (
<>
{}
<div
ref={setNodeRef}
style={placeholderStyle}
{...attributes}
{...listeners}
>
<strong>{item.name}</strong>
<br />
<small>{item.email}</small>
</div>
{}
{isDragging && (
<div style={dragOverlayStyle}>
<strong>{item.name}</strong>
<br />
<small>{item.email}</small>
</div>
)}
</>
);
}
export default function MiniDndDemo() {
const [items, setItems] = useState([
{ id: '1', name: 'John Smith', email: '[email protected]' },
{ id: '2', name: 'Jane Doe', email: '[email protected]' },
{ id: '3', name: 'Bob Johnson', email: '[email protected]' },
{ id: '4', name: 'Alice Brown', email: '[email protected]' },
{ id: '5', name: 'Charlie Davis', email: '[email protected]' },
]);
const handleDragStart = (event) => {
if (event.active) {
console.log('Drag started:', event.active.data.item.name);
}
};
const handleDragEnd = (event) => {
const { active, over } = event;
if (!active) {
return;
}
console.log(`Drag ended. Item: ${active.data.item.name}, Over: ${over?.data.item.name || 'nothing'}`);
if (!over || active.id === over.id) {
return;
}
setItems((currentItems) => {
const oldIndex = currentItems.findIndex((item) => item.id === active.id);
const newIndex = currentItems.findIndex((item) => item.id === over.id);
const newItems = Array.from(currentItems);
const [removed] = newItems.splice(oldIndex, 1);
newItems.splice(newIndex, 0, removed);
return newItems;
});
};
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px', maxWidth: '600px', margin: '40px auto', borderRadius: '20px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' }}>
<DndProvider onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div style={{ marginTop: '20px' }}>
<div style={{ cursor: 'grab' }}>
{items.map((item, index) => (
<SortableItem key={item.id} item={item} index={index} />
))}
</div>
</div>
</DndProvider>
</div>
);
}