또 쓰고 싶은 dnd 라이브러리, ddo-dnd
A lightweight drag-and-drop (DnD) library for React. It offers coordinate-based dragging, grid snapping, and collision checking with a simple API, including multiple collision modes: rectangle (AABB), circle, and OBB (Oriented Bounding Box). The repository root contains the library, and playground/ contains a runnable demo.
# pnpm
pnpm add ddo-dnd react react-dom
# npm
npm i ddo-dnd react react-dom
# yarn
yarn add ddo-dnd react react-domRun the dev server (playground):
cd playground && pnpm i && pnpm devOpen in your browser: http://localhost:5173
2025-11-18.7.35.57.mov
import { DragContainer, DraggableItem, DraggingItem } from 'ddo-dnd';
import {
useDragBlock,
useBlocksTransition,
useSetPointerEvents,
} from 'ddo-dnd';
import type { BlockType, Position, Size } from 'ddo-dnd';-
DragContainer- props:
{ containerRef: RefObject<HTMLDivElement|null>, children, onDrop?, onDragOver? } - Role: Root container where draggable items are placed; used as the coordinate reference.
- props:
-
DraggableItem- props:
{ position: Position, isDragging: boolean, handleStartDrag: (e: React.PointerEvent<HTMLDivElement>) => void, children } - Role: Absolutely positioned item moved via
transformbased onposition. Propagates the drag-start event upward.
- props:
-
DraggingItem- props:
{ position: Position, children } - Role: Ghost element that follows the pointer during dragging (no pointer events).
- props:
-
useDragBlock({ containerRef, scrollOffset, workBlocks, updateWorkBlockTimeOnServer, updateWorkBlocks })- Returns:
{ draggingBlock, dragPointerPosition, dragOffset, handleStartDrag } - Behavior:
- Receives pointer moves via global listeners and updates
draggingBlock.positionsnapped withsnapPositionToGrid - On drop: bounds check (
isInBound) → resolve collisions (resolveCollision) → animate (useBlocksTransition) → final commit (updateWorkBlockTimeOnServer)
- Receives pointer moves via global listeners and updates
- Returns:
-
useBlocksTransition(updateWorkBlocks)- Returns:
{ animateBlocksTransition(prevBlocks, nextBlocks, durationMs=250) } - Role: Interpolates between previous and next block arrays to apply smooth movement animation, then commits
nextBlocks.
- Returns:
-
useSetPointerEvents({ onPointerMove?, onPointerUp? })- Role: Registers/unregisters pointer event listeners on
window.
- Role: Registers/unregisters pointer event listeners on
BlockType:{ id: number; position: Position; size: Size }Position:{ x: number; y: number }Size:{ width: number; height: number }
isInBound(position, block, scrollOffset, containerRect, defaultPosition, defaultPositionOffset?, directions?): Container bounds check
<DragContainer containerRef={containerRef}>
{blocks.map(block => (
<DraggableItem
key={block.id}
position={block.position}
isDragging={draggingBlock?.id === block.id}
handleStartDrag={e => {
handleStartDrag(e.nativeEvent as PointerEvent, block);
}}
>
{/* block content */}
</DraggableItem>
))}
{dragPointerPosition && draggingBlock && (
<DraggingItem
position={{
x: dragPointerPosition.x - dragOffset.x,
y: dragPointerPosition.y - dragOffset.y,
}}
>
{/* dragging content */}
</DraggingItem>
)}
</DragContainer>Root:
pnpm i
pnpm buildPlayground:
cd playground
pnpm i
pnpm devMIT