dnd-block-tree 0.5.0 → 1.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
@@ -13,8 +13,9 @@ A headless React library for building hierarchical drag-and-drop interfaces. Bri
13
13
  ## Features
14
14
 
15
15
  - **Stable Drop Zones** - Zones render based on original block positions, not preview state, ensuring consistent drop targets during drag
16
- - **Ghost Preview** - Semi-transparent preview shows where blocks will land without affecting zone positions
17
- - **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with hysteresis to prevent flickering
16
+ - **Ghost Preview** - In-flow semi-transparent preview shows where blocks will land with accurate layout
17
+ - **Snapshotted Collision** - Zone rects are frozen on drag start and re-measured after each ghost commit, preventing layout-shift feedback loops
18
+ - **Depth-Aware Collision** - Smart algorithm prefers nested zones when cursor is at indented levels, with cross-depth-aware hysteresis
18
19
  - **Mobile & Touch Support** - Separate touch/pointer activation constraints with configurable `longPressDelay` and optional `hapticFeedback`
19
20
  - **Snapshot-Based Computation** - State captured at drag start. All preview computations use snapshot, ensuring consistent behavior
20
21
  - **Debounced Preview** - 150ms debounced virtual state for smooth drag previews without jitter
@@ -627,6 +628,31 @@ const collision = createStickyCollision(20)
627
628
 
628
629
  You can also pass any `CollisionDetection` function from `@dnd-kit/core`.
629
630
 
