dnd-block-tree 0.4.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/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useDroppable, useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DragOverlay as DragOverlay$1, DndContext, useDraggable } from '@dnd-kit/core';
2
- import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, createContext, useContext, useState } from 'react';
2
+ import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, useState, createContext, useLayoutEffect, useContext } from 'react';
3
3
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
 
5
5
  // src/core/types.ts
@@ -13,11 +13,11 @@ function extractBlockId(zoneId) {
13
13
  }
14
14
 
15
15
  // src/core/collision.ts
16
- function computeCollisionScores(droppableContainers, collisionRect) {
16
+ function computeCollisionScores(droppableContainers, collisionRect, snapshotRects) {
17
17
  const pointerX = collisionRect.left + collisionRect.width / 2;
18
18
  const pointerY = collisionRect.top + collisionRect.height / 2;
19
19
  const candidates = droppableContainers.map((container) => {
20
- const rect = container.rect.current;
20
+ const rect = snapshotRects?.get(container.id) ?? container.rect.current;
21
21
  if (!rect) return null;
22
22
  const distanceToTop = Math.abs(pointerY - rect.top);
23
23
  const distanceToBottom = Math.abs(pointerY - rect.bottom);
@@ -27,16 +27,17 @@ function computeCollisionScores(droppableContainers, collisionRect) {
27
27
  const isWithinX = pointerX >= rect.left && pointerX <= rect.right;
28
28
  let horizontalScore = 0;
29
29
  if (isWithinX) {
30
- horizontalScore = -rect.left * 0.1;
30
+ horizontalScore = Math.abs(pointerX - rect.left) * 0.3;
31
31
  } else {
32
32
  const distanceToZone = pointerX < rect.left ? rect.left - pointerX : pointerX - rect.right;
33
- horizontalScore = Math.min(distanceToZone, 50);
33
+ horizontalScore = distanceToZone * 2;
34
34
  }
35
35
  return {
36
36
  id: container.id,
37
37
  data: {
38
38
  droppableContainer: container,
39
- value: edgeDistance + bias + horizontalScore
39
+ value: edgeDistance + bias + horizontalScore,
40
+ left: rect.left
40
41
  }
41
42
  };
42
43
  }).filter((c) => c !== null);
@@ -55,14 +56,14 @@ var weightedVerticalCollision = ({
55
56
  const candidates = computeCollisionScores(droppableContainers, collisionRect);
56
57
  return candidates.slice(0, 1);
57
58
  };
58
- function createStickyCollision(threshold = 15) {
59
+ function createStickyCollision(threshold = 15, snapshotRef) {
59
60
  let currentZoneId = null;
60
61
  const detector = ({
61
62
  droppableContainers,
62
63
  collisionRect
63
64
  }) => {
64
65
  if (!collisionRect) return [];
65
- const candidates = computeCollisionScores(droppableContainers, collisionRect);
66
+ const candidates = computeCollisionScores(droppableContainers, collisionRect, snapshotRef?.current);
66
67
  if (candidates.length === 0) return [];
67
68
  const bestCandidate = candidates[0];
68
69
  const bestScore = bestCandidate.data.value;
@@ -70,7 +71,11 @@ function createStickyCollision(threshold = 15) {
70
71
  const currentCandidate = candidates.find((c) => c.id === currentZoneId);
71
72
  if (currentCandidate) {
72
73
  const currentScore = currentCandidate.data.value;
73
- if (currentScore - bestScore < threshold) {
74
+ const currentLeft = currentCandidate.data.left;
75
+ const bestLeft = bestCandidate.data.left;
76
+ const crossDepth = Math.abs(currentLeft - bestLeft) > 20;
77
+ const effectiveThreshold = crossDepth ? threshold * 0.25 : threshold;
78
+ if (currentScore - bestScore < effectiveThreshold) {
74
79
  return [currentCandidate];
75
80
  }
76
81
  }
@@ -133,7 +138,7 @@ function useConfiguredSensors(config = {}) {
133
138
  distance: activationDistance
134
139
  };
135
140
  touchConstraint = {
136
- delay: 200,
141
+ delay: config.longPressDelay ?? 200,
137
142
  tolerance: 5
138
143
  };
139
144
  }
@@ -195,6 +200,11 @@ function debounce(fn, delay) {
195
200
  function generateId() {
196
201
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
197
202
  }
203
+ function triggerHaptic(durationMs = 10) {
204
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
205
+ navigator.vibrate(durationMs);
206
+ }
207
+ }
198
208
  function DropZoneComponent({
199
209
  id,
200
210
  parentId,
@@ -227,6 +237,9 @@ function DropZoneComponent({
227
237
  );
228
238
  }
229
239
  var DropZone = memo(DropZoneComponent);
240
+ function GhostPreview({ children }) {
241
+ return /* @__PURE__ */ jsx("div", { "data-dnd-ghost": true, className: "opacity-50", style: { pointerEvents: "none" }, children });
242
+ }
230
243
  function DraggableBlock({
231
244
  block,
232
245
  children,
@@ -276,10 +289,15 @@ function TreeRenderer({
276
289
  draggedBlock,
277
290
  focusedId,
278
291
  selectedIds,
279
- onBlockClick
292
+ onBlockClick,
293
+ animation,
294
+ virtualVisibleIds
280
295
  }) {
281
296
  const items = blocksByParent.get(parentId) ?? [];
282
- const filteredBlocks = items.filter((block) => block.id !== activeId);
297
+ let filteredBlocks = items.filter((block) => block.id !== activeId);
298
+ if (virtualVisibleIds && depth === 0) {
299
+ filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
300
+ }
283
301
  const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
284
302
  const containerClass = depth === 0 ? rootClassName : indentClassName;
285
303
  return /* @__PURE__ */ jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
@@ -308,7 +326,7 @@ function TreeRenderer({
308
326
  }
309
327
  const GhostRenderer = draggedBlock ? renderers[draggedBlock.type] : null;
310
328
  return /* @__PURE__ */ jsxs(Fragment, { children: [
311
- ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
329
+ ghostBeforeThis && GhostRenderer && /* @__PURE__ */ jsx(GhostPreview, { children: GhostRenderer({
312
330
  block: draggedBlock,
313
331
  isDragging: true,
314
332
  depth
@@ -323,7 +341,11 @@ function TreeRenderer({
323
341
  onBlockClick,
324
342
  children: ({ isDragging }) => {
325
343
  if (isContainer) {
326
- const childContent = isExpanded ? /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
344
+ const expandStyle = animation?.expandDuration ? {
345
+ transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
346
+ opacity: isExpanded ? 1 : 0
347
+ } : void 0;
348
+ const childContent = isExpanded ? /* @__PURE__ */ jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsx(
327
349
  TreeRenderer,
328
350
  {
329
351
  blocks,
@@ -345,7 +367,9 @@ function TreeRenderer({
345
367
  draggedBlock,
346
368
  focusedId,
347
369
  selectedIds,
348
- onBlockClick
370
+ onBlockClick,
371
+ animation,
372
+ virtualVisibleIds
349
373
  }
350
374
  ) }) : null;
351
375
  return Renderer({
@@ -380,7 +404,7 @@ function TreeRenderer({
380
404
  }),
381
405
  showGhostHere && previewPosition.index >= filteredBlocks.length && draggedBlock && (() => {
382
406
  const GhostRenderer = renderers[draggedBlock.type];
383
- return GhostRenderer ? /* @__PURE__ */ jsx("div", { className: "opacity-50 pointer-events-none", style: { minWidth: 0 }, children: GhostRenderer({
407
+ return GhostRenderer ? /* @__PURE__ */ jsx(GhostPreview, { children: GhostRenderer({
384
408
  block: draggedBlock,
385
409
  isDragging: true,
386
410
  depth
@@ -632,6 +656,23 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
632
656
  }
633
657
  if (dragged.id === zoneTargetId) return state;
634
658
  const oldList = byParent.get(oldParentId) ?? [];
659
+ const currentIndexInOldParent = oldList.indexOf(dragged.id);
660
+ const preNewList = byParent.get(newParentId) ?? [];
661
+ let targetIndex;
662
+ if (isInto) {
663
+ targetIndex = 0;
664
+ } else if (isEnd) {
665
+ targetIndex = preNewList.length;
666
+ } else {
667
+ const idx = preNewList.indexOf(zoneTargetId);
668
+ targetIndex = idx === -1 ? preNewList.length : isAfter ? idx + 1 : idx;
669
+ }
670
+ if (oldParentId === newParentId && currentIndexInOldParent !== -1) {
671
+ const adjustedTarget = targetIndex > currentIndexInOldParent ? targetIndex - 1 : targetIndex;
672
+ if (adjustedTarget === currentIndexInOldParent) {
673
+ return state;
674
+ }
675
+ }
635
676
  const filtered = oldList.filter((id) => id !== dragged.id);
636
677
  byParent.set(oldParentId, filtered);
637
678
  const newList = [...byParent.get(newParentId) ?? []];
@@ -644,10 +685,6 @@ function reparentBlockIndex(state, activeId, targetZone, containerTypes = [], or
644
685
  const idx = newList.indexOf(zoneTargetId);
645
686
  insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx;
646
687
  }
647
- const currentIndex = newList.indexOf(dragged.id);
648
- if (dragged.parentId === newParentId && currentIndex === insertIndex) {
649
- return state;
650
- }
651
688
  newList.splice(insertIndex, 0, dragged.id);
652
689
  byParent.set(newParentId, newList);
653
690
  let newOrder = dragged.order;
@@ -784,18 +821,21 @@ function BlockTree({
784
821
  canDrop,
785
822
  collisionDetection,
786
823
  sensors: sensorConfig,
824
+ animation,
787
825
  initialExpanded,
788
826
  orderingStrategy = "integer",
789
827
  maxDepth,
790
828
  keyboardNavigation = false,
791
829
  multiSelect = false,
792
830
  selectedIds: externalSelectedIds,
793
- onSelectionChange
831
+ onSelectionChange,
832
+ virtualize
794
833
  }) {
795
834
  const sensors = useConfiguredSensors({
796
835
  activationDistance: sensorConfig?.activationDistance ?? activationDistance,
797
836
  activationDelay: sensorConfig?.activationDelay,
798
- tolerance: sensorConfig?.tolerance
837
+ tolerance: sensorConfig?.tolerance,
838
+ longPressDelay: sensorConfig?.longPressDelay
799
839
  });
800
840
  const initialExpandedMap = useMemo(
801
841
  () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
@@ -814,7 +854,20 @@ function BlockTree({
814
854
  const cachedReorderRef = useRef(null);
815
855
  const fromPositionRef = useRef(null);
816
856
  const draggedIdsRef = useRef([]);
817
- const stickyCollisionRef = useRef(createStickyCollision(20));
857
+ const snapshotRectsRef = useRef(null);
858
+ const needsResnapshot = useRef(false);
859
+ const stickyCollisionRef = useRef(createStickyCollision(20, snapshotRectsRef));
860
+ const snapshotZoneRects = useCallback(() => {
861
+ const root = rootRef.current;
862
+ if (!root) return;
863
+ const zones = root.querySelectorAll("[data-zone-id]");
864
+ const map = /* @__PURE__ */ new Map();
865
+ zones.forEach((el) => {
866
+ const id = el.getAttribute("data-zone-id");
867
+ if (id) map.set(id, el.getBoundingClientRect());
868
+ });
869
+ snapshotRectsRef.current = map;
870
+ }, []);
818
871
  const [, forceRender] = useReducer((x) => x + 1, 0);
819
872
  const debouncedSetVirtual = useRef(
820
873
  debounce((newBlocks) => {
@@ -823,6 +876,7 @@ function BlockTree({
823
876
  } else {
824
877
  stateRef.current.virtualState = null;
825
878
  }
879
+ needsResnapshot.current = true;
826
880
  forceRender();
827
881
  }, previewDebounce)
828
882
  ).current;
@@ -961,6 +1015,13 @@ function BlockTree({
961
1015
  }
962
1016
  lastClickedIdRef.current = blockId;
963
1017
  }, [multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1018
+ useEffect(() => {
1019
+ if (!needsResnapshot.current || !stateRef.current.isDragging) return;
1020
+ needsResnapshot.current = false;
1021
+ requestAnimationFrame(() => {
1022
+ snapshotZoneRects();
1023
+ });
1024
+ });
964
1025
  useEffect(() => {
965
1026
  if (!keyboardNavigation || !focusedIdRef.current || !rootRef.current) return;
966
1027
  const el = rootRef.current.querySelector(`[data-block-id="${focusedIdRef.current}"]`);
@@ -1006,12 +1067,16 @@ function BlockTree({
1006
1067
  setSelectedIds(/* @__PURE__ */ new Set([id]));
1007
1068
  }
1008
1069
  }
1070
+ if (sensorConfig?.hapticFeedback) {
1071
+ triggerHaptic();
1072
+ }
1009
1073
  stateRef.current.activeId = id;
1010
1074
  stateRef.current.isDragging = true;
1011
1075
  initialBlocksRef.current = [...blocks];
1012
1076
  cachedReorderRef.current = null;
1077
+ needsResnapshot.current = true;
1013
1078
  forceRender();
1014
- }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1079
+ }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1015
1080
  const handleDragMove = useCallback((event) => {
1016
1081
  if (!onDragMove) return;
1017
1082
  const id = stateRef.current.activeId;
@@ -1085,6 +1150,7 @@ function BlockTree({
1085
1150
  cachedReorderRef.current = null;
1086
1151
  initialBlocksRef.current = [];
1087
1152
  fromPositionRef.current = null;
1153
+ snapshotRectsRef.current = null;
1088
1154
  forceRender();
1089
1155
  return;
1090
1156
  }
@@ -1124,6 +1190,7 @@ function BlockTree({
1124
1190
  initialBlocksRef.current = [];
1125
1191
  fromPositionRef.current = null;
1126
1192
  draggedIdsRef.current = [];
1193
+ snapshotRectsRef.current = null;
1127
1194
  if (cached && onChange) {
1128
1195
  onChange(cached.reorderedBlocks);
1129
1196
  }
@@ -1152,6 +1219,7 @@ function BlockTree({
1152
1219
  initialBlocksRef.current = [];
1153
1220
  fromPositionRef.current = null;
1154
1221
  draggedIdsRef.current = [];
1222
+ snapshotRectsRef.current = null;
1155
1223
  forceRender();
1156
1224
  }, [blocks, debouncedSetVirtual, debouncedDragMove, onDragCancel, onDragEnd]);
1157
1225
  const handleHover = useCallback((zoneId, _parentId) => {
@@ -1205,6 +1273,61 @@ function BlockTree({
1205
1273
  forceRender();
1206
1274
  }, [blocks, onExpandChange]);
1207
1275
  toggleExpandRef.current = handleToggleExpand;
1276
+ const virtualContainerRef = useRef(null);
1277
+ const [virtualScroll, setVirtualScroll] = useState({ scrollTop: 0, clientHeight: 0 });
1278
+ useEffect(() => {
1279
+ if (!virtualize) return;
1280
+ const el = virtualContainerRef.current;
1281
+ if (!el) return;
1282
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1283
+ const onScroll = () => {
1284
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1285
+ };
1286
+ el.addEventListener("scroll", onScroll, { passive: true });
1287
+ return () => el.removeEventListener("scroll", onScroll);
1288
+ }, [virtualize]);
1289
+ const virtualResult = useMemo(() => {
1290
+ if (!virtualize) return null;
1291
+ const { itemHeight, overscan = 5 } = virtualize;
1292
+ const { scrollTop, clientHeight } = virtualScroll;
1293
+ const totalHeight = visibleBlockIds.length * itemHeight;
1294
+ const startRaw = Math.floor(scrollTop / itemHeight);
1295
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
1296
+ const start = Math.max(0, startRaw - overscan);
1297
+ const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
1298
+ const offsetY = start * itemHeight;
1299
+ const visibleSet = /* @__PURE__ */ new Set();
1300
+ for (let i = start; i <= end; i++) {
1301
+ visibleSet.add(visibleBlockIds[i]);
1302
+ }
1303
+ return { totalHeight, offsetY, visibleSet };
1304
+ }, [virtualize, virtualScroll, visibleBlockIds]);
1305
+ const treeContent = /* @__PURE__ */ jsx(
1306
+ TreeRenderer,
1307
+ {
1308
+ blocks,
1309
+ blocksByParent,
1310
+ parentId: null,
1311
+ activeId: stateRef.current.activeId,
1312
+ expandedMap: stateRef.current.expandedMap,
1313
+ renderers,
1314
+ containerTypes,
1315
+ onHover: handleHover,
1316
+ onToggleExpand: handleToggleExpand,
1317
+ dropZoneClassName,
1318
+ dropZoneActiveClassName,
1319
+ indentClassName,
1320
+ rootClassName: className,
1321
+ canDrag,
1322
+ previewPosition,
1323
+ draggedBlock,
1324
+ focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1325
+ selectedIds: multiSelect ? selectedIds : void 0,
1326
+ onBlockClick: multiSelect ? handleBlockClick : void 0,
1327
+ animation,
1328
+ virtualVisibleIds: virtualResult?.visibleSet ?? null
1329
+ }
1330
+ );
1208
1331
  return /* @__PURE__ */ jsxs(
1209
1332
  DndContext,
1210
1333
  {
@@ -1216,7 +1339,20 @@ function BlockTree({
1216
1339
  onDragEnd: handleDragEnd,
1217
1340
  onDragCancel: handleDragCancel,
1218
1341
  children: [
1219
- /* @__PURE__ */ jsx(
1342
+ virtualize ? /* @__PURE__ */ jsx(
1343
+ "div",
1344
+ {
1345
+ ref: (el) => {
1346
+ virtualContainerRef.current = el;
1347
+ rootRef.current = el;
1348
+ },
1349
+ className,
1350
+ style: { minWidth: 0, overflow: "auto", position: "relative" },
1351
+ onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1352
+ role: keyboardNavigation ? "tree" : void 0,
1353
+ children: /* @__PURE__ */ jsx("div", { style: { height: virtualResult.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: virtualResult.offsetY, left: 0, right: 0 }, children: treeContent }) })
1354
+ }
1355
+ ) : /* @__PURE__ */ jsx(
1220
1356
  "div",
1221
1357
  {
1222
1358
  ref: rootRef,
@@ -1224,30 +1360,7 @@ function BlockTree({
1224
1360
  style: { minWidth: 0 },
1225
1361
  onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1226
1362
  role: keyboardNavigation ? "tree" : void 0,
1227
- children: /* @__PURE__ */ jsx(
1228
- TreeRenderer,
1229
- {
1230
- blocks,
1231
- blocksByParent,
1232
- parentId: null,
1233
- activeId: stateRef.current.activeId,
1234
- expandedMap: stateRef.current.expandedMap,
1235
- renderers,
1236
- containerTypes,
1237
- onHover: handleHover,
1238
- onToggleExpand: handleToggleExpand,
1239
- dropZoneClassName,
1240
- dropZoneActiveClassName,
1241
- indentClassName,
1242
- rootClassName: className,
1243
- canDrag,
1244
- previewPosition,
1245
- draggedBlock,
1246
- focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1247
- selectedIds: multiSelect ? selectedIds : void 0,
1248
- onBlockClick: multiSelect ? handleBlockClick : void 0
1249
- }
1250
- )
1363
+ children: treeContent
1251
1364
  }
1252
1365
  ),
1253
1366
  /* @__PURE__ */ jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
@@ -1255,6 +1368,16 @@ function BlockTree({
1255
1368
  }
1256
1369
  );
1257
1370
  }
1371
+ function BlockTreeSSR({ fallback = null, ...props }) {
1372
+ const [mounted, setMounted] = useState(false);
1373
+ useEffect(() => {
1374
+ setMounted(true);
1375
+ }, []);
1376
+ if (!mounted) {
1377
+ return /* @__PURE__ */ jsx(Fragment$1, { children: fallback });
1378
+ }
1379
+ return /* @__PURE__ */ jsx(BlockTree, { ...props });
1380
+ }
1258
1381
  function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
1259
1382
  switch (action.type) {
1260
1383
  case "ADD_ITEM": {
@@ -1659,7 +1782,141 @@ function useBlockHistory(initialBlocks, options = {}) {
1659
1782
  canRedo: state.future.length > 0
1660
1783
  };
1661
1784
  }
1785
+ function useLayoutAnimation(containerRef, options = {}) {
1786
+ const {
1787
+ duration = 200,
1788
+ easing = "ease",
1789
+ selector = "[data-block-id]"
1790
+ } = options;
1791
+ const prevPositions = useRef(/* @__PURE__ */ new Map());
1792
+ useLayoutEffect(() => {
1793
+ const container = containerRef.current;
1794
+ if (!container) return;
1795
+ const children = container.querySelectorAll(selector);
1796
+ const currentPositions = /* @__PURE__ */ new Map();
1797
+ children.forEach((el) => {
1798
+ const id = el.dataset.blockId;
1799
+ if (id) {
1800
+ currentPositions.set(id, el.getBoundingClientRect());
1801
+ }
1802
+ });
1803
+ children.forEach((el) => {
1804
+ const htmlEl = el;
1805
+ const id = htmlEl.dataset.blockId;
1806
+ if (!id) return;
1807
+ const prev = prevPositions.current.get(id);
1808
+ const curr = currentPositions.get(id);
1809
+ if (!prev || !curr) return;
1810
+ const deltaY = prev.top - curr.top;
1811
+ const deltaX = prev.left - curr.left;
1812
+ if (deltaY === 0 && deltaX === 0) return;
1813
+ htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
1814
+ htmlEl.style.transition = "none";
1815
+ requestAnimationFrame(() => {
1816
+ htmlEl.style.transition = `transform ${duration}ms ${easing}`;
1817
+ htmlEl.style.transform = "";
1818
+ const onEnd = () => {
1819
+ htmlEl.style.transition = "";
1820
+ htmlEl.removeEventListener("transitionend", onEnd);
1821
+ };
1822
+ htmlEl.addEventListener("transitionend", onEnd);
1823
+ });
1824
+ });
1825
+ prevPositions.current = currentPositions;
1826
+ });
1827
+ }
1828
+ function useVirtualTree({
1829
+ containerRef,
1830
+ itemCount,
1831
+ itemHeight,
1832
+ overscan = 5
1833
+ }) {
1834
+ const [scrollTop, setScrollTop] = useState(0);
1835
+ const [containerHeight, setContainerHeight] = useState(0);
1836
+ const rafId = useRef(0);
1837
+ const handleScroll = useCallback(() => {
1838
+ cancelAnimationFrame(rafId.current);
1839
+ rafId.current = requestAnimationFrame(() => {
1840
+ const el = containerRef.current;
1841
+ if (el) {
1842
+ setScrollTop(el.scrollTop);
1843
+ setContainerHeight(el.clientHeight);
1844
+ }
1845
+ });
1846
+ }, [containerRef]);
1847
+ useEffect(() => {
1848
+ const el = containerRef.current;
1849
+ if (!el) return;
1850
+ setScrollTop(el.scrollTop);
1851
+ setContainerHeight(el.clientHeight);
1852
+ el.addEventListener("scroll", handleScroll, { passive: true });
1853
+ return () => {
1854
+ el.removeEventListener("scroll", handleScroll);
1855
+ cancelAnimationFrame(rafId.current);
1856
+ };
1857
+ }, [containerRef, handleScroll]);
1858
+ const totalHeight = itemCount * itemHeight;
1859
+ const startRaw = Math.floor(scrollTop / itemHeight);
1860
+ const visibleCount = Math.ceil(containerHeight / itemHeight);
1861
+ const start = Math.max(0, startRaw - overscan);
1862
+ const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
1863
+ const offsetY = start * itemHeight;
1864
+ return {
1865
+ visibleRange: { start, end },
1866
+ totalHeight,
1867
+ offsetY
1868
+ };
1869
+ }
1870
+
1871
+ // src/utils/serialization.ts
1872
+ function flatToNested(blocks) {
1873
+ const byParent = /* @__PURE__ */ new Map();
1874
+ for (const block of blocks) {
1875
+ const key = block.parentId ?? null;
1876
+ const list = byParent.get(key);
1877
+ if (list) {
1878
+ list.push(block);
1879
+ } else {
1880
+ byParent.set(key, [block]);
1881
+ }
1882
+ }
1883
+ for (const list of byParent.values()) {
1884
+ list.sort((a, b) => {
1885
+ if (typeof a.order === "string" && typeof b.order === "string") {
1886
+ return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
1887
+ }
1888
+ return Number(a.order) - Number(b.order);
1889
+ });
1890
+ }
1891
+ function buildChildren(parentId) {
1892
+ const siblings = byParent.get(parentId) ?? [];
1893
+ return siblings.map((block) => {
1894
+ const { parentId: _p, order: _o, ...rest } = block;
1895
+ return {
1896
+ ...rest,
1897
+ children: buildChildren(block.id)
1898
+ };
1899
+ });
1900
+ }
1901
+ return buildChildren(null);
1902
+ }
1903
+ function nestedToFlat(nested) {
1904
+ const result = [];
1905
+ function walk(nodes, parentId) {
1906
+ for (let i = 0; i < nodes.length; i++) {
1907
+ const { children, ...rest } = nodes[i];
1908
+ result.push({
1909
+ ...rest,
1910
+ parentId,
1911
+ order: i
1912
+ });
1913
+ walk(children, rest.id);
1914
+ }
1915
+ }
1916
+ walk(nested, null);
1917
+ return result;
1918
+ }
1662
1919
 
1663
- export { BlockTree, DragOverlay, DropZone, TreeRenderer, 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 };
1920
+ export { BlockTree, BlockTreeSSR, DragOverlay, DropZone, TreeRenderer, 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 };
1664
1921
  //# sourceMappingURL=index.mjs.map
1665
1922
  //# sourceMappingURL=index.mjs.map