dnd-block-tree 0.3.0 → 0.5.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
@@ -15,11 +15,20 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
15
15
  - **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
16
16
  - **Ghost Preview** - Semi-transparent preview shows where blocks will land without affecting zone positions
17
17
  - **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with hysteresis to prevent flickering
18
- - **Mobile & Touch Support** - Separate touch/pointer activation constraints prevent interference with scrolling on mobile devices
19
- - **8px Activation Distance** - Prevents accidental drags. Pointer must move 8px before drag starts, allowing normal clicks
18
+ - **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
20
19
  - **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
21
20
  - **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
22
21
  - **Customizable Drag Rules** - `canDrag` and `canDrop` filters for fine-grained control over drag behavior
22
+ - **Max Depth Constraint** - Limit nesting depth via `maxDepth` prop, enforced in both drag validation and programmatic APIs
23
+ - **Keyboard Navigation** - Arrow key traversal, Enter/Space to expand/collapse, Home/End to jump. Opt-in via `keyboardNavigation` prop
24
+ - **Multi-Select Drag** - Cmd/Ctrl+Click and Shift+Click selection with batch drag. Opt-in via `multiSelect` prop
25
+ - **Undo/Redo** - Composable `useBlockHistory` hook with past/future stacks for state history management
26
+ - **Lifecycle Callbacks** - `onBlockAdd`, `onBlockDelete`, `onBlockMove`, `onBeforeMove` middleware, and more
27
+ - **Fractional Indexing** - Opt-in CRDT-compatible ordering via `orderingStrategy: 'fractional'`
28
+ - **Serialization** - `flatToNested` / `nestedToFlat` converters for flat array ↔ nested tree transforms
29
+ - **SSR Compatible** - `BlockTreeSSR` wrapper for hydration-safe rendering in Next.js and other SSR environments
30
+ - **Animation Support** - CSS expand/collapse transitions via `AnimationConfig` and FLIP reorder animations via `useLayoutAnimation`
31
+ - **Virtual Scrolling** - Windowed rendering for large trees (1000+ blocks) via `virtualize` prop
23
32
 
24
33
  ## Installation
25
34
 