631
+ ### Snapshotted Zone Rects
632
+
633
+ `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.
634
+
635
+ ```typescript
636
+ import { createStickyCollision, type SnapshotRectsRef } from 'dnd-block-tree'
637
+
638
+ const snapshotRef: SnapshotRectsRef = { current: null }
639
+ const collision = createStickyCollision(20, snapshotRef)
640
+
641
+ // Snapshot all zone rects after drag starts:
642
+ snapshotRef.current = new Map(
643
+ [...document.querySelectorAll('[data-zone-id]')].map(el => [
644
+ el.getAttribute('data-zone-id')!,
645
+ el.getBoundingClientRect(),
646
+ ])
647
+ )
648
+ ```
649
+
650
+ `BlockTree` handles this lifecycle automatically — zones are snapshotted on drag start and re-measured via `requestAnimationFrame` after each ghost position commit.
651
+
652
+ ### Cross-Depth Hysteresis
653
+
654
+ 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.
655
+
630
656
  ## Type Safety
631
657
 
632
658
  The library provides automatic type inference for container vs non-container renderers:
@@ -687,7 +713,8 @@ import {
687
713
  // Collision detection
688
714
  weightedVerticalCollision, // Edge-distance collision, depth-aware
689
715
  closestCenterCollision, // Simple closest-center collision
690
- createStickyCollision, // Hysteresis wrapper to prevent flickering
716
+ createStickyCollision, // Hysteresis wrapper with snapshot support
717
+ type SnapshotRectsRef, // Ref type for frozen zone rects
691
718
 
692
719
  // Hooks
693
720
  useBlockHistory, // Undo/redo state management
package/dist/index.d.mts CHANGED
@@ -389,6 +389,9 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
389
389
  blockMap: Map<string, T>;
390
390
  }
391
391
 
392
+ type SnapshotRectsRef = {
393
+ current: Map<UniqueIdentifier, DOMRect> | null;
394
+ };
392
395
  /**
393
396
  * Custom collision detection that scores drop zones by distance to nearest edge.
394
397
  * Uses edge-distance scoring with a bottom bias for more natural drag behavior.
@@ -404,8 +407,11 @@ declare const weightedVerticalCollision: CollisionDetection;
404
407
  * between adjacent drop zones.
405
408
  *
406
409
  * @param threshold - Minimum score improvement required to switch zones (default: 15px)
410
+ * @param snapshotRef - Optional ref to snapshotted zone rects. When populated,
411
+ * collision detection uses these frozen rects instead of live DOM measurements,
412
+ * preventing layout-shift feedback loops from in-flow ghost previews.
407
413
  */
408
- declare function createStickyCollision(threshold?: number): CollisionDetection & {
414
+ declare function createStickyCollision(threshold?: number, snapshotRef?: SnapshotRectsRef): CollisionDetection & {
409
415
  reset: () => void;
410
416
  };
411
417
  /**
@@ -811,4 +817,4 @@ declare function initFractionalOrder<T extends {
811
817
  order: number | string;
812
818
  }>(blocks: T[]): T[];
813
819
 
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 };
820
+ 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, type SnapshotRectsRef, 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.d.ts CHANGED
@@ -389,6 +389,9 @@ interface TreeStateProviderProps<T extends BaseBlock = BaseBlock> {
389
389
  blockMap: Map<string, T>;
390
390
  }
391
391
 
392
+ type SnapshotRectsRef = {
393
+ current: Map<UniqueIdentifier, DOMRect> | null;
394
+ };
392
395
  /**
393
396
  * Custom collision detection that scores drop zones by distance to nearest edge.
394
397
  * Uses edge-distance scoring with a bottom bias for more natural drag behavior.
@@ -404,8 +407,11 @@ declare const weightedVerticalCollision: CollisionDetection;
404
407
  * between adjacent drop zones.
405
408
  *
406
409
  * @param threshold - Minimum score improvement required to switch zones (default: 15px)
410
+ * @param snapshotRef - Optional ref to snapshotted zone rects. When populated,
411
+ * collision detection uses these frozen rects instead of live DOM measurements,
412
+ * preventing layout-shift feedback loops from in-flow ghost previews.
407
413
  */
408
- declare function createStickyCollision(threshold?: number): CollisionDetection & {
414
+ declare function createStickyCollision(threshold?: number, snapshotRef?: SnapshotRectsRef): CollisionDetection & {
409
415
  reset: () => void;
410
416
  };
411
417
  /**
@@ -811,4 +817,4 @@ declare function initFractionalOrder<T extends {
811
817
  order: number | string;
812
818
  }>(blocks: T[]): T[];
813
819
 
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 };
820
+ 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, type SnapshotRectsRef, 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
@@ -15,11 +15,11 @@ function extractBlockId(zoneId) {
15
15
  }
16
16
 
17
17
  // src/core/collision.ts
18
- function computeCollisionScores(droppableContainers, collisionRect) {
18
+ function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
19
19
  const pointerX = collisionRect.left + collisionRect.width / 2;
20
20
  const pointerY = collisionRect.top + collisionRect.height / 2;
21
21
  const candidates = droppableContainers.map((container) => {
22
- const rect = container.rect.current;
22
+ const rect = snapshotRects?.get(container.id) ?? container.rect.current;
23
23
  if (!rect) return null;
24
24
  const distanceToTop = Math.abs(pointerY - rect.top);
25
25
  const distanceToBottom = Math.abs(pointerY - rect.bottom);
@@ -29,16 +29,17 @@ function computeCollisionScores(droppableContainers, collisionRect) {
29
29
  const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
30
30
  let horizontalScore = 0;
31
31
  if (isWithinX) {
32
- horizontalScore = -rect.left * 0.1;
32
+ horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
33
33
  } else {
34
34
  const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
35
- horizontalScore = Math.min(distanceToZone, 50);
35
+ horizontalScore = distanceToZone * 2;
36
36
  }
37
37
  return {
38
38
  id: container.id,
39
39
  data: {
40
40
  droppableContainer: container,
41
- value: edgeDistance + bias + horizontalScore
41
+ value: edgeDistance + bias + horizontalScore,
42
+ left: rect.left
42
43
  }
43
44
  };
44
45
  }).filter((c) => c !== null);
@@ -57,14 +58,14 @@ var weightedVerticalCollision = ({
57
58
  const candidates = computeCollisionScores(droppableContainers, collisionRect);
58
59
  return candidates.slice(0, 1);
59
60
  };
60
- function createStickyCollision(threshold = 15) {
61
+ function createStickyCollision(threshold = 15, snapshotRef) {
61
62
  let currentZoneId = null;
62
63
  const detector = ({
63
64
  droppableContainers,
64
65
  collisionRect
65
66
  }) => {
66
67
  if (!collisionRect) return [];
67
- const candidates = computeCollisionScores(droppableContainers, collisionRect);
68
+ const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
68
69
  if (candidates.length === 0) return [];
69
70
  const bestCandidate = candidates[0];
70
71
  const bestScore = bestCandidate.data.value;
@@ -72,7 +73,11 @@ function createStickyCollision(threshold = 15) {
72
73
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
73
74
  if (currentCandidate) {
74
75
  const currentScore = currentCandidate.data.value;
75
- if (currentScore - bestScore < threshold) {
76
+ const currentLeft = currentCandidate.data.left;
77
+ const bestLeft = bestCandidate.data.left;
78
+ const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
79
+ const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
80
+ if (currentScore - bestScore < effectiveThreshold) {
76
81
  return [currentCandidate];
77
82
  }
78
83
  }
@@ -234,6 +239,9 @@ function DropZoneComponent({
234
239
  );
235
240
  }
236
241
  var DropZone = react.memo(DropZoneComponent);
242
+ function GhostPreview({ children }) {
243
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
244
+ }
237
245
  function DraggableBlock({
238
246
  block,
239
247
  children,
@@ -320,7 +328,7 @@ function TreeRenderer({
320
328
  }
321
329
  const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
322
330
  return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
323
- ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
331
+ ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
324
332
  block: draggedBlock,
325
333
  isDragging: true,
326
334
  depth
@@ -398,7 +406,7 @@ function TreeRenderer({
398
406
  }),
399
407
  showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
400
408
  const GhostRenderer = renderers[draggedBlock.type];
401
- return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
409
+ return GhostRenderer ? /* @__PURE__ */ jsxRuntime.jsx(GhostPreview, { children: GhostRenderer({
402
410
  block: draggedBlock,
403
411
  isDragging: true,
404
412
  depth
@@ -650,6 +658,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
650
658
  }
651
659
  if (dragged.id === zoneTargetId) return state;
652
660
  const oldList = byParent.get(oldParentId) ?? [];
661
+ const currentIndexInOldParent = oldList.indexOf(dragged.id);
662
+ const preNewList = byParent.get(newParentId) ?? [];
663
+ let targetIndex;
664
+ if (isInto) {
665
+ targetIndex = 0;
666
+ } else if (isEnd) {
667
+ targetIndex = preNewList.length;
668
+ } else {
669
+ const idx = preNewList.indexOf(zoneTargetId);
670
+ targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
671
+ }
672
+ if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
673
+ const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
674
+ if (adjustedTarget === currentIndexInOldParent) {
675
+ return state;
676
+ }
677
+ }
653
678
  const filtered = oldList.filter((id) => id !== dragged.id);
654
679
  byParent.set(oldParentId, filtered);
655
680
  const newList = [...byParent.get(newParentId) ?? []];
@@ -662,10 +687,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
662
687
  const idx = newList.indexOf(zoneTargetId);
663
688
  insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
664
689
  }
665
- const currentIndex = newList.indexOf(dragged.id);
666
- if (dragged.parentId === newParentId && currentIndex === insertIndex) {
667
- return state;
668
- }
669
690
  newList.splice(insertIndex, 0, dragged.id);
670
691
  byParent.set(newParentId, newList);
671
692
  let newOrder = dragged.order;
@@ -835,7 +856,20 @@ function BlockTree({
835
856
  const cachedReorderRef = react.useRef(null);
836
857
  const fromPositionRef = react.useRef(null);
837
858
  const draggedIdsRef = react.useRef([]);
838
- const stickyCollisionRef = react.useRef(createStickyCollision(20));
859
+ const snapshotRectsRef = react.useRef(null);
860
+ const needsResnapshot = react.useRef(false);
861
+ const stickyCollisionRef = react.useRef(createStickyCollision(20, snapshotRectsRef));
862
+ const snapshotZoneRects = react.useCallback(() => {
863
+ const root = rootRef.current;
864
+ if (!root) return;
865
+ const zones = root.querySelectorAll("[data-zone-id]");
866
+ const map = /* @__PURE__ */ new Map();
867
+ zones.forEach((el) => {
868
+ const id = el.getAttribute("data-zone-id");
869
+ if (id) map.set(id, el.getBoundingClientRect());
870
+ });
871
+ snapshotRectsRef.current = map;
872
+ }, []);
839
873
  const [, forceRender] = react.useReducer((x) => x + 1, 0);
840
874
  const debouncedSetVirtual = react.useRef(
841
875
  debounce((newBlocks) => {
@@ -844,6 +878,7 @@ function BlockTree({
844
878
  } else {
845
879
  stateRef.current.virtualState = null;
846
880
  }
881
+ needsResnapshot.current = true;
847
882
  forceRender();
848
883
  }, previewDebounce)
849
884
  ).current;
@@ -982,6 +1017,13 @@ function BlockTree({
982
1017
  }
983
1018
  lastClickedIdRef.current = blockId;
984
1019
  }, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1020
+ react.useEffect(() => {
1021
+ if (!needsResnapshot.current || !stateRef.current.isDragging) return;
1022
+ needsResnapshot.current = false;
1023
+ requestAnimationFrame(() => {
1024
+ snapshotZoneRects();
1025
+ });
1026
+ });
985
1027
  react.useEffect(() => {
986
1028
  if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
987
1029
  const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
@@ -1034,6 +1076,7 @@ function BlockTree({
1034
1076
  stateRef.current.isDragging = true;
1035
1077
  initialBlocksRef.current = [...blocks];
1036
1078
  cachedReorderRef.current = null;
1079
+ needsResnapshot.current = true;
1037
1080
  forceRender();
1038
1081
  }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1039
1082
  const handleDragMove = react.useCallback((event) => {
@@ -1109,6 +1152,7 @@ function BlockTree({
1109
1152
  cachedReorderRef.current = null;
1110
1153
  initialBlocksRef.current = [];
1111
1154
  fromPositionRef.current = null;
1155
+ snapshotRectsRef.current = null;
1112
1156
  forceRender();
1113
1157
  return;
1114
1158
  }
@@ -1148,6 +1192,7 @@ function BlockTree({
1148
1192
  initialBlocksRef.current = [];
1149
1193
  fromPositionRef.current = null;
1150
1194
  draggedIdsRef.current = [];
1195
+ snapshotRectsRef.current = null;
1151
1196
  if (cached && onChange) {
1152
1197
  onChange(cached.reorderedBlocks);
1153
1198
  }
@@ -1176,6 +1221,7 @@ function BlockTree({
1176
1221
  initialBlocksRef.current = [];
1177
1222
  fromPositionRef.current = null;
1178
1223
  draggedIdsRef.current = [];
1224
+ snapshotRectsRef.current = null;
1179
1225
  forceRender();
1180
1226
  }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
1181
1227
  const handleHover = react.useCallback((zoneId, _parentId) => {