dnd-block-tree 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,52 +6,44 @@
6
6
 
7
7
  A headless React library for building hierarchical drag-and-drop interfaces. Bring your own components, we handle the complexity.
8
8
 
9
- ## Features
9
+ ## Packages
10
+
11
+ As of v2.0.0, the library is split into a monorepo with three packages:
12
+
13
+ | Package | Description |
14
+ |---------|-------------|
15
+ | [`@dnd-block-tree/core`](./packages/core) | Framework-agnostic core — types, collision detection, reducers, tree factory, utilities. Zero dependencies. |
16
+ | [`@dnd-block-tree/react`](./packages/react) | React adapter — components, hooks, @dnd-kit integration. |
17
+ | [`dnd-block-tree`](./packages/dnd-block-tree) | Compatibility wrapper — re-exports everything from `@dnd-block-tree/react`. |
18
+
19
+ **Existing users**: `import { ... } from 'dnd-block-tree'` continues to work with no code changes.
10
20
 
11
- - **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
12
- - **Ghost Preview** - In-flow semi-transparent preview shows where blocks will land with accurate layout
13
- - **Snapshotted Collision** - Zone rects are frozen on drag start and re-measured after each ghost commit, preventing layout-shift feedback loops
14
- - **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with cross-depth-aware hysteresis
15
- - **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
16
- - **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
17
- - **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
18
- - **Customizable Drag Rules** - `canDrag` and `canDrop` filters for fine-grained control over drag behavior
19
- - **Max Depth Constraint** - Limit nesting depth via `maxDepth` prop, enforced in both drag validation and programmatic APIs
20
- - **Keyboard Navigation** - Arrow key traversal, Enter/Space to expand/collapse, Home/End to jump. Opt-in via `keyboardNavigation` prop
21
- - **Multi-Select Drag** - Cmd/Ctrl+Click and Shift+Click selection with batch drag. Opt-in via `multiSelect` prop
22
- - **Undo/Redo** - Composable `useBlockHistory` hook with past/future stacks for state history management
23
- - **Lifecycle Callbacks** - `onBlockAdd`, `onBlockDelete`, `onBlockMove`, `onBeforeMove` middleware, and more
24
- - **Fractional Indexing** - Opt-in CRDT-compatible ordering via `orderingStrategy: 'fractional'`
25
- - **Serialization** - `flatToNested` / `nestedToFlat` converters for flat array ↔ nested tree transforms
26
- - **SSR Compatible** - `BlockTreeSSR` wrapper for hydration-safe rendering in Next.js and other SSR environments
27
- - **Animation Support** - CSS expand/collapse transitions via `AnimationConfig` and FLIP reorder animations via `useLayoutAnimation`
28
- - **Virtual Scrolling** - Windowed rendering for large trees (1000+ blocks) via `virtualize` prop
21
+ **New users**: you can import from `dnd-block-tree` for the full API, or use `@dnd-block-tree/core` directly for framework-agnostic tree operations (server-side manipulation, testing, non-React frameworks).
29
22
 
30
23
  ## Installation
31
24
 
32
25
  ```bash
33
- npm install dnd-block-tree
26
+ npm install dnd-block-tree @dnd-kit/core @dnd-kit/utilities
34
27
  ```
35
28
 
29
+ Requires **React 18+** and **@dnd-kit/core 6+**.
30
+
36
31
  ## Quick Start
37
32
 
38
33
  ```tsx
39
34
  import { BlockTree, type BaseBlock, type BlockRenderers } from 'dnd-block-tree'
40
35
 
41
- // Define your block type
42
36
  interface MyBlock extends BaseBlock {
43
37
  type: 'section' | 'task' | 'note'
44
38
  title: string
45
39
  }
46
40
 
47
- // Define which types can have children
48
41
  const CONTAINER_TYPES = ['section'] as const
49
42
 
50
- // Create renderers for each block type
51
43
  const renderers: BlockRenderers<MyBlock, typeof CONTAINER_TYPES> = {
52
- section: (props) => <SectionBlock {...props} />, // Gets ContainerRendererProps
53
- task: (props) => <TaskBlock {...props} />, // Gets BlockRendererProps
54
- note: (props) => <NoteBlock {...props} />, // Gets BlockRendererProps
44
+ section: (props) => <SectionBlock {...props} />,
45
+ task: (props) => <TaskBlock {...props} />,
46
+ note: (props) => <NoteBlock {...props} />,
55
47
  }
56
48
 
57
49
  function App() {
@@ -68,684 +60,40 @@ function App() {
68
60
  }
69
61
  ```
