dnd-block-tree 0.4.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/dist/index.d.ts CHANGED
@@ -213,6 +213,10 @@ interface SensorConfig {
213
213
  activationDistance?: number;
214
214
  activationDelay?: number;
215
215
  tolerance?: number;
216
+ /** Override the default long-press delay (200ms) for touch sensors */
217
+ longPressDelay?: number;
218
+ /** Trigger haptic feedback (vibration) on drag start for touch devices */
219
+ hapticFeedback?: boolean;
216
220
  }
217
221
  /**
218
222
  * Drop zone configuration
@@ -466,12 +470,19 @@ interface BlockTreeProps<T extends BaseBlock, C extends readonly T['type'][] = r
466
470
  selectedIds?: Set<string>;
467
471
  /** Called when selection changes (for multi-select) */
468
472
  onSelectionChange?: (selectedIds: Set<string>) => void;
473
+ /** Enable virtual scrolling for large trees (fixed item height only) */
474
+ virtualize?: {
475
+ /** Fixed height of each item in pixels */
476
+ itemHeight: number;
477
+ /** Number of extra items to render outside the visible range (default: 5) */
478
+ overscan?: number;
479
+ };
469
480
  }
470
481
  /**
471
482
  * Main BlockTree component
472
483
  * Provides drag-and-drop functionality for hierarchical block structures
473
484
  */
474
- declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
485
+ declare function BlockTree<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ blocks, renderers, containerTypes, onChange, dragOverlay, activationDistance, previewDebounce, className, dropZoneClassName, dropZoneActiveClassName, indentClassName, showDropPreview, onDragStart, onDragMove, onDragEnd, onDragCancel, onBeforeMove, onBlockMove, onExpandChange, onHoverChange, canDrag, canDrop, collisionDetection, sensors: sensorConfig, animation, initialExpanded, orderingStrategy, maxDepth, keyboardNavigation, multiSelect, selectedIds: externalSelectedIds, onSelectionChange, virtualize, }: BlockTreeProps<T, C>): react_jsx_runtime.JSX.Element;
475
486
 
