react-native-tree-multi-select 3.0.0-beta.1 → 3.0.0-beta.3

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.
Files changed (29) hide show
  1. package/README.md +1 -1
  2. package/lib/module/components/DragOverlay.js +3 -0
  3. package/lib/module/components/DragOverlay.js.map +1 -1
  4. package/lib/module/components/NodeList.js +61 -25
  5. package/lib/module/components/NodeList.js.map +1 -1
  6. package/lib/module/helpers/toggleCheckbox.helper.js +1 -1
  7. package/lib/module/hooks/useDragDrop.js +214 -30
  8. package/lib/module/hooks/useDragDrop.js.map +1 -1
  9. package/lib/module/store/treeView.store.js +6 -3
  10. package/lib/module/store/treeView.store.js.map +1 -1
  11. package/lib/typescript/src/components/DragOverlay.d.ts +1 -0
  12. package/lib/typescript/src/components/DragOverlay.d.ts.map +1 -1
  13. package/lib/typescript/src/components/NodeList.d.ts.map +1 -1
  14. package/lib/typescript/src/hooks/useDragDrop.d.ts +5 -0
  15. package/lib/typescript/src/hooks/useDragDrop.d.ts.map +1 -1
  16. package/lib/typescript/src/store/treeView.store.d.ts +2 -1
  17. package/lib/typescript/src/store/treeView.store.d.ts.map +1 -1
  18. package/lib/typescript/src/types/dragDrop.types.d.ts +9 -0
  19. package/lib/typescript/src/types/dragDrop.types.d.ts.map +1 -1
  20. package/lib/typescript/src/types/treeView.types.d.ts +94 -4
  21. package/lib/typescript/src/types/treeView.types.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/DragOverlay.tsx +3 -1
  24. package/src/components/NodeList.tsx +62 -29
  25. package/src/helpers/toggleCheckbox.helper.ts +1 -1
  26. package/src/hooks/useDragDrop.ts +237 -31
  27. package/src/store/treeView.store.ts +6 -2
  28. package/src/types/dragDrop.types.ts +9 -0
  29. package/src/types/treeView.types.ts +94 -7