70
62
 
71
- ## API
72
-
73
- ### BlockTree Props
74
-
75
- | Prop | Type | Default | Description |
76
- |------|------|---------|-------------|
77
- | `blocks` | `T[]` | required | Array of blocks to render |
78
- | `renderers` | `BlockRenderers<T, C>` | required | Map of block types to render functions |
79
- | `containerTypes` | `readonly string[]` | `[]` | Block types that can have children |
80
- | `onChange` | `(blocks: T[]) => void` | - | Called when blocks are reordered |
81
- | `dragOverlay` | `(block: T) => ReactNode` | - | Custom drag overlay renderer |
82
- | `activationDistance` | `number` | `8` | Pixels to move before drag starts |
83
- | `previewDebounce` | `number` | `150` | Debounce delay for preview updates |
84
- | `showDropPreview` | `boolean` | `true` | Show live preview of block at drop position |
85
- | `canDrag` | `(block: T) => boolean` | - | Filter which blocks can be dragged |
86
- | `canDrop` | `(block, zone, target) => boolean` | - | Filter valid drop targets |
87
- | `collisionDetection` | `CollisionDetection` | sticky | Custom collision detection algorithm (from `@dnd-kit/core`) |
88
- | `sensors` | `SensorConfig` | - | Sensor configuration (see [Sensor Config](#sensor-config)) |
89
- | `animation` | `AnimationConfig` | - | Animation configuration (see [Animation](#animation--transitions)) |
90
- | `maxDepth` | `number` | - | Maximum nesting depth (1 = flat, 2 = one level, etc.) |
91
- | `keyboardNavigation` | `boolean` | `false` | Enable keyboard navigation with arrow keys |
92
- | `multiSelect` | `boolean` | `false` | Enable multi-select with Cmd/Ctrl+Click and Shift+Click |
93
- | `selectedIds` | `Set<string>` | - | Externally-controlled selected IDs (for multi-select) |
94
- | `onSelectionChange` | `(ids: Set<string>) => void` | - | Called when selection changes |
95
- | `orderingStrategy` | `'integer' \| 'fractional'` | `'integer'` | Sibling ordering strategy |
96
- | `initialExpanded` | `string[] \| 'all' \| 'none'` | `'all'` | Initially expanded container IDs |
97
- | `virtualize` | `{ itemHeight: number; overscan?: number }` | - | Enable virtual scrolling (see [Virtual Scrolling](#virtual-scrolling)) |
98
- | `className` | `string` | - | Root container class |
99
- | `dropZoneClassName` | `string` | - | Drop zone class |
100
- | `dropZoneActiveClassName` | `string` | - | Active drop zone class |
101
- | `indentClassName` | `string` | - | Nested children indent class |
102
-
103
- ### Callbacks
104
-
105
- | Callback | Type | Description |
106
- |----------|------|-------------|
107
- | `onDragStart` | `(event: DragStartEvent<T>) => boolean \| void` | Called when drag starts. Return `false` to prevent. |
108
- | `onDragMove` | `(event: DragMoveEvent<T>) => void` | Called during drag movement (debounced) |
109
- | `onDragEnd` | `(event: DragEndEvent<T>) => void` | Called when drag ends |
110
- | `onDragCancel` | `(event: DragEndEvent<T>) => void` | Called when drag is cancelled |
111
- | `onBeforeMove` | `(op: MoveOperation<T>) => MoveOperation<T> \| false \| void` | Middleware to transform or cancel moves |
112
- | `onBlockMove` | `(event: BlockMoveEvent<T>) => void` | Called after a block is moved |
113
- | `onBlockAdd` | `(event: BlockAddEvent<T>) => void` | Called after a block is added |
114
- | `onBlockDelete` | `(event: BlockDeleteEvent<T>) => void` | Called after a block is deleted |
115
- | `onExpandChange` | `(event: ExpandChangeEvent<T>) => void` | Called when expand/collapse changes |
116
- | `onHoverChange` | `(event: HoverChangeEvent<T>) => void` | Called when hover zone changes |
117
-
118
- ### Sensor Config
119
-
120
- ```typescript
121
- interface SensorConfig {
122
- /** Distance in pixels before drag starts (default: 8) */
123
- activationDistance?: number
124
- /** Delay in ms before drag starts (overrides distance) */
125
- activationDelay?: number
126
- /** Tolerance in px for delay-based activation (default: 5) */
127
- tolerance?: number
128
- /** Override the default long-press delay for touch sensors (default: 200ms) */
129
- longPressDelay?: number
130
- /** Trigger haptic feedback (vibration) on drag start for touch devices */
131
- hapticFeedback?: boolean
132
- }
133
- ```
134
-
135
- ```tsx
136
- <BlockTree
137
- sensors={{
138
- longPressDelay: 300, // 300ms long-press for touch
139
- hapticFeedback: true, // Vibrate on drag start
140
- }}
141
- ...
142
- />
143
- ```
144
-
145
- ### Animation Config
146
-
147
- ```typescript
148
- interface AnimationConfig {
149
- /** Duration for expand/collapse animations in ms */
150
- expandDuration?: number
151
- /** Duration for drag overlay animation in ms */
152
- dragOverlayDuration?: number
153
- /** Easing function (CSS timing function, default: 'ease') */
154
- easing?: string
155
- }
156
- ```
157
-
158
- ### Event Types
159
-
160
- All event types are exported and generic over your block type `T`.
161
-
162
- #### DragStartEvent
163
-
164
- ```typescript
165
- interface DragStartEvent<T> {
166
- block: T
167
- blockId: string
168
- }
169
- ```
170
-
171
- #### DragMoveEvent
172
-
173
- ```typescript
174
- interface DragMoveEvent<T> {
175
- block: T
176
- blockId: string
177
- overZone: string | null // Current hover zone ID
178
- coordinates: { x: number; y: number }
179
- }
180
- ```
181
-
182
- #### DragEndEvent
183
-
184
- ```typescript
185
- interface DragEndEvent<T> {
186
- block: T
187
- blockId: string
188
- targetZone: string | null // Zone where block was dropped
189
- cancelled: boolean
190
- }
191
- ```
192
-
193
- #### BlockMoveEvent
194
-
195
- ```typescript
196
- interface BlockMoveEvent<T> {
197
- block: T
198
- from: BlockPosition // { parentId, index }
199
- to: BlockPosition
200
- blocks: T[] // Full block array after the move
201
- movedIds: string[] // All moved block IDs (for multi-select)
202
- }
203
- ```
204
-
205
- #### MoveOperation
206
-
207
- Passed to the `onBeforeMove` middleware:
208
-
209
- ```typescript
210
- interface MoveOperation<T> {
211
- block: T
212
- from: BlockPosition
213
- targetZone: string // Drop zone ID (e.g. "after-uuid", "into-uuid")
214
- }
215
- ```
216
-
217
- #### BlockAddEvent
218
-
219
- ```typescript
220
- interface BlockAddEvent<T> {
221
- block: T
222
- parentId: string | null
223
- index: number
224
- }
225
- ```
226
-
227
- #### BlockDeleteEvent
228
-
229
- ```typescript
230
- interface BlockDeleteEvent<T> {
231
- block: T
232
- deletedIds: string[] // Block + all descendant IDs
233
- parentId: string | null
234
- }
235
- ```
236
-
237
- #### ExpandChangeEvent
238
-
239
- ```typescript
240
- interface ExpandChangeEvent<T> {
241
- block: T
242
- blockId: string
243
- expanded: boolean
244
- }
245
- ```
246
-
247
- #### HoverChangeEvent
248
-
249
- ```typescript
250
- interface HoverChangeEvent<T> {
251
- zoneId: string | null
252
- zoneType: 'before' | 'after' | 'into' | null
253
- targetBlock: T | null
254
- }
255
- ```
256
-
257
- ### Types
258
-
259
- #### BaseBlock
260
-
261
- All blocks must extend `BaseBlock`:
262
-
263
- ```typescript
264
- interface BaseBlock {
265
- id: string
266
- type: string
267
- parentId: string | null
268
- order: number | string // number for integer ordering, string for fractional
269
- }
270
- ```
271
-
272
- #### BlockRendererProps
273
-
274
- Props passed to non-container block renderers:
275
-
276
- ```typescript
277
- interface BlockRendererProps<T extends BaseBlock> {
278
- block: T
279
- children?: ReactNode
280
- isDragging?: boolean
281
- isOver?: boolean
282
- depth: number
283
- }
284
- ```
285
-
286
- #### ContainerRendererProps
287
-
288
- Props passed to container block renderers (extends BlockRendererProps):
289
-
290
- ```typescript
291
- interface ContainerRendererProps<T extends BaseBlock> extends BlockRendererProps<T> {
292
- children: ReactNode
293
- isExpanded: boolean
294
- onToggleExpand: () => void
295
- }
296
- ```
297
-
298
- #### NestedBlock
299
-
300
- Tree representation used by serialization helpers:
301
-
302
- ```typescript
303
- type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
304
- children: NestedBlock<T>[]
305
- }
306
- ```
307
-
308
- ## Serialization Helpers
309
-
310
- Convert between flat block arrays and nested tree structures:
311
-
312
- ```tsx
313
- import { flatToNested, nestedToFlat, type NestedBlock } from 'dnd-block-tree'
314
-
315
- // Flat array -> nested tree (e.g. for JSON export)
316
- const nested: NestedBlock<MyBlock>[] = flatToNested(blocks)
317
-
318
- // Nested tree -> flat array (e.g. for JSON import)
319
- const flat: MyBlock[] = nestedToFlat(nested)
320
- ```
321
-
322
- `flatToNested` groups by `parentId`, sorts siblings by `order`, and recursively builds `children` arrays. `parentId` and `order` are omitted from the output since they're structural.
323
-
324
- `nestedToFlat` performs a DFS walk, assigning `parentId` and integer `order` on the way down.
325
-
326
- ## SSR Compatibility
327
-
328
- For Next.js App Router or other SSR environments, use `BlockTreeSSR` to avoid hydration mismatches:
329
-
330
- ```tsx
331
- import { BlockTreeSSR } from 'dnd-block-tree'
332
-
333
- function Page() {
334
- return (
335
- <BlockTreeSSR
336
- blocks={blocks}
337
- renderers={renderers}
338
- containerTypes={CONTAINER_TYPES}
339
- onChange={setBlocks}
340
- fallback={<div>Loading tree...</div>}
341
- />
342
- )
343
- }
344
- ```
345
-
346
- `BlockTreeSSR` renders the `fallback` (default: `null`) on the server, then mounts the full `BlockTree` after hydration via `useEffect`. All `BlockTree` props are passed through.
347
-
348
- ```typescript
349
- interface BlockTreeSSRProps<T, C> extends BlockTreeProps<T, C> {
350
- fallback?: ReactNode
351
- }
352
- ```
353
-
354
- ## Animation & Transitions
355
-
356
- ### Expand/Collapse Transitions
357
-
358
- Pass an `AnimationConfig` to enable CSS transitions on container expand/collapse:
359
-
360
- ```tsx
361
- <BlockTree
362
- animation={{
363
- expandDuration: 200, // ms
364
- easing: 'ease-in-out', // CSS timing function
365
- }}
366
- ...
367
- />
368
- ```
369
-
370
- ### FLIP Reorder Animation
371
-
372
- The `useLayoutAnimation` hook provides FLIP-based (First-Last-Invert-Play) animations for block reorder transitions. It's a standalone composable — not built into BlockTree:
373
-
374
- ```tsx
375
- import { useLayoutAnimation } from 'dnd-block-tree'
376
-
377
- function MyTree() {
378
- const containerRef = useRef<HTMLDivElement>(null)
379
-
380
- useLayoutAnimation(containerRef, {
381
- duration: 200, // Transition duration in ms (default: 200)
382
- easing: 'ease', // CSS easing (default: 'ease')
383
- selector: '[data-block-id]', // Selector for animated children (default)
384
- })
385
-
386
- return (
387
- <div ref={containerRef}>
388
- <BlockTree ... />
389
- </div>
390
- )
391
- }
392
- ```
393
-
394
- ```typescript
395
- interface UseLayoutAnimationOptions {
396
- duration?: number // default: 200
397
- easing?: string // default: 'ease'
398
- selector?: string // default: '[data-block-id]'
399
- }
400
- ```
401
-
402
- ## Virtual Scrolling
403
-
404
- Enable windowed rendering for large trees (1000+ blocks) with the `virtualize` prop:
405
-
406
- ```tsx
407
- <BlockTree
408
- virtualize={{
409
- itemHeight: 40, // Fixed height of each item in pixels
410
- overscan: 5, // Extra items rendered outside viewport (default: 5)
411
- }}
412
- blocks={blocks}
413
- ...
414
- />
415
- ```
416
-
417
- When enabled, only visible blocks (plus overscan) are rendered. The tree is wrapped in a scrollable container with a spacer div maintaining correct total height.
418
-
419
- **Limitations:** Fixed item height only. Variable height items are not supported.
420
-
421
- ### useVirtualTree Hook
422
-
423
- For custom virtual scrolling implementations outside of BlockTree:
424
-
425
- ```tsx
426
- import { useVirtualTree } from 'dnd-block-tree'
427
-
428
- const containerRef = useRef<HTMLDivElement>(null)
429
- const { visibleRange, totalHeight, offsetY } = useVirtualTree({
430
- containerRef,
431
- itemCount: 1000,
432
- itemHeight: 40,
433
- overscan: 5,
434
- })
435
- ```
436
-
437
- ```typescript
438
- interface UseVirtualTreeOptions {
439
- containerRef: React.RefObject<HTMLElement | null>
440
- itemCount: number
441
- itemHeight: number
442
- overscan?: number // default: 5
443
- }
444
-
445
- interface UseVirtualTreeResult {
446
- visibleRange: { start: number; end: number }
447
- totalHeight: number
448
- offsetY: number
449
- }
450
- ```
451
-
452
- ## Touch & Mobile
453
-
454
- Touch support is built-in with sensible defaults (200ms long-press, 5px tolerance). For fine-tuning:
455
-
456
- ```tsx
457
- <BlockTree
458
- sensors={{
459
- longPressDelay: 300, // Override default 200ms touch activation
460
- hapticFeedback: true, // Vibrate on drag start (uses navigator.vibrate)
461
- }}
462
- ...
463
- />
464
- ```
465
-
466
- The `triggerHaptic` utility is also exported for use in custom components:
467
-
468
- ```tsx
469
- import { triggerHaptic } from 'dnd-block-tree'
470
-
471
- triggerHaptic(10) // Vibrate for 10ms (default)
472
- ```
473
-
474
- ## Undo/Redo
475
-
476
- The `useBlockHistory` hook provides undo/redo support as a composable layer on top of `BlockTree`:
477
-
478
- ```tsx
479
- import { BlockTree, useBlockHistory } from 'dnd-block-tree'
480
-
481
- function App() {
482
- const { blocks, set, undo, redo, canUndo, canRedo } = useBlockHistory<MyBlock>(initialBlocks, {
483
- maxSteps: 50, // optional, default 50
484
- })
485
-
486
- return (
487
- <>
488
- <div>
489
- <button onClick={undo} disabled={!canUndo}>Undo</button>
490
- <button onClick={redo} disabled={!canRedo}>Redo</button>
491
- </div>
492
- <BlockTree blocks={blocks} onChange={set} ... />
493
- </>
494
- )
495
- }
496
- ```
497
-
498
- ## Keyboard Navigation
499
-
500
- Enable accessible tree navigation with the `keyboardNavigation` prop:
501
-
502
- ```tsx
503
- <BlockTree keyboardNavigation blocks={blocks} ... />
504
- ```
505
-
506
- | Key | Action |
507
- |-----|--------|
508
- | Arrow Down | Focus next visible block |
509
- | Arrow Up | Focus previous visible block |
510
- | Arrow Right | Expand container, or focus first child |
511
- | Arrow Left | Collapse container, or focus parent |
512
- | Enter / Space | Toggle expand/collapse |
513
- | Home | Focus first block |
514
- | End | Focus last block |
515
-
516
- Blocks receive `data-block-id` and `tabIndex` attributes for focus management, and the tree root gets `role="tree"`. Each block element includes WAI-ARIA TreeView attributes: `aria-level`, `aria-posinset`, `aria-setsize`, `aria-expanded` (containers only), and `aria-selected`.
517
-
518
- ## Multi-Select Drag
519
-
520
- Enable batch selection and drag with the `multiSelect` prop:
521
-
522
- ```tsx
523
- <BlockTree multiSelect blocks={blocks} ... />
524
- ```
525
-
526
- - **Cmd/Ctrl+Click** toggles a single block in the selection
527
- - **Shift+Click** range-selects between the last clicked and current block
528
- - **Plain click** clears selection and selects only the clicked block
529
- - Dragging a selected block moves all selected blocks, preserving relative order
530
- - The drag overlay shows a stacked card effect with a count badge
531
-
532
- You can also control selection externally via `selectedIds` and `onSelectionChange`.
533
-
534
- ## Max Depth
535
-
536
- Limit nesting depth to prevent deeply nested trees:
537
-
538
- ```tsx
539
- <BlockTree maxDepth={2} blocks={blocks} ... />
540
- ```
541
-
542
- - `maxDepth={1}` - flat list, no nesting allowed
543
- - `maxDepth={2}` - blocks can nest one level inside containers
544
- - When a move would exceed the limit, the drop zone is rejected and the move is a no-op
545
-
546
- ## Move Middleware (`onBeforeMove`)
547
-
548
- The `onBeforeMove` callback intercepts moves before they are committed. You can use it to validate, transform, or cancel moves:
549
-
550
- ```tsx
551
- <BlockTree
552
- blocks={blocks}
553
- onChange={setBlocks}
554
- onBeforeMove={(operation) => {
555
- // Cancel: return false to prevent the move
556
- if (operation.block.type === 'locked') {
557
- return false
558
- }
559
-
560
- // Transform: change the target zone
561
- if (operation.targetZone.startsWith('into-') && someCondition) {
562
- return { ...operation, targetZone: `after-${extractBlockId(operation.targetZone)}` }
563
- }
564
-
565
- // Allow: return void/undefined to proceed as-is
566
- }}
567
- />
568
- ```
569
-
570
- The middleware receives a `MoveOperation` with the block, its original position (`from`), and the target drop zone. Returning:
571
- - `false` cancels the move entirely
572
- - A modified `MoveOperation` transforms the move (e.g. redirect to a different zone)
573
- - `void` / `undefined` allows the move as-is
574
-
575
- ## Fractional Indexing
576
-
577
- By default, siblings are reindexed `0, 1, 2, ...` on every move. For collaborative or CRDT-compatible scenarios, use fractional indexing:
578
-
579
- ```tsx
580
- import { BlockTree, initFractionalOrder } from 'dnd-block-tree'
581
-
582
- // Convert existing blocks to fractional ordering
583
- const [blocks, setBlocks] = useState(() => initFractionalOrder(initialBlocks))
584
-
585
- <BlockTree
586
- blocks={blocks}
587
- onChange={setBlocks}
588
- orderingStrategy="fractional"
589
- />
590
- ```
591
-
592
- With fractional ordering, only the moved block receives a new `order` value (a lexicographically sortable string key). Siblings are never reindexed, making it safe for concurrent edits.
593
-
594
- Related utilities:
595
-
596
- ```typescript
597
- import {
598
- generateKeyBetween, // Generate a key between two existing keys
599
- generateNKeysBetween, // Generate N keys between two existing keys
600
- generateInitialKeys, // Generate N evenly-spaced initial keys
601
- initFractionalOrder, // Convert integer-ordered blocks to fractional
602
- compareFractionalKeys, // Compare two fractional keys
603
- } from 'dnd-block-tree'
604
- ```
605
-
606
- ## Collision Detection
607
-
608
- The library ships with three collision detection strategies:
609
-
610
- ```typescript
611
- import {
612
- weightedVerticalCollision, // Default: edge-distance based, depth-aware
613
- closestCenterCollision, // Simple closest-center algorithm
614
- createStickyCollision, // Wraps any strategy with hysteresis to prevent flickering
615
- } from 'dnd-block-tree'
616
-
617
- // Use a custom collision strategy
618
- <BlockTree collisionDetection={closestCenterCollision} ... />
619
-
620
- // Or use the sticky wrapper with a custom threshold (px)
621
- const collision = createStickyCollision(20)
622
- <BlockTree collisionDetection={collision} ... />
623
- ```
624
-
625
- You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
626
-
627
- ### Snapshotted Zone Rects
628
-
629
- `createStickyCollision` accepts an optional `SnapshotRectsRef` — a ref to a `Map<string, DOMRect>` of frozen zone positions. When provided, collision detection uses these snapshots instead of live DOM measurements, preventing feedback loops caused by the in-flow ghost preview shifting zone positions.
630
-
631
- ```typescript
632
- import { createStickyCollision, type SnapshotRectsRef } from 'dnd-block-tree'
633
-
634
- const snapshotRef: SnapshotRectsRef = { current: null }
635
- const collision = createStickyCollision(20, snapshotRef)
636
-
637
- // Snapshot all zone rects after drag starts:
638
- snapshotRef.current = new Map(
639
- [...document.querySelectorAll('[data-zone-id]')].map(el => [
640
- el.getAttribute('data-zone-id')!,
641
- el.getBoundingClientRect(),
642
- ])
643
- )
644
- ```
645
-
646
- `BlockTree` handles this lifecycle automatically — zones are snapshotted on drag start and re-measured via `requestAnimationFrame` after each ghost position commit.
647
-
648
- ### Cross-Depth Hysteresis
649
-
650
- The sticky collision uses a reduced threshold (25% of normal) when switching between zones at different indentation levels. This makes it easy to drag blocks in and out of containers while still preventing flickering between same-depth adjacent zones.
651
-
652
- ## Type Safety
653
-
654
- The library provides automatic type inference for container vs non-container renderers:
655
-
656
- ```tsx
657
- const CONTAINER_TYPES = ['section'] as const // Must use `as const`
658
-
659
- const renderers: BlockRenderers<MyBlock, typeof CONTAINER_TYPES> = {
660
- // TypeScript knows 'section' renderer gets ContainerRendererProps
661
- section: (props) => {
662
- const { isExpanded, onToggleExpand } = props // Available!
663
- return <div>...</div>
664
- },
665
- // TypeScript knows 'task' renderer gets BlockRendererProps
666
- task: (props) => {
667
- // props.isExpanded would be a type error here
668
- return <div>...</div>
669
- },
670
- }
671
- ```
672
-
673
- ## Utilities
674
-
675
- The library exports utility functions for tree manipulation, ID generation, and zone parsing:
676
-
677
- ```typescript
678
- import {
679
- // Tree operations
680
- computeNormalizedIndex, // Convert flat array to { byId, byParent } index
681
- buildOrderedBlocks, // Convert index back to ordered flat array
682
- reparentBlockIndex, // Move a single block to a new position
683
- reparentMultipleBlocks, // Move multiple blocks preserving relative order
684
- getDescendantIds, // Get all descendant IDs of a block (Set)
685
- deleteBlockAndDescendants, // Remove a block and all its descendants from index
686
- getBlockDepth, // Compute depth of a block (root = 1)
687
- getSubtreeDepth, // Max depth of a subtree (leaf = 1)
688
- validateBlockTree, // Validate tree for cycles, orphans, stale refs
689
-
690
- // Serialization
691
- flatToNested, // Convert flat block array to nested tree
692
- nestedToFlat, // Convert nested tree to flat block array
693
-
694
- // ID / zone helpers
695
- generateId, // Generate unique block IDs
696
- extractUUID, // Extract block ID from zone ID string
697
- getDropZoneType, // Parse zone type: 'before' | 'after' | 'into'
698
- extractBlockId, // Extract block ID from zone ID (alias)
699
-
700
- // Touch
701
- triggerHaptic, // Trigger haptic feedback (navigator.vibrate)
702
-
703
- // Fractional indexing
704
- generateKeyBetween, // Generate a fractional key between two keys
705
- generateNKeysBetween, // Generate N keys between two existing keys
706
- generateInitialKeys, // Generate N evenly-spaced initial keys
707
- initFractionalOrder, // Convert integer-ordered blocks to fractional
708
- compareFractionalKeys, // Compare two fractional keys
709
-
710
- // Collision detection
711
- weightedVerticalCollision, // Edge-distance collision, depth-aware
712
- closestCenterCollision, // Simple closest-center collision
713
- createStickyCollision, // Hysteresis wrapper with snapshot support
714
- type SnapshotRectsRef, // Ref type for frozen zone rects
715
-
716
- // Internal helpers
717
- cloneMap, // Clone a Map
718
- cloneParentMap, // Deep clone a parent->children Map
719
- debounce, // Debounce with cancel()
720
-
721
- // Hooks
722
- createBlockState, // Factory for block state context + provider
723
- createTreeState, // Factory for tree UI state context + provider
724
- useBlockHistory, // Undo/redo state management
725
- useLayoutAnimation, // FLIP-based reorder animations
726
- useVirtualTree, // Virtual scrolling primitives
727
- useConfiguredSensors, // Configure dnd-kit sensors
728
- getSensorConfig, // Get sensor config from SensorConfig
63
+ ## Features
729
64
 
730
- // Components
731
- BlockTree, // Main drag-and-drop tree component
732
- BlockTreeSSR, // SSR-safe wrapper
733
- TreeRenderer, // Recursive tree renderer
734
- DropZone, // Individual drop zone
735
- DragOverlay, // Drag overlay wrapper
736
- } from 'dnd-block-tree'
737
- ```
65
+ - **Stable Drop Zones** — zones render from original positions, not preview state
66
+ - **Ghost Preview** — in-flow semi-transparent preview with accurate layout
67
+ - **Snapshotted Collision** — frozen zone rects prevent layout-shift feedback loops
68
+ - **Depth-Aware Collision** — prefers nested zones at indented cursor levels
69
+ - **Mobile & Touch** — long-press activation, haptic feedback, touch-optimized sensors ([docs](https://blocktree.sandybridge.io/docs/touch-mobile))
70
+ - **Keyboard Navigation** — arrow keys, Home/End, Enter/Space with ARIA roles ([docs](https://blocktree.sandybridge.io/docs/keyboard-navigation))
71
+ - **Multi-Select Drag** — Cmd/Ctrl+Click and Shift+Click with batch drag ([docs](https://blocktree.sandybridge.io/docs/multi-select))
72
+ - **Undo/Redo** — composable `useBlockHistory` hook ([docs](https://blocktree.sandybridge.io/docs/undo-redo))
73
+ - **Max Depth** — limit nesting via `maxDepth` prop ([docs](https://blocktree.sandybridge.io/docs/constraints))
74
+ - **Move Middleware** — `onBeforeMove` to validate, transform, or cancel moves ([docs](https://blocktree.sandybridge.io/docs/constraints))
75
+ - **Fractional Indexing** — CRDT-compatible ordering with `orderingStrategy: 'fractional'` ([docs](https://blocktree.sandybridge.io/docs/fractional-indexing))
76
+ - **Serialization** — `flatToNested` / `nestedToFlat` converters ([docs](https://blocktree.sandybridge.io/docs/serialization))
77
+ - **SSR Compatible** — `BlockTreeSSR` for hydration-safe rendering ([docs](https://blocktree.sandybridge.io/docs/ssr))
78
+ - **Animation** — CSS expand/collapse transitions + FLIP reorder animations ([docs](https://blocktree.sandybridge.io/docs/animation))
79
+ - **Virtual Scrolling** — windowed rendering for 1000+ blocks ([docs](https://blocktree.sandybridge.io/docs/virtual-scrolling))
80
+ - **Custom Collision Detection** — pluggable algorithms with sticky hysteresis ([docs](https://blocktree.sandybridge.io/docs/collision-detection))
81
+
82
+ ## Documentation
83
+
84
+ Full API reference, guides, and examples at **[blocktree.sandybridge.io](https://blocktree.sandybridge.io/docs)**.
738
85
 
739
86
  ## Demo
740
87
 
741
- Check out the [live demo](https://blocktree.sandybridge.io) to see the library in action with two example use cases:
88
+ Check out the [live demo](https://blocktree.sandybridge.io) with two example use cases:
742
89
 
743
- - **Productivity** - Sections, tasks, and notes with undo/redo, max depth control, and keyboard navigation
744
- - **File System** - Folders and files
90
+ - **Productivity** sections, tasks, and notes with undo/redo, max depth, keyboard nav
91
+ - **File System** folders and files
745
92
 
746
93
  ## Built With
747
94
 
748
- - [dnd-kit](https://dndkit.com/) - Modern drag and drop toolkit for React
95
+ - [dnd-kit](https://dndkit.com/) drag and drop toolkit for React
96
+ - [Turborepo](https://turbo.build/) — monorepo build system
749
97
  - React 18+ / React 19
750
98
  - TypeScript
751
99