476
487
  interface TreeRendererProps<T extends BaseBlock> {
477
488
  blocks: T[];
@@ -502,11 +513,15 @@ interface TreeRendererProps<T extends BaseBlock> {
502
513
  selectedIds?: Set<string>;
503
514
  /** Click handler for multi-select */
504
515
  onBlockClick?: (blockId: string, event: React.MouseEvent) => void;
516
+ /** Animation configuration */
517
+ animation?: AnimationConfig;
518
+ /** When virtual scrolling is active, only render blocks in this set */
519
+ virtualVisibleIds?: Set<string> | null;
505
520
  }
506
521
  /**
507
522
  * Recursive tree renderer with smart drop zones
508
523
  */
509
- declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
524
+ declare function TreeRenderer<T extends BaseBlock>({ blocks, blocksByParent, parentId, activeId, expandedMap, renderers, containerTypes, onHover, onToggleExpand, depth, dropZoneClassName, dropZoneActiveClassName, indentClassName, rootClassName, canDrag, previewPosition, draggedBlock, focusedId, selectedIds, onBlockClick, animation, virtualVisibleIds, }: TreeRendererProps<T>): react_jsx_runtime.JSX.Element;
510
525
 
511
526
  interface DropZoneProps {
512
527
  id: string;
@@ -536,6 +551,17 @@ interface DragOverlayProps<T extends BaseBlock> {
536
551
  */
537
552
  declare function DragOverlay<T extends BaseBlock>({ activeBlock, children, selectedCount, }: DragOverlayProps<T>): react_jsx_runtime.JSX.Element;
538
553
 
554
+ interface BlockTreeSSRProps<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]> extends BlockTreeProps<T, C> {
555
+ /** Content to render before hydration completes (default: null) */
556
+ fallback?: ReactNode;
557
+ }
558
+ /**
559
+ * Hydration-safe wrapper for BlockTree in SSR environments.
560
+ * Renders the fallback (or nothing) on the server and during initial hydration,
561
+ * then mounts the full BlockTree after the client has hydrated.
562
+ */
563
+ declare function BlockTreeSSR<T extends BaseBlock, C extends readonly T['type'][] = readonly T['type'][]>({ fallback, ...props }: BlockTreeSSRProps<T, C>): react_jsx_runtime.JSX.Element;
564
+
539
565
  /**
540
566
  * Create block state context and hooks
541
567
  */
@@ -588,6 +614,58 @@ interface UseBlockHistoryResult<T extends BaseBlock> {
588
614
  */
589
615
  declare function useBlockHistory<T extends BaseBlock>(initialBlocks: T[], options?: UseBlockHistoryOptions): UseBlockHistoryResult<T>;
590
616
 
617
+ interface UseLayoutAnimationOptions {
618
+ /** Duration of the transition in ms (default: 200) */
619
+ duration?: number;
620
+ /** CSS easing function (default: 'ease') */
621
+ easing?: string;
622
+ /** CSS selector for animated children (default: '[data-block-id]') */
623
+ selector?: string;
624
+ }
625
+ /**
626
+ * FLIP-based layout animation hook for reorder transitions.
627
+ *
628
+ * Captures block positions before render (layout effect cleanup),
629
+ * computes the delta after render, and applies a CSS transform transition.
630
+ *
631
+ * Usage:
632
+ * ```tsx
633
+ * const containerRef = useRef<HTMLDivElement>(null)
634
+ * useLayoutAnimation(containerRef, { duration: 200 })
635
+ * return <div ref={containerRef}>...</div>
636
+ * ```
637
+ */
638
+ declare function useLayoutAnimation(containerRef: React.RefObject<HTMLElement | null>, options?: UseLayoutAnimationOptions): void;
639
+
640
+ interface UseVirtualTreeOptions {
641
+ /** Ref to the scrollable container element */
642
+ containerRef: React.RefObject<HTMLElement | null>;
643
+ /** Total number of items in the tree */
644
+ itemCount: number;
645
+ /** Fixed height of each item in pixels */
646
+ itemHeight: number;
647
+ /** Number of extra items to render outside the visible range (default: 5) */
648
+ overscan?: number;
649
+ }
650
+ interface UseVirtualTreeResult {
651
+ /** Start and end indices of the visible range (inclusive) */
652
+ visibleRange: {
653
+ start: number;
654
+ end: number;
655
+ };
656
+ /** Total height of all items for the spacer div */
657
+ totalHeight: number;
658
+ /** Offset from top for the first rendered item */
659
+ offsetY: number;
660
+ }
661
+ /**
662
+ * Lightweight fixed-height virtual scrolling hook for tree lists.
663
+ *
664
+ * Tracks scroll position on the container and computes which items
665
+ * should be rendered based on the viewport and overscan.
666
+ */
667
+ declare function useVirtualTree({ containerRef, itemCount, itemHeight, overscan, }: UseVirtualTreeOptions): UseVirtualTreeResult;
668
+
591
669
  /**
592
670
  * Clone a Map
593
671
  */
@@ -659,6 +737,29 @@ declare function debounce<Args extends unknown[]>(fn: (...args: Args) => void, d
659
737
  * Generate a unique ID (simple implementation)
660
738
  */
661
739
  declare function generateId(): string;
740
+ /**
741
+ * Trigger haptic feedback (vibration) on supported devices.
742
+ * Safe to call in any environment — no-ops when `navigator.vibrate` is unavailable.
743
+ */
744
+ declare function triggerHaptic(durationMs?: number): void;
745
+
746
+ /**
747
+ * A nested/tree representation of a block.
748
+ * Omits `parentId` and `order` since they are reconstructed during flattening.
749
+ */
750
+ type NestedBlock<T extends BaseBlock> = Omit<T, 'parentId' | 'order'> & {
751
+ children: NestedBlock<T>[];
752
+ };
753
+ /**
754
+ * Convert a flat block array into a nested tree structure.
755
+ * Groups blocks by parentId, sorts siblings by order, and recursively builds children arrays.
756
+ */
757
+ declare function flatToNested<T extends BaseBlock>(blocks: T[]): NestedBlock<T>[];
758
+ /**
759
+ * Convert a nested tree structure back to a flat block array.
760
+ * DFS walk assigning parentId and integer order on the way down.
761
+ */
762
+ declare function nestedToFlat<T extends BaseBlock>(nested: NestedBlock<T>[]): T[];
662
763
 
663
764
  /**
664
765
  * Fractional indexing utilities
@@ -710,4 +811,4 @@ declare function initFractionalOrder<T extends {
710
811
  order: number | string;
711
812
  }>(blocks: T[]): T[];
712
813
 
713
- export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, reparentBlockIndex, reparentMultipleBlocks, useBlockHistory, useConfiguredSensors, weightedVerticalCollision };
814
+ export { type AnimationConfig, type AutoExpandConfig, type BaseBlock, type BlockAction, type BlockAddEvent, type BlockDeleteEvent, type BlockIndex, type BlockMoveEvent, type BlockPosition, type BlockRendererProps, type BlockRenderers, type BlockStateContextValue, type BlockStateProviderProps, BlockTree, type BlockTreeCallbacks, type BlockTreeConfig, type BlockTreeCustomization, type BlockTreeProps, BlockTreeSSR, type BlockTreeSSRProps, type CanDragFn, type CanDropFn, type ContainerRendererProps, type DragEndEvent, type DragMoveEvent, DragOverlay, type DragOverlayProps$1 as DragOverlayProps, type DragStartEvent, DropZone, type DropZoneConfig, type DropZoneProps, type DropZoneType, type ExpandChangeEvent, type HoverChangeEvent, type IdGeneratorFn, type InternalRenderers, type MoveOperation, type NestedBlock, type OrderingStrategy, type RendererPropsFor, type SensorConfig, TreeRenderer, type TreeRendererProps, type TreeStateContextValue, type TreeStateProviderProps, type UseBlockHistoryOptions, type UseBlockHistoryResult, type UseLayoutAnimationOptions, type UseVirtualTreeOptions, type UseVirtualTreeResult, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
package/dist/index.js CHANGED
@@ -135,7 +135,7 @@ function useConfiguredSensors(config = {}) {
135
135
  distance: activationDistance
136
136
  };
137
137
  touchConstraint = {
138
- delay: 200,
138
+ delay: config.longPressDelay ?? 200,
139
139
  tolerance: 5
140
140
  };
141
141
  }
@@ -197,6 +197,11 @@ function debounce(fn, delay) {
197
197
  function generateId() {
198
198
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
199
199
  }
200
+ function triggerHaptic(durationMs = 10) {
201
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
202
+ navigator.vibrate(durationMs);
203
+ }
204
+ }
200
205
  function DropZoneComponent({
201
206
  id,
202
207
  parentId,
@@ -278,10 +283,15 @@ function TreeRenderer({
278
283
  draggedBlock,
279
284
  focusedId,
280
285
  selectedIds,
281
- onBlockClick
286
+ onBlockClick,
287
+ animation,
288
+ virtualVisibleIds
282
289
  }) {
283
290
  const items = blocksByParent.get(parentId) ?? [];
284
- const filteredBlocks = items.filter((block) => block.id !== activeId);
291
+ let filteredBlocks = items.filter((block) => block.id !== activeId);
292
+ if (virtualVisibleIds && depth === 0) {
293
+ filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
294
+ }
285
295
  const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
286
296
  const containerClass = depth === 0 ? rootClassName : indentClassName;
287
297
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
@@ -325,7 +335,11 @@ function TreeRenderer({
325
335
  onBlockClick,
326
336
  children: ({ isDragging }) => {
327
337
  if (isContainer) {
328
- const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(
338
+ const expandStyle = animation?.expandDuration ? {
339
+ transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
340
+ opacity: isExpanded ? 1 : 0
341
+ } : void 0;
342
+ const childContent = isExpanded ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
329
343
  TreeRenderer,
330
344
  {
331
345
  blocks,
@@ -347,7 +361,9 @@ function TreeRenderer({
347
361
  draggedBlock,
348
362
  focusedId,
349
363
  selectedIds,
350
- onBlockClick
364
+ onBlockClick,
365
+ animation,
366
+ virtualVisibleIds
351
367
  }
352
368
  ) }) : null;
353
369
  return Renderer({
@@ -786,18 +802,21 @@ function BlockTree({
786
802
  canDrop,
787
803
  collisionDetection,
788
804
  sensors: sensorConfig,
805
+ animation,
789
806
  initialExpanded,
790
807
  orderingStrategy = "integer",
791
808
  maxDepth,
792
809
  keyboardNavigation = false,
793
810
  multiSelect = false,
794
811
  selectedIds: externalSelectedIds,
795
- onSelectionChange
812
+ onSelectionChange,
813
+ virtualize
796
814
  }) {
797
815
  const sensors = useConfiguredSensors({
798
816
  activationDistance: sensorConfig?.activationDistance ?? activationDistance,
799
817
  activationDelay: sensorConfig?.activationDelay,
800
- tolerance: sensorConfig?.tolerance
818
+ tolerance: sensorConfig?.tolerance,
819
+ longPressDelay: sensorConfig?.longPressDelay
801
820
  });
802
821
  const initialExpandedMap = react.useMemo(
803
822
  () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
@@ -1008,12 +1027,15 @@ function BlockTree({
1008
1027
  setSelectedIds(/* @__PURE__ */ new Set([id]));
1009
1028
  }
1010
1029
  }
1030
+ if (sensorConfig?.hapticFeedback) {
1031
+ triggerHaptic();
1032
+ }
1011
1033
  stateRef.current.activeId = id;
1012
1034
  stateRef.current.isDragging = true;
1013
1035
  initialBlocksRef.current = [...blocks];
1014
1036
  cachedReorderRef.current = null;
1015
1037
  forceRender();
1016
- }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1038
+ }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1017
1039
  const handleDragMove = react.useCallback((event) => {
1018
1040
  if (!onDragMove) return;
1019
1041
  const id = stateRef.current.activeId;
@@ -1207,6 +1229,61 @@ function BlockTree({
1207
1229
  forceRender();
1208
1230
  }, [blocks, onExpandChange]);
1209
1231
  toggleExpandRef.current = handleToggleExpand;
1232
+ const virtualContainerRef = react.useRef(null);
1233
+ const [virtualScroll, setVirtualScroll] = react.useState({ scrollTop: 0, clientHeight: 0 });
1234
+ react.useEffect(() => {
1235
+ if (!virtualize) return;
1236
+ const el = virtualContainerRef.current;
1237
+ if (!el) return;
1238
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1239
+ const onScroll = () => {
1240
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1241
+ };
1242
+ el.addEventListener("scroll", onScroll, { passive: true });
1243
+ return () => el.removeEventListener("scroll", onScroll);
1244
+ }, [virtualize]);
1245
+ const virtualResult = react.useMemo(() => {
1246
+ if (!virtualize) return null;
1247
+ const { itemHeight, overscan = 5 } = virtualize;
1248
+ const { scrollTop, clientHeight } = virtualScroll;
1249
+ const totalHeight = visibleBlockIds.length * itemHeight;
1250
+ const startRaw = Math.floor(scrollTop / itemHeight);
1251
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
1252
+ const start = Math.max(0, startRaw - overscan);
1253
+ const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
1254
+ const offsetY = start * itemHeight;
1255
+ const visibleSet = /* @__PURE__ */ new Set();
1256
+ for (let i = start; i <= end; i++) {
1257
+ visibleSet.add(visibleBlockIds[i]);
1258
+ }
1259
+ return { totalHeight, offsetY, visibleSet };
1260
+ }, [virtualize, virtualScroll, visibleBlockIds]);
1261
+ const treeContent = /* @__PURE__ */ jsxRuntime.jsx(
1262
+ TreeRenderer,
1263
+ {
1264
+ blocks,
1265
+ blocksByParent,
1266
+ parentId: null,
1267
+ activeId: stateRef.current.activeId,
1268
+ expandedMap: stateRef.current.expandedMap,
1269
+ renderers,
1270
+ containerTypes,
1271
+ onHover: handleHover,
1272
+ onToggleExpand: handleToggleExpand,
1273
+ dropZoneClassName,
1274
+ dropZoneActiveClassName,
1275
+ indentClassName,
1276
+ rootClassName: className,
1277
+ canDrag,
1278
+ previewPosition,
1279
+ draggedBlock,
1280
+ focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1281
+ selectedIds: multiSelect ? selectedIds : void 0,
1282
+ onBlockClick: multiSelect ? handleBlockClick : void 0,
1283
+ animation,
1284
+ virtualVisibleIds: virtualResult?.visibleSet ?? null
1285
+ }
1286
+ );
1210
1287
  return /* @__PURE__ */ jsxRuntime.jsxs(
1211
1288
  core.DndContext,
1212
1289
  {
@@ -1218,7 +1295,20 @@ function BlockTree({
1218
1295
  onDragEnd: handleDragEnd,
1219
1296
  onDragCancel: handleDragCancel,
1220
1297
  children: [
1221
- /* @__PURE__ */ jsxRuntime.jsx(
1298
+ virtualize ? /* @__PURE__ */ jsxRuntime.jsx(
1299
+ "div",
1300
+ {
1301
+ ref: (el) => {
1302
+ virtualContainerRef.current = el;
1303
+ rootRef.current = el;
1304
+ },
1305
+ className,
1306
+ style: { minWidth: 0, overflow: "auto", position: "relative" },
1307
+ onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1308
+ role: keyboardNavigation ? "tree" : void 0,
1309
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: virtualResult.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", top: virtualResult.offsetY, left: 0, right: 0 }, children: treeContent }) })
1310
+ }
1311
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1222
1312
  "div",
1223
1313
  {
1224
1314
  ref: rootRef,
@@ -1226,30 +1316,7 @@ function BlockTree({
1226
1316
  style: { minWidth: 0 },
1227
1317
  onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1228
1318
  role: keyboardNavigation ? "tree" : void 0,
1229
- children: /* @__PURE__ */ jsxRuntime.jsx(
1230
- TreeRenderer,
1231
- {
1232
- blocks,
1233
- blocksByParent,
1234
- parentId: null,
1235
- activeId: stateRef.current.activeId,
1236
- expandedMap: stateRef.current.expandedMap,
1237
- renderers,
1238
- containerTypes,
1239
- onHover: handleHover,
1240
- onToggleExpand: handleToggleExpand,
1241
- dropZoneClassName,
1242
- dropZoneActiveClassName,
1243
- indentClassName,
1244
- rootClassName: className,
1245
- canDrag,
1246
- previewPosition,
1247
- draggedBlock,
1248
- focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1249
- selectedIds: multiSelect ? selectedIds : void 0,
1250
- onBlockClick: multiSelect ? handleBlockClick : void 0
1251
- }
1252
- )
1319
+ children: treeContent
1253
1320
  }
1254
1321
  ),
1255
1322
  /* @__PURE__ */ jsxRuntime.jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
@@ -1257,6 +1324,16 @@ function BlockTree({
1257
1324
  }
1258
1325
  );
1259
1326
  }
1327
+ function BlockTreeSSR({ fallback = null, ...props }) {
1328
+ const [mounted, setMounted] = react.useState(false);
1329
+ react.useEffect(() => {
1330
+ setMounted(true);
1331
+ }, []);
1332
+ if (!mounted) {
1333
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
1334
+ }
1335
+ return /* @__PURE__ */ jsxRuntime.jsx(BlockTree, { ...props });
1336
+ }
1260
1337
  function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
1261
1338
  switch (action.type) {
1262
1339
  case "ADD_ITEM": {
@@ -1661,8 +1738,143 @@ function useBlockHistory(initialBlocks, options = {}) {
1661
1738
  canRedo: state.future.length > 0
1662
1739
  };
1663
1740
  }
1741
+ function useLayoutAnimation(containerRef, options = {}) {
1742
+ const {
1743
+ duration = 200,
1744
+ easing = "ease",
1745
+ selector = "[data-block-id]"
1746
+ } = options;
1747
+ const prevPositions = react.useRef(/* @__PURE__ */ new Map());
1748
+ react.useLayoutEffect(() => {
1749
+ const container = containerRef.current;
1750
+ if (!container) return;
1751
+ const children = container.querySelectorAll(selector);
1752
+ const currentPositions = /* @__PURE__ */ new Map();
1753
+ children.forEach((el) => {
1754
+ const id = el.dataset.blockId;
1755
+ if (id) {
1756
+ currentPositions.set(id, el.getBoundingClientRect());
1757
+ }
1758
+ });
1759
+ children.forEach((el) => {
1760
+ const htmlEl = el;
1761
+ const id = htmlEl.dataset.blockId;
1762
+ if (!id) return;
1763
+ const prev = prevPositions.current.get(id);
1764
+ const curr = currentPositions.get(id);
1765
+ if (!prev || !curr) return;
1766
+ const deltaY = prev.top - curr.top;
1767
+ const deltaX = prev.left - curr.left;
1768
+ if (deltaY === 0 && deltaX === 0) return;
1769
+ htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
1770
+ htmlEl.style.transition = "none";
1771
+ requestAnimationFrame(() => {
1772
+ htmlEl.style.transition = `transform ${duration}ms ${easing}`;
1773
+ htmlEl.style.transform = "";
1774
+ const onEnd = () => {
1775
+ htmlEl.style.transition = "";
1776
+ htmlEl.removeEventListener("transitionend", onEnd);
1777
+ };
1778
+ htmlEl.addEventListener("transitionend", onEnd);
1779
+ });
1780
+ });
1781
+ prevPositions.current = currentPositions;
1782
+ });
1783
+ }
1784
+ function useVirtualTree({
1785
+ containerRef,
1786
+ itemCount,
1787
+ itemHeight,
1788
+ overscan = 5
1789
+ }) {
1790
+ const [scrollTop, setScrollTop] = react.useState(0);
1791
+ const [containerHeight, setContainerHeight] = react.useState(0);
1792
+ const rafId = react.useRef(0);
1793
+ const handleScroll = react.useCallback(() => {
1794
+ cancelAnimationFrame(rafId.current);
1795
+ rafId.current = requestAnimationFrame(() => {
1796
+ const el = containerRef.current;
1797
+ if (el) {
1798
+ setScrollTop(el.scrollTop);
1799
+ setContainerHeight(el.clientHeight);
1800
+ }
1801
+ });
1802
+ }, [containerRef]);
1803
+ react.useEffect(() => {
1804
+ const el = containerRef.current;
1805
+ if (!el) return;
1806
+ setScrollTop(el.scrollTop);
1807
+ setContainerHeight(el.clientHeight);
1808
+ el.addEventListener("scroll", handleScroll, { passive: true });
1809
+ return () => {
1810
+ el.removeEventListener("scroll", handleScroll);
1811
+ cancelAnimationFrame(rafId.current);
1812
+ };
1813
+ }, [containerRef, handleScroll]);
1814
+ const totalHeight = itemCount * itemHeight;
1815
+ const startRaw = Math.floor(scrollTop / itemHeight);
1816
+ const visibleCount = Math.ceil(containerHeight / itemHeight);
1817
+ const start = Math.max(0, startRaw - overscan);
1818
+ const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
1819
+ const offsetY = start * itemHeight;
1820
+ return {
1821
+ visibleRange: { start, end },
1822
+ totalHeight,
1823
+ offsetY
1824
+ };
1825
+ }
1826
+
1827
+ // src/utils/serialization.ts
1828
+ function flatToNested(blocks) {
1829
+ const byParent = /* @__PURE__ */ new Map();
1830
+ for (const block of blocks) {
1831
+ const key = block.parentId ?? null;
1832
+ const list = byParent.get(key);
1833
+ if (list) {
1834
+ list.push(block);
1835
+ } else {
1836
+ byParent.set(key, [block]);
1837
+ }
1838
+ }
1839
+ for (const list of byParent.values()) {
1840
+ list.sort((a, b) => {
1841
+ if (typeof a.order === "string" && typeof b.order === "string") {
1842
+ return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
1843
+ }
1844
+ return Number(a.order) - Number(b.order);
1845
+ });
1846
+ }
1847
+ function buildChildren(parentId) {
1848
+ const siblings = byParent.get(parentId) ?? [];
1849
+ return siblings.map((block) => {
1850
+ const { parentId: _p, order: _o, ...rest } = block;
1851
+ return {
1852
+ ...rest,
1853
+ children: buildChildren(block.id)
1854
+ };
1855
+ });
1856
+ }
1857
+ return buildChildren(null);
1858
+ }
1859
+ function nestedToFlat(nested) {
1860
+ const result = [];
1861
+ function walk(nodes, parentId) {
1862
+ for (let i = 0; i < nodes.length; i++) {
1863
+ const { children, ...rest } = nodes[i];
1864
+ result.push({
1865
+ ...rest,
1866
+ parentId,
1867
+ order: i
1868
+ });
1869
+ walk(children, rest.id);
1870
+ }
1871
+ }
1872
+ walk(nested, null);
1873
+ return result;
1874
+ }
1664
1875
 
1665
1876
  exports.BlockTree = BlockTree;
1877
+ exports.BlockTreeSSR = BlockTreeSSR;
1666
1878
  exports.DragOverlay = DragOverlay;
1667
1879
  exports.DropZone = DropZone;
1668
1880
  exports.TreeRenderer = TreeRenderer;
@@ -1679,6 +1891,7 @@ exports.debounce = debounce;
1679
1891
  exports.deleteBlockAndDescendants = deleteBlockAndDescendants;
1680
1892
  exports.extractBlockId = extractBlockId;
1681
1893
  exports.extractUUID = extractUUID;
1894
+ exports.flatToNested = flatToNested;
1682
1895
  exports.generateId = generateId;
1683
1896
  exports.generateInitialKeys = generateInitialKeys;
1684
1897
  exports.generateKeyBetween = generateKeyBetween;
@@ -1689,10 +1902,14 @@ exports.getDropZoneType = getDropZoneType;
1689
1902
  exports.getSensorConfig = getSensorConfig;
1690
1903
  exports.getSubtreeDepth = getSubtreeDepth;
1691
1904
  exports.initFractionalOrder = initFractionalOrder;
1905
+ exports.nestedToFlat = nestedToFlat;
1692
1906
  exports.reparentBlockIndex = reparentBlockIndex;
1693
1907
  exports.reparentMultipleBlocks = reparentMultipleBlocks;
1908
+ exports.triggerHaptic = triggerHaptic;
1694
1909
  exports.useBlockHistory = useBlockHistory;
1695
1910
  exports.useConfiguredSensors = useConfiguredSensors;
1911
+ exports.useLayoutAnimation = useLayoutAnimation;
1912
+ exports.useVirtualTree = useVirtualTree;
1696
1913
  exports.weightedVerticalCollision = weightedVerticalCollision;
1697
1914
  //# sourceMappingURL=index.js.map
1698
1915
  //# sourceMappingURL=index.js.map