@@ -16,7 +16,7 @@ interface UseDragDropParams<ID> {
16
16
  storeId: string;
17
17
  flattenedNodes: __FlattenedTreeNode__<ID>[];
18
18
  flashListRef: React.RefObject<FlashList<__FlattenedTreeNode__<ID>> | null>;
19
- containerRef: React.RefObject<{ measureInWindow: (cb: (x: number, y: number, w: number, h: number) => void) => void } | null>;
19
+ containerRef: React.RefObject<{ measureInWindow: (cb: (x: number, y: number, w: number, h: number) => void) => void; } | null>;
20
20
  dragEnabled: boolean;
21
21
  onDragEnd?: (event: DragEndEvent<ID>) => void;
22
22
  longPressDuration: number;
@@ -26,20 +26,25 @@ interface UseDragDropParams<ID> {
26
26
  measuredItemHeightRef: React.MutableRefObject<number>;
27
27
  dragOverlayOffset: number;
28
28
  autoExpandDelay: number;
29
+ /** Pixels per nesting level, used for magnetic overlay shift. */
30
+ indentationMultiplier: number;
29
31
  }
30
32
 
31
33
  interface UseDragDropReturn<ID> {
32
34
  panResponder: PanResponderInstance;
33
35
  overlayY: Animated.Value;
36
+ overlayX: Animated.Value;
34
37
  isDragging: boolean;
35
38
  draggedNode: __FlattenedTreeNode__<ID> | null;
36
39
  dropTarget: DropTarget<ID> | null;
40
+ effectiveDropLevel: number;
37
41
  handleNodeTouchStart: (
38
42
  nodeId: ID,
39
43
  pageY: number,
40
44
  locationY: number,
41
45
  nodeIndex: number,
42
46
  ) => void;
47
+ handleNodeTouchEnd: () => void;
43
48
  cancelLongPressTimer: () => void;
44
49
  scrollOffsetRef: React.MutableRefObject<number>;
45
50
  headerOffsetRef: React.MutableRefObject<number>;
@@ -62,6 +67,7 @@ export function useDragDrop<ID>(
62
67
  measuredItemHeightRef,
63
68
  dragOverlayOffset,
64
69
  autoExpandDelay,
70
+ indentationMultiplier,
65
71
  } = params;
66
72
 
67
73
  // --- Refs for mutable state (no stale closures in PanResponder) ---
@@ -72,6 +78,7 @@ export function useDragDrop<ID>(
72
78
 
73
79
  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
74
80
 
81
+ const containerPageXRef = useRef(0);
75
82
  const containerPageYRef = useRef(0);
76
83
  const containerHeightRef = useRef(0);
77
84
  const grabOffsetYRef = useRef(0);
@@ -80,6 +87,8 @@ export function useDragDrop<ID>(
80
87
  const itemHeightRef = useRef(36);
81
88
 
82
89
  const overlayY = useRef(new Animated.Value(0)).current;
90
+ const overlayX = useRef(new Animated.Value(0)).current;
91
+ const prevEffectiveLevelRef = useRef<number | null>(null);
83
92
 
84
93
  const autoScrollRAFRef = useRef<number | null>(null);
85
94
  const autoScrollSpeedRef = useRef(0);
@@ -94,8 +103,11 @@ export function useDragDrop<ID>(
94
103
  // Track which nodes were auto-expanded during this drag (to collapse on drag end)
95
104
  const autoExpandedDuringDragRef = useRef<Set<ID>>(new Set());
96
105
 
106
+ // Tracks whether the PanResponder has captured the current gesture
107
+ const panResponderActiveRef = useRef(false);
108
+
97
109
  // Previous drop target for hysteresis (prevents flicker between "below N" / "above N+1")
98
- const prevDropTargetRef = useRef<{ targetIndex: number; position: "above" | "below" | "inside" } | null>(null);
110
+ const prevDropTargetRef = useRef<{ targetIndex: number; position: "above" | "below" | "inside"; } | null>(null);
99
111
 
100
112
  // Keep flattenedNodes ref current for PanResponder closures
101
113
  const flattenedNodesRef = useRef(flattenedNodes);
@@ -109,8 +121,9 @@ export function useDragDrop<ID>(
109
121
  const [isDragging, setIsDragging] = useState(false);
110
122
  const [draggedNode, setDraggedNode] = useState<__FlattenedTreeNode__<ID> | null>(null);
111
123
  const [dropTarget, setDropTarget] = useState<DropTarget<ID> | null>(null);
124
+ const [effectiveDropLevel, setEffectiveDropLevel] = useState(0);
112
125
 
113
- // Ref mirror of dropTarget avoids nesting Zustand updates inside React state updaters
126
+ // Ref mirror of dropTarget - avoids nesting Zustand updates inside React state updaters
114
127
  const dropTargetRef = useRef<DropTarget<ID> | null>(null);
115
128
 
116
129
  // --- Long press timer ---
@@ -152,7 +165,8 @@ export function useDragDrop<ID>(
152
165
  const container = containerRef.current;
153
166
  if (!container) return;
154
167
 
155
- container.measureInWindow((_x, y, _w, h) => {
168
+ container.measureInWindow((x, y, _w, h) => {
169
+ containerPageXRef.current = x;
156
170
  containerPageYRef.current = y;
157
171
  containerHeightRef.current = h;
158
172
 
@@ -212,11 +226,16 @@ export function useDragDrop<ID>(
212
226
  const overlayLocalY = fingerLocalY - locationY + dragOverlayOffset * itemHeightRef.current;
213
227
  overlayY.setValue(overlayLocalY);
214
228
 
229
+ // Reset magnetic overlay
230
+ overlayX.setValue(0);
231
+ prevEffectiveLevelRef.current = node.level ?? 0;
232
+
215
233
  // Set React state
216
234
  isDraggingRef.current = true;
217
235
  autoExpandedDuringDragRef.current.clear();
218
236
  setIsDragging(true);
219
237
  setDraggedNode(node);
238
+ setEffectiveDropLevel(node.level ?? 0);
220
239
  setDropTarget(null);
221
240
 
222
241
  // Start auto-scroll loop
@@ -314,7 +333,7 @@ export function useDragDrop<ID>(
314
333
 
315
334
  // --- Calculate drop target ---
316
335
  const calculateDropTarget = useCallback(
317
- (fingerPageY: number) => {
336
+ (fingerPageY: number, fingerPageX: number) => {
318
337
  const nodes = flattenedNodesRef.current;
319
338
  if (nodes.length === 0) return;
320
339
 
@@ -338,17 +357,120 @@ export function useDragDrop<ID>(
338
357
  const positionInItem =
339
358
  (adjustedContentY - clampedIndex * iH) / iH;
340
359
  let position: "above" | "below" | "inside";
341
- if (positionInItem < 0.3) {
360
+ if (positionInItem < 0.15) {
342
361
  position = "above";
343
- } else if (positionInItem > 0.7) {
362
+ } else if (positionInItem > 0.85) {
344
363
  position = "below";
345
364
  } else {
346
365
  position = "inside";
347
366
  }
348
367
 
368
+ // --- Horizontal control at level cliffs ---
369
+ // At the boundary between nodes at different depths, the user's
370
+ // horizontal finger position decides the drop level:
371
+ // finger RIGHT of threshold → stay at deep level (inside parent)
372
+ // finger LEFT of threshold → switch to shallow level (outside parent)
373
+ // The threshold uses a generous buffer so dragging slightly left is enough.
374
+ const fingerLocalX = fingerPageX - containerPageXRef.current;
375
+ // logicalTargetId/logicalPosition: when the visual indicator node differs
376
+ // from the actual moveTreeNode target (e.g., ancestor at a shallower level).
377
+ let logicalTargetId: ID | null = null;
378
+ let logicalPosition: "above" | "below" | "inside" | null = null;
379
+ let visualDropLevel: number | null = null;
380
+
381
+ if (position === "below" || position === "inside") {
382
+ const currentLevel = targetNode.level ?? 0;
383
+ let isCliff = false;
384
+ let shallowLevel = 0;
385
+
386
+ if (clampedIndex < nodes.length - 1) {
387
+ const nextNode = nodes[clampedIndex + 1];
388
+ const nextLevel = nextNode?.level ?? 0;
389
+ if (nextNode && nextLevel < currentLevel) {
390
+ isCliff = true;
391
+ shallowLevel = nextLevel;
392
+ }
393
+ } else if (currentLevel > 0) {
394
+ // Last item in the list - treat as cliff to root level
395
+ isCliff = true;
396
+ shallowLevel = 0;
397
+ }
398
+
399
+ if (isCliff) {
400
+ // Generous threshold: midpoint of the two levels + 2× indent buffer
401
+ const threshold =
402
+ ((currentLevel + shallowLevel) / 2) * indentationMultiplier
403
+ + indentationMultiplier * 2;
404
+
405
+ if (fingerLocalX < threshold) {
406
+ // User wants the shallow level
407
+ if (clampedIndex < nodes.length - 1) {
408
+ // Non-last item: switch to "above" on the next (shallower) node
409
+ const nextNode = nodes[clampedIndex + 1]!;
410
+ clampedIndex = clampedIndex + 1;
411
+ targetNode = nextNode;
412
+ position = "above";
413
+ } else {
414
+ // Last item: find ancestor at shallow level, target it with "below"
415
+ const { childToParentMap } = getTreeViewStore<ID>(storeId).getState();
416
+ let ancestorId = targetNode.id;
417
+ let walkLevel = currentLevel;
418
+ while (walkLevel > shallowLevel) {
419
+ const parentId = childToParentMap.get(ancestorId);
420
+ if (parentId === undefined) break;
421
+ ancestorId = parentId;
422
+ walkLevel--;
423
+ }
424
+ // Visual stays on the last item; logical goes to ancestor
425
+ logicalTargetId = ancestorId;
426
+ logicalPosition = "below";
427
+ visualDropLevel = shallowLevel;
428
+ }
429
+ }
430
+ }
431
+ }
432
+ if (position === "above" && clampedIndex > 0) {
433
+ const prevNode = nodes[clampedIndex - 1];
434
+ const prevLevel = prevNode?.level ?? 0;
435
+ const currentLevel = targetNode.level ?? 0;
436
+ if (prevNode && prevLevel > currentLevel) {
437
+ // Level cliff above - same generous threshold
438
+ const threshold =
439
+ ((prevLevel + currentLevel) / 2) * indentationMultiplier
440
+ + indentationMultiplier * 2;
441
+
442
+ if (fingerLocalX >= threshold) {
443
+ clampedIndex = clampedIndex - 1;
444
+ targetNode = prevNode;
445
+ position = "below";
446
+ }
447
+ }
448
+ }
449
+
450
+ // --- Suppress "below" when it's redundant or confusing ---
451
+ // After horizontal control, any remaining "below" that isn't at a
452
+ // cliff is redundant with "above" on the next node → show "inside".
453
+ if (position === "below") {
454
+ const expandedSet = getTreeViewStore<ID>(storeId).getState().expanded;
455
+
456
+ // (a) Expanded parent: "below" visually sits at the parent/child junction
457
+ // but semantically inserts as a sibling after the entire subtree.
458
+ if (targetNode.children?.length && expandedSet.has(targetNode.id)) {
459
+ position = "inside";
460
+ }
461
+ // (b) No level cliff below: convert to "inside" so the highlight
462
+ // covers the full bottom of the node.
463
+ else if (clampedIndex < nodes.length - 1) {
464
+ const nextNode = nodes[clampedIndex + 1];
465
+ if (nextNode && (nextNode.level ?? 0) >= (targetNode.level ?? 0)) {
466
+ position = "inside";
467
+ }
468
+ }
469
+ }
470
+
349
471
  // --- Hysteresis: prevent flicker between "below N" and "above N+1" ---
350
- // These two positions represent the same visual gap between nodes.
351
- // Keep the previous one to avoid the indicator jumping between nodes.
472
+ // Only applies to same-level boundaries. Level cliffs are handled
473
+ // by horizontal control above, so they pass through without forced resolution.
352
474
  const prev = prevDropTargetRef.current;
353
475
  if (prev) {
354
476
  const sameGap =
@@ -357,8 +479,17 @@ export function useDragDrop<ID>(
357
479
  (prev.position === "above" && position === "below" &&
358
480
  clampedIndex === prev.targetIndex - 1);
359
481
  if (sameGap) {
360
- // Keep previous target — they're at the same visual gap
361
- return;
482
+ const upperIdx = Math.min(prev.targetIndex, clampedIndex);
483
+ const lowerIdx = Math.max(prev.targetIndex, clampedIndex);
484
+ const upperLevel = nodes[upperIdx]?.level ?? 0;
485
+ const lowerLevel = nodes[lowerIdx]?.level ?? 0;
486
+
487
+ if (upperLevel === lowerLevel) {
488
+ // Same level - pure visual hysteresis, keep previous
489
+ return;
490
+ }
491
+ // Level cliff - horizontal control already resolved this,
492
+ // let the result pass through.
362
493
  }
363
494
  }
364
495
  prevDropTargetRef.current = { targetIndex: clampedIndex, position };
@@ -376,7 +507,7 @@ export function useDragDrop<ID>(
376
507
  // --- Auto-expand: if hovering "inside" a collapsed expandable node ---
377
508
  if (isValid && position === "inside" && targetNode.children?.length && !expanded.has(targetNode.id)) {
378
509
  if (autoExpandTargetRef.current !== targetNode.id) {
379
- // New hover target start timer
510
+ // New hover target - start timer
380
511
  cancelAutoExpandTimer();
381
512
  autoExpandTargetRef.current = targetNode.id;
382
513
  autoExpandTimerRef.current = setTimeout(() => {
@@ -387,12 +518,46 @@ export function useDragDrop<ID>(
387
518
  }, autoExpandDelay);
388
519
  }
389
520
  } else {
390
- // Not hovering inside a collapsed expandable node cancel timer
521
+ // Not hovering inside a collapsed expandable node - cancel timer
391
522
  if (autoExpandTargetRef.current !== null) {
392
523
  cancelAutoExpandTimer();
393
524
  }
394
525
  }
395
526
 
527
+ // --- Magnetic overlay: update the effective level so the overlay
528
+ // renders its content at the correct indentation natively.
529
+ // A brief translateX spring provides a smooth transition. ---
530
+ const draggedLevel = draggedNodeRef.current?.level ?? 0;
531
+ // When a logical target overrides the visual (e.g. ancestor at last-item cliff),
532
+ // the effective level comes from the visual drop level, not the target node.
533
+ const effectiveLevel = isValid
534
+ ? (visualDropLevel !== null
535
+ ? visualDropLevel // "below" ancestor → sibling at that level
536
+ : position === "inside"
537
+ ? (targetNode.level ?? 0) + 1
538
+ : (targetNode.level ?? 0))
539
+ : draggedLevel;
540
+ if (effectiveLevel !== prevEffectiveLevelRef.current) {
541
+ const prevLevel = prevEffectiveLevelRef.current ?? effectiveLevel;
542
+ prevEffectiveLevelRef.current = effectiveLevel;
543
+ setEffectiveDropLevel(effectiveLevel);
544
+
545
+ // The level prop change snaps the content to the correct indent.
546
+ // Counteract that visual jump with an initial translateX offset,
547
+ // then spring to 0 for a smooth "magnetic snap" transition.
548
+ if (prevLevel !== effectiveLevel) {
549
+ overlayX.setValue(
550
+ (prevLevel - effectiveLevel) * indentationMultiplier
551
+ );
552
+ Animated.spring(overlayX, {
553
+ toValue: 0,
554
+ useNativeDriver: true,
555
+ speed: 40,
556
+ bounciness: 4,
557
+ }).start();
558
+ }
559
+ }
560
+
396
561
  const newTarget: DropTarget<ID> = {
397
562
  targetNodeId: targetNode.id,
398
563
  targetIndex: clampedIndex,
@@ -404,13 +569,23 @@ export function useDragDrop<ID>(
404
569
 
405
570
  // Update the store so each Node can render its own indicator
406
571
  if (isValid) {
407
- store.getState().updateDropTarget(targetNode.id, position);
572
+ store.getState().updateDropTarget(targetNode.id, position, visualDropLevel);
408
573
  } else {
409
574
  store.getState().updateDropTarget(null, null);
410
575
  }
411
576
 
412
577
  // Keep ref in sync (used by handleDragEnd to avoid setState-during-render)
413
- dropTargetRef.current = newTarget;
578
+ // When a logical target exists (e.g. ancestor at a cliff), use it
579
+ // for the actual move while the visual indicator stays on the current node.
580
+ if (logicalTargetId !== null && logicalPosition !== null) {
581
+ dropTargetRef.current = {
582
+ ...newTarget,
583
+ targetNodeId: logicalTargetId,
584
+ position: logicalPosition,
585
+ };
586
+ } else {
587
+ dropTargetRef.current = newTarget;
588
+ }
414
589
 
415
590
  setDropTarget((prevTarget) => {
416
591
  if (
@@ -424,12 +599,12 @@ export function useDragDrop<ID>(
424
599
  return newTarget;
425
600
  });
426
601
  },
427
- [storeId, autoExpandDelay, cancelAutoExpandTimer]
602
+ [storeId, autoExpandDelay, cancelAutoExpandTimer, indentationMultiplier, overlayX]
428
603
  );
429
604
 
430
605
  // --- Handle drag end ---
431
606
  const handleDragEnd = useCallback(
432
- (fingerPageY?: number) => {
607
+ (fingerPageY?: number, fingerPageX?: number) => {
433
608
  stopAutoScroll();
434
609
  cancelLongPressTimer();
435
610
  cancelAutoExpandTimer();
@@ -438,9 +613,9 @@ export function useDragDrop<ID>(
438
613
  if (!isDraggingRef.current) return;
439
614
  isDraggingRef.current = false;
440
615
 
441
- // Recalculate drop target at final position if we have a pageY
616
+ // Recalculate drop target at final position if we have coords
442
617
  if (fingerPageY !== undefined) {
443
- calculateDropTarget(fingerPageY);
618
+ calculateDropTarget(fingerPageY, fingerPageX ?? 0);
444
619
  }
445
620
 
446
621
  // Cancel any auto-expand timer that calculateDropTarget may have just started.
@@ -498,17 +673,31 @@ export function useDragDrop<ID>(
498
673
  newTreeData: newData,
499
674
  });
500
675
 
501
- // Scroll to the dropped node after React processes the expansion
676
+ // Scroll to the dropped node after React processes the expansion,
677
+ // but only if it's outside the visible viewport. An animated
678
+ // scroll would consume the user's next touch (RN stops the
679
+ // animation on tap), so we skip when the node is already visible.
502
680
  setTimeout(() => {
503
681
  const nodes = flattenedNodesRef.current;
504
682
  const idx = nodes.findIndex(n => n.id === droppedNodeId);
505
- if (idx >= 0) {
506
- flashListRef.current?.scrollToIndex?.({
507
- index: idx,
508
- animated: true,
509
- viewPosition: 0.5,
510
- });
683
+ if (idx < 0) return;
684
+
685
+ const itemH = itemHeightRef.current;
686
+ const scrollTop = scrollOffsetRef.current;
687
+ const containerH = containerHeightRef.current;
688
+ const estimatedTop = idx * itemH;
689
+ const estimatedBottom = estimatedTop + itemH;
690
+
691
+ // Already in view → no scroll needed
692
+ if (estimatedTop >= scrollTop && estimatedBottom <= scrollTop + containerH) {
693
+ return;
511
694
  }
695
+
696
+ flashListRef.current?.scrollToIndex?.({
697
+ index: idx,
698
+ animated: true,
699
+ viewPosition: 0.5,
700
+ });
512
701
  }, 100);
513
702
  }
514
703
 
@@ -547,6 +736,8 @@ export function useDragDrop<ID>(
547
736
  store2.getState().updateDropTarget(null, null);
548
737
 
549
738
  // Reset all refs
739
+ overlayX.setValue(0);
740
+ prevEffectiveLevelRef.current = null;
550
741
  dropTargetRef.current = null;
551
742
  draggedNodeRef.current = null;
552
743
  draggedNodeIdRef.current = null;
@@ -567,6 +758,16 @@ export function useDragDrop<ID>(
567
758
  ]
568
759
  );
569
760
 
761
+ // --- Handle node touch end ---
762
+ // If the PanResponder never captured the gesture (no movement after long
763
+ // press fired), end the drag here so the node doesn't stay "lifted".
764
+ const handleNodeTouchEnd = useCallback(() => {
765
+ cancelLongPressTimer();
766
+ if (isDraggingRef.current && !panResponderActiveRef.current) {
767
+ handleDragEnd();
768
+ }
769
+ }, [cancelLongPressTimer, handleDragEnd]);
770
+
570
771
  // --- PanResponder ---
571
772
  const panResponder = useRef(
572
773
  PanResponder.create({
@@ -578,7 +779,7 @@ export function useDragDrop<ID>(
578
779
  isDraggingRef.current,
579
780
 
580
781
  onPanResponderGrant: () => {
581
- // Touch captured successfully
782
+ panResponderActiveRef.current = true;
582
783
  },
583
784
 
584
785
  onPanResponderMove: (evt) => {
@@ -593,10 +794,10 @@ export function useDragDrop<ID>(
593
794
  fingerLocalY - grabOffsetYRef.current + dragOverlayOffset * itemHeightRef.current;
594
795
  overlayY.setValue(overlayLocalY);
595
796
 
596
- // Calculate drop target
597
- calculateDropTarget(fingerPageY);
797
+ // Calculate drop target (horizontal position used at level cliffs)
798
+ calculateDropTarget(fingerPageY, evt.nativeEvent.pageX);
598
799
 
599
- // Auto-scroll at edges use delta-based position relative to container
800
+ // Auto-scroll at edges - use delta-based position relative to container
600
801
  const fingerInContainer =
601
802
  initialFingerContainerYRef.current +
602
803
  (fingerPageY - initialFingerPageYRef.current);
@@ -604,10 +805,12 @@ export function useDragDrop<ID>(
604
805
  },
605
806
 
606
807
  onPanResponderRelease: (evt) => {
607
- handleDragEnd(evt.nativeEvent.pageY);
808
+ panResponderActiveRef.current = false;
809
+ handleDragEnd(evt.nativeEvent.pageY, evt.nativeEvent.pageX);
608
810
  },
609
811
 
610
812
  onPanResponderTerminate: () => {
813
+ panResponderActiveRef.current = false;
611
814
  handleDragEnd();
612
815
  },
613
816
  })
@@ -632,10 +835,13 @@ export function useDragDrop<ID>(
632
835
  return {
633
836
  panResponder,
634
837
  overlayY,
838
+ overlayX,
635
839
  isDragging,
636
840
  draggedNode,
637
841
  dropTarget,
842
+ effectiveDropLevel,
638
843
  handleNodeTouchStart,
844
+ handleNodeTouchEnd,
639
845
  cancelLongPressTimer,
640
846
  scrollOffsetRef,
641
847
  headerOffsetRef,
@@ -54,7 +54,8 @@ export type TreeViewState<ID> = {
54
54
  // Drop target state (used by nodes to render their own indicator)
55
55
  dropTargetNodeId: ID | null;
56
56
  dropPosition: DropPosition | null;
57
- updateDropTarget: (nodeId: ID | null, position: DropPosition | null) => void;
57
+ dropLevel: number | null;
58
+ updateDropTarget: (nodeId: ID | null, position: DropPosition | null, level?: number | null) => void;
58
59
 
59
60
  // Cleanup all states in this store
60
61
  cleanUpTreeViewStore: () => void;
@@ -120,9 +121,11 @@ export function getTreeViewStore<ID>(id: string): UseBoundStore<StoreApi<TreeVie
120
121
 
121
122
  dropTargetNodeId: null,
122
123
  dropPosition: null,
123
- updateDropTarget: (nodeId, position) => set({
124
+ dropLevel: null,
125
+ updateDropTarget: (nodeId, position, level) => set({
124
126
  dropTargetNodeId: nodeId,
125
127
  dropPosition: position,
128
+ dropLevel: level ?? null,
126
129
  }),
127
130
 
128
131
  cleanUpTreeViewStore: () =>
@@ -141,6 +144,7 @@ export function getTreeViewStore<ID>(id: string): UseBoundStore<StoreApi<TreeVie
141
144
  invalidDragTargetIds: new Set<ID>(),
142
145
  dropTargetNodeId: null,
143
146
  dropPosition: null,
147
+ dropLevel: null,
144
148
  }),
145
149
  }));
146
150
 
@@ -1,7 +1,9 @@
1
1
  import type { TreeNode } from "./treeView.types";
2
2
 
3
+ /** Where a node is dropped relative to the target: as a sibling above/below, or as a child inside */
3
4
  export type DropPosition = "above" | "below" | "inside";
4
5
 
6
+ /** Event payload passed to the onDragEnd callback after a successful drop */
5
7
  export interface DragEndEvent<ID = string> {
6
8
  /** The id of the node that was dragged */
7
9
  draggedNodeId: ID;
@@ -13,11 +15,18 @@ export interface DragEndEvent<ID = string> {
13
15
  newTreeData: TreeNode<ID>[];
14
16
  }
15
17
 
18
+ /** Internal representation of the current drop target during a drag operation */
16
19
  export interface DropTarget<ID = string> {
20
+ /** The id of the node being hovered over */
17
21
  targetNodeId: ID;
22
+ /** Index of the target node in the flattened list */
18
23
  targetIndex: number;
24
+ /** Where the drop would occur relative to the target */
19
25
  position: DropPosition;
26
+ /** Whether this is a valid drop location (e.g. not dropping a node onto itself or its descendants) */
20
27
  isValid: boolean;
28
+ /** Nesting level of the target node */
21
29
  targetLevel: number;
30
+ /** Y-coordinate for positioning the drop indicator */
22
31
  indicatorTop: number;
23
32
  }