@@ -78,11 +87,176 @@ function App() {
78
87
  | `showDropPreview` | `boolean` | `true` | Show live preview of block at drop position |
79
88
  | `canDrag` | `(block: T) => boolean` | - | Filter which blocks can be dragged |
80
89
  | `canDrop` | `(block, zone, target) => boolean` | - | Filter valid drop targets |
90
+ | `collisionDetection` | `CollisionDetection` | sticky | Custom collision detection algorithm (from `@dnd-kit/core`) |
91
+ | `sensors` | `SensorConfig` | - | Sensor configuration (see [Sensor Config](#sensor-config)) |
92
+ | `animation` | `AnimationConfig` | - | Animation configuration (see [Animation](#animation--transitions)) |
93
+ | `maxDepth` | `number` | - | Maximum nesting depth (1 = flat, 2 = one level, etc.) |
94
+ | `keyboardNavigation` | `boolean` | `false` | Enable keyboard navigation with arrow keys |
95
+ | `multiSelect` | `boolean` | `false` | Enable multi-select with Cmd/Ctrl+Click and Shift+Click |
96
+ | `selectedIds` | `Set<string>` | - | Externally-controlled selected IDs (for multi-select) |
97
+ | `onSelectionChange` | `(ids: Set<string>) => void` | - | Called when selection changes |
98
+ | `orderingStrategy` | `'integer' \| 'fractional'` | `'integer'` | Sibling ordering strategy |
99
+ | `initialExpanded` | `string[] \| 'all' \| 'none'` | `'all'` | Initially expanded container IDs |
100
+ | `virtualize` | `{ itemHeight: number; overscan?: number }` | - | Enable virtual scrolling (see [Virtual Scrolling](#virtual-scrolling)) |
81
101
  | `className` | `string` | - | Root container class |
82
102
  | `dropZoneClassName` | `string` | - | Drop zone class |
83
103
  | `dropZoneActiveClassName` | `string` | - | Active drop zone class |
84
104
  | `indentClassName` | `string` | - | Nested children indent class |
85
105
 
106
+ ### Callbacks
107
+
108
+ | Callback | Type | Description |
109
+ |----------|------|-------------|
110
+ | `onDragStart` | `(event: DragStartEvent<T>) => boolean \| void` | Called when drag starts. Return `false` to prevent. |
111
+ | `onDragMove` | `(event: DragMoveEvent<T>) => void` | Called during drag movement (debounced) |
112
+ | `onDragEnd` | `(event: DragEndEvent<T>) => void` | Called when drag ends |
113
+ | `onDragCancel` | `(event: DragEndEvent<T>) => void` | Called when drag is cancelled |
114
+ | `onBeforeMove` | `(op: MoveOperation<T>) => MoveOperation<T> \| false \| void` | Middleware to transform or cancel moves |
115
+ | `onBlockMove` | `(event: BlockMoveEvent<T>) => void` | Called after a block is moved |
116
+ | `onBlockAdd` | `(event: BlockAddEvent<T>) => void` | Called after a block is added |
117
+ | `onBlockDelete` | `(event: BlockDeleteEvent<T>) => void` | Called after a block is deleted |
118
+ | `onExpandChange` | `(event: ExpandChangeEvent<T>) => void` | Called when expand/collapse changes |
119
+ | `onHoverChange` | `(event: HoverChangeEvent<T>) => void` | Called when hover zone changes |
120
+
121
+ ### Sensor Config
122
+
123
+ ```typescript
124
+ interface SensorConfig {
125
+ /** Distance in pixels before drag starts (default: 8) */
126
+ activationDistance?: number
127
+ /** Delay in ms before drag starts (overrides distance) */
128
+ activationDelay?: number
129
+ /** Tolerance in px for delay-based activation (default: 5) */
130
+ tolerance?: number
131
+ /** Override the default long-press delay for touch sensors (default: 200ms) */
132
+ longPressDelay?: number
133
+ /** Trigger haptic feedback (vibration) on drag start for touch devices */
134
+ hapticFeedback?: boolean
135
+ }
136
+ ```
137
+
138
+ ```tsx
139
+ <BlockTree
140
+ sensors={{
141
+ longPressDelay: 300, // 300ms long-press for touch
142
+ hapticFeedback: true, // Vibrate on drag start
143
+ }}
144
+ ...
145
+ />
146
+ ```
147
+
148
+ ### Animation Config
149
+
150
+ ```typescript
151
+ interface AnimationConfig {
152
+ /** Duration for expand/collapse animations in ms */
153
+ expandDuration?: number
154
+ /** Duration for drag overlay animation in ms */
155
+ dragOverlayDuration?: number
156
+ /** Easing function (CSS timing function, default: 'ease') */
157
+ easing?: string
158
+ }
159
+ ```
160
+
161
+ ### Event Types
162
+
163
+ All event types are exported and generic over your block type `T`.
164
+
165
+ #### DragStartEvent
166
+
167
+ ```typescript
168
+ interface DragStartEvent<T> {
169
+ block: T
170
+ blockId: string
171
+ }
172
+ ```
173
+
174
+ #### DragMoveEvent
175
+
176
+ ```typescript
177
+ interface DragMoveEvent<T> {
178
+ block: T
179
+ blockId: string
180
+ overZone: string | null // Current hover zone ID
181
+ coordinates: { x: number; y: number }
182
+ }
183
+ ```
184
+
185
+ #### DragEndEvent
186
+
187
+ ```typescript
188
+ interface DragEndEvent<T> {
189
+ block: T
190
+ blockId: string
191
+ targetZone: string | null // Zone where block was dropped
192
+ cancelled: boolean
193
+ }
194
+ ```
195
+
196
+ #### BlockMoveEvent
197
+
198
+ ```typescript
199
+ interface BlockMoveEvent<T> {
200
+ block: T
201
+ from: BlockPosition // { parentId, index }
202
+ to: BlockPosition
203
+ blocks: T[] // Full block array after the move
204
+ movedIds: string[] // All moved block IDs (for multi-select)
205
+ }
206
+ ```
207
+
208
+ #### MoveOperation
209
+
210
+ Passed to the `onBeforeMove` middleware:
211
+
212
+ ```typescript
213
+ interface MoveOperation<T> {
214
+ block: T
215
+ from: BlockPosition
216
+ targetZone: string // Drop zone ID (e.g. "after-uuid", "into-uuid")
217
+ }
218
+ ```
219
+
220
+ #### BlockAddEvent
221
+
222
+ ```typescript
223
+ interface BlockAddEvent<T> {
224
+ block: T
225
+ parentId: string | null
226
+ index: number
227
+ }
228
+ ```
229
+
230
+ #### BlockDeleteEvent
231
+
232
+ ```typescript
233
+ interface BlockDeleteEvent<T> {
234
+ block: T
235
+ deletedIds: string[] // Block + all descendant IDs
236
+ parentId: string | null
237
+ }
238
+ ```
239
+
240
+ #### ExpandChangeEvent
241
+
242
+ ```typescript
243
+ interface ExpandChangeEvent<T> {
244
+ block: T
245
+ blockId: string
246
+ expanded: boolean
247
+ }
248
+ ```
249
+
250
+ #### HoverChangeEvent
251
+
252
+ ```typescript
253
+ interface HoverChangeEvent<T> {
254
+ zoneId: string | null
255
+ zoneType: 'before' | 'after' | 'into' | null
256
+ targetBlock: T | null
257
+ }
258
+ ```
259
+
86
260
  ### Types
87
261
 
88
262
  #### BaseBlock
@@ -94,7 +268,7 @@ interface BaseBlock {
94
268
  id: string
95
269
  type: string
96
270
  parentId: string | null
97
- order: number
271
+ order: number | string // number for integer ordering, string for fractional
98
272
  }
99
273
  ```
100
274
 
@@ -124,6 +298,335 @@ interface ContainerRendererProps<T extends BaseBlock> extends BlockRendererProps
124
298
  }
125
299
  ```
126
300
 
301
+ #### NestedBlock
302
+
303
+ Tree representation used by serialization helpers:
304
+
305
+ ```typescript
306
+ type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
307
+ children: NestedBlock<T>[]
308
+ }
309
+ ```
310
+
311
+ ## Serialization Helpers
312
+
313
+ Convert between flat block arrays and nested tree structures:
314
+
315
+ ```tsx
316
+ import { flatToNested, nestedToFlat, type NestedBlock } from 'dnd-block-tree'
317
+
318
+ // Flat array -> nested tree (e.g. for JSON export)
319
+ const nested: NestedBlock<MyBlock>[] = flatToNested(blocks)
320
+
321
+ // Nested tree -> flat array (e.g. for JSON import)
322
+ const flat: MyBlock[] = nestedToFlat(nested)
323
+ ```
324
+
325
+ `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.
326
+
327
+ `nestedToFlat` performs a DFS walk, assigning `parentId` and integer `order` on the way down.
328
+
329
+ ## SSR Compatibility
330
+
331
+ For Next.js App Router or other SSR environments, use `BlockTreeSSR` to avoid hydration mismatches:
332
+
333
+ ```tsx
334
+ import { BlockTreeSSR } from 'dnd-block-tree'
335
+
336
+ function Page() {
337
+ return (
338
+ <BlockTreeSSR
339
+ blocks={blocks}
340
+ renderers={renderers}
341
+ containerTypes={CONTAINER_TYPES}
342
+ onChange={setBlocks}
343
+ fallback={<div>Loading tree...</div>}
344
+ />
345
+ )
346
+ }
347
+ ```
348
+
349
+ `BlockTreeSSR` renders the `fallback` (default: `null`) on the server, then mounts the full `BlockTree` after hydration via `useEffect`. All `BlockTree` props are passed through.
350
+
351
+ ```typescript
352
+ interface BlockTreeSSRProps<T, C> extends BlockTreeProps<T, C> {
353
+ fallback?: ReactNode
354
+ }
355
+ ```
356
+
357
+ ## Animation & Transitions
358
+
359
+ ### Expand/Collapse Transitions
360
+
361
+ Pass an `AnimationConfig` to enable CSS transitions on container expand/collapse:
362
+
363
+ ```tsx
364
+ <BlockTree
365
+ animation={{
366
+ expandDuration: 200, // ms
367
+ easing: 'ease-in-out', // CSS timing function
368
+ }}
369
+ ...
370
+ />
371
+ ```
372
+
373
+ ### FLIP Reorder Animation
374
+
375
+ The `useLayoutAnimation` hook provides FLIP-based (First-Last-Invert-Play) animations for block reorder transitions. It's a standalone composable — not built into BlockTree:
376
+
377
+ ```tsx
378
+ import { useLayoutAnimation } from 'dnd-block-tree'
379
+
380
+ function MyTree() {
381
+ const containerRef = useRef<HTMLDivElement>(null)
382
+
383
+ useLayoutAnimation(containerRef, {
384
+ duration: 200, // Transition duration in ms (default: 200)
385
+ easing: 'ease', // CSS easing (default: 'ease')
386
+ selector: '[data-block-id]', // Selector for animated children (default)
387
+ })
388
+
389
+ return (
390
+ <div ref={containerRef}>
391
+ <BlockTree ... />
392
+ </div>
393
+ )
394
+ }
395
+ ```
396
+
397
+ ```typescript
398
+ interface UseLayoutAnimationOptions {
399
+ duration?: number // default: 200
400
+ easing?: string // default: 'ease'
401
+ selector?: string // default: '[data-block-id]'
402
+ }
403
+ ```
404
+
405
+ ## Virtual Scrolling
406
+
407
+ Enable windowed rendering for large trees (1000+ blocks) with the `virtualize` prop:
408
+
409
+ ```tsx
410
+ <BlockTree
411
+ virtualize={{
412
+ itemHeight: 40, // Fixed height of each item in pixels
413
+ overscan: 5, // Extra items rendered outside viewport (default: 5)
414
+ }}
415
+ blocks={blocks}
416
+ ...
417
+ />
418
+ ```
419
+
420
+ 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.
421
+
422
+ **Limitations:** Fixed item height only. Variable height items are not supported.
423
+
424
+ ### useVirtualTree Hook
425
+
426
+ For custom virtual scrolling implementations outside of BlockTree:
427
+
428
+ ```tsx
429
+ import { useVirtualTree } from 'dnd-block-tree'
430
+
431
+ const containerRef = useRef<HTMLDivElement>(null)
432
+ const { visibleRange, totalHeight, offsetY } = useVirtualTree({
433
+ containerRef,
434
+ itemCount: 1000,
435
+ itemHeight: 40,
436
+ overscan: 5,
437
+ })
438
+ ```
439
+
440
+ ```typescript
441
+ interface UseVirtualTreeOptions {
442
+ containerRef: React.RefObject<HTMLElement | null>
443
+ itemCount: number
444
+ itemHeight: number
445
+ overscan?: number // default: 5
446
+ }
447
+
448
+ interface UseVirtualTreeResult {
449
+ visibleRange: { start: number; end: number }
450
+ totalHeight: number
451
+ offsetY: number
452
+ }
453
+ ```
454
+
455
+ ## Touch & Mobile
456
+
457
+ Touch support is built-in with sensible defaults (200ms long-press, 5px tolerance). For fine-tuning:
458
+
459
+ ```tsx
460
+ <BlockTree
461
+ sensors={{
462
+ longPressDelay: 300, // Override default 200ms touch activation
463
+ hapticFeedback: true, // Vibrate on drag start (uses navigator.vibrate)
464
+ }}
465
+ ...
466
+ />
467
+ ```
468
+
469
+ The `triggerHaptic` utility is also exported for use in custom components:
470
+
471
+ ```tsx
472
+ import { triggerHaptic } from 'dnd-block-tree'
473
+
474
+ triggerHaptic(10) // Vibrate for 10ms (default)
475
+ ```
476
+
477
+ ## Undo/Redo
478
+
479
+ The `useBlockHistory` hook provides undo/redo support as a composable layer on top of `BlockTree`:
480
+
481
+ ```tsx
482
+ import { BlockTree, useBlockHistory } from 'dnd-block-tree'
483
+
484
+ function App() {
485
+ const { blocks, set, undo, redo, canUndo, canRedo } = useBlockHistory<MyBlock>(initialBlocks, {
486
+ maxSteps: 50, // optional, default 50
487
+ })
488
+
489
+ return (
490
+ <>
491
+ <div>
492
+ <button onClick={undo} disabled={!canUndo}>Undo</button>
493
+ <button onClick={redo} disabled={!canRedo}>Redo</button>
494
+ </div>
495
+ <BlockTree blocks={blocks} onChange={set} ... />
496
+ </>
497
+ )
498
+ }
499
+ ```
500
+
501
+ ## Keyboard Navigation
502
+
503
+ Enable accessible tree navigation with the `keyboardNavigation` prop:
504
+
505
+ ```tsx
506
+ <BlockTree keyboardNavigation blocks={blocks} ... />
507
+ ```
508
+
509
+ | Key | Action |
510
+ |-----|--------|
511
+ | Arrow Down | Focus next visible block |
512
+ | Arrow Up | Focus previous visible block |
513
+ | Arrow Right | Expand container, or focus first child |
514
+ | Arrow Left | Collapse container, or focus parent |
515
+ | Enter / Space | Toggle expand/collapse |
516
+ | Home | Focus first block |
517
+ | End | Focus last block |
518
+
519
+ Blocks receive `data-block-id` and `tabIndex` attributes for focus management, and the tree root gets `role="tree"`.
520
+
521
+ ## Multi-Select Drag
522
+
523
+ Enable batch selection and drag with the `multiSelect` prop:
524
+
525
+ ```tsx
526
+ <BlockTree multiSelect blocks={blocks} ... />
527
+ ```
528
+
529
+ - **Cmd/Ctrl+Click** toggles a single block in the selection
530
+ - **Shift+Click** range-selects between the last clicked and current block
531
+ - **Plain click** clears selection and selects only the clicked block
532
+ - Dragging a selected block moves all selected blocks, preserving relative order
533
+ - The drag overlay shows a stacked card effect with a count badge
534
+
535
+ You can also control selection externally via `selectedIds` and `onSelectionChange`.
536
+
537
+ ## Max Depth
538
+
539
+ Limit nesting depth to prevent deeply nested trees:
540
+
541
+ ```tsx
542
+ <BlockTree maxDepth={2} blocks={blocks} ... />
543
+ ```
544
+
545
+ - `maxDepth={1}` - flat list, no nesting allowed
546
+ - `maxDepth={2}` - blocks can nest one level inside containers
547
+ - When a move would exceed the limit, the drop zone is rejected and the move is a no-op
548
+
549
+ ## Move Middleware (`onBeforeMove`)
550
+
551
+ The `onBeforeMove` callback intercepts moves before they are committed. You can use it to validate, transform, or cancel moves:
552
+
553
+ ```tsx
554
+ <BlockTree
555
+ blocks={blocks}
556
+ onChange={setBlocks}
557
+ onBeforeMove={(operation) => {
558
+ // Cancel: return false to prevent the move
559
+ if (operation.block.type === 'locked') {
560
+ return false
561
+ }
562
+
563
+ // Transform: change the target zone
564
+ if (operation.targetZone.startsWith('into-') && someCondition) {
565
+ return { ...operation, targetZone: `after-${extractBlockId(operation.targetZone)}` }
566
+ }
567
+
568
+ // Allow: return void/undefined to proceed as-is
569
+ }}
570
+ />
571
+ ```
572
+
573
+ The middleware receives a `MoveOperation` with the block, its original position (`from`), and the target drop zone. Returning:
574
+ - `false` cancels the move entirely
575
+ - A modified `MoveOperation` transforms the move (e.g. redirect to a different zone)
576
+ - `void` / `undefined` allows the move as-is
577
+
578
+ ## Fractional Indexing
579
+
580
+ By default, siblings are reindexed `0, 1, 2, ...` on every move. For collaborative or CRDT-compatible scenarios, use fractional indexing:
581
+
582
+ ```tsx
583
+ import { BlockTree, initFractionalOrder } from 'dnd-block-tree'
584
+
585
+ // Convert existing blocks to fractional ordering
586
+ const [blocks, setBlocks] = useState(() => initFractionalOrder(initialBlocks))
587
+
588
+ <BlockTree
589
+ blocks={blocks}
590
+ onChange={setBlocks}
591
+ orderingStrategy="fractional"
592
+ />
593
+ ```
594
+
595
+ 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.
596
+
597
+ Related utilities:
598
+
599
+ ```typescript
600
+ import {
601
+ generateKeyBetween, // Generate a key between two existing keys
602
+ generateNKeysBetween, // Generate N keys between two existing keys
603
+ generateInitialKeys, // Generate N evenly-spaced initial keys
604
+ initFractionalOrder, // Convert integer-ordered blocks to fractional
605
+ compareFractionalKeys, // Compare two fractional keys
606
+ } from 'dnd-block-tree'
607
+ ```
608
+
609
+ ## Collision Detection
610
+
611
+ The library ships with three collision detection strategies:
612
+
613
+ ```typescript
614
+ import {
615
+ weightedVerticalCollision, // Default: edge-distance based, depth-aware
616
+ closestCenterCollision, // Simple closest-center algorithm
617
+ createStickyCollision, // Wraps any strategy with hysteresis to prevent flickering
618
+ } from 'dnd-block-tree'
619
+
620
+ // Use a custom collision strategy
621
+ <BlockTree collisionDetection={closestCenterCollision} ... />
622
+
623
+ // Or use the sticky wrapper with a custom threshold (px)
624
+ const collision = createStickyCollision(20)
625
+ <BlockTree collisionDetection={collision} ... />
626
+ ```
627
+
628
+ You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
629
+
127
630
  ## Type Safety
128
631
 
129
632
  The library provides automatic type inference for container vs non-container renderers:
@@ -147,14 +650,56 @@ const renderers: BlockRenderers<MyBlock, typeof CONTAINER_TYPES> = {
147
650
 
148
651
  ## Utilities
149
652
 
150
- The library exports several utility functions:
653
+ The library exports utility functions for tree manipulation, ID generation, and zone parsing:
151
654
 
152
655
  ```typescript
153
656
  import {
154
- computeNormalizedIndex, // Convert flat array to normalized index
155
- buildOrderedBlocks, // Convert index back to ordered array
156
- reparentBlockIndex, // Move a block to a new position
157
- generateId, // Generate unique block IDs
657
+ // Tree operations
658
+ computeNormalizedIndex, // Convert flat array to { byId, byParent } index
659
+ buildOrderedBlocks, // Convert index back to ordered flat array
660
+ reparentBlockIndex, // Move a single block to a new position
661
+ reparentMultipleBlocks, // Move multiple blocks preserving relative order
662
+ getDescendantIds, // Get all descendant IDs of a block (Set)
663
+ deleteBlockAndDescendants, // Remove a block and all its descendants from index
664
+ getBlockDepth, // Compute depth of a block (root = 1)
665
+ getSubtreeDepth, // Max depth of a subtree (leaf = 1)
666
+
667
+ // Serialization
668
+ flatToNested, // Convert flat block array to nested tree
669
+ nestedToFlat, // Convert nested tree to flat block array
670
+
671
+ // ID / zone helpers
672
+ generateId, // Generate unique block IDs
673
+ extractUUID, // Extract block ID from zone ID string
674
+ getDropZoneType, // Parse zone type: 'before' | 'after' | 'into'
675
+ extractBlockId, // Extract block ID from zone ID (alias)
676
+
677
+ // Touch
678
+ triggerHaptic, // Trigger haptic feedback (navigator.vibrate)
679
+
680
+ // Fractional indexing
681
+ generateKeyBetween, // Generate a fractional key between two keys
682
+ generateNKeysBetween, // Generate N keys between two existing keys
683
+ generateInitialKeys, // Generate N evenly-spaced initial keys
684
+ initFractionalOrder, // Convert integer-ordered blocks to fractional
685
+ compareFractionalKeys, // Compare two fractional keys
686
+
687
+ // Collision detection
688
+ weightedVerticalCollision, // Edge-distance collision, depth-aware
689
+ closestCenterCollision, // Simple closest-center collision
690
+ createStickyCollision, // Hysteresis wrapper to prevent flickering
691
+
692
+ // Hooks
693
+ useBlockHistory, // Undo/redo state management
694
+ useLayoutAnimation, // FLIP-based reorder animations
695
+ useVirtualTree, // Virtual scrolling primitives
696
+
697
+ // Components
698
+ BlockTree, // Main drag-and-drop tree component
699
+ BlockTreeSSR, // SSR-safe wrapper
700
+ TreeRenderer, // Recursive tree renderer
701
+ DropZone, // Individual drop zone
702
+ DragOverlay, // Drag overlay wrapper
158
703
  } from 'dnd-block-tree'
159
704
  ```
160
705
 
@@ -162,13 +707,13 @@ import {
162
707
 
163
708
  Check out the [live demo](https://dnd-block-tree.vercel.app) to see the library in action with two example use cases:
164
709
 
165
- - **Productivity** - Sections, tasks, and notes
710
+ - **Productivity** - Sections, tasks, and notes with undo/redo, max depth control, and keyboard navigation
166
711
  - **File System** - Folders and files
167
712
 
168
713
  ## Built With
169
714
 
170
715
  - [dnd-kit](https://dndkit.com/) - Modern drag and drop toolkit for React
171
- - React 18+
716
+ - React 18+ / React 19
172
717
  - TypeScript
173
718
 
174
719
  ## License