@particle-academy/react-fancy 2.8.0 → 2.9.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.cts CHANGED
@@ -2231,7 +2231,12 @@ interface CanvasContextValue {
2231
2231
  nodeRects: Map<string, NodeRect>;
2232
2232
  registryVersion: number;
2233
2233
  containerRef: React.RefObject<HTMLDivElement | null>;
2234
+ /** Grid spacing in canvas-space pixels (unaffected by zoom). */
2235
+ gridSize: number;
2236
+ /** When true, dragged nodes snap their top-left corner to the grid. */
2237
+ snapToGrid: boolean;
2234
2238
  }
2239
+ type GridStyle = "dots" | "lines" | "none";
2235
2240
  interface CanvasProps {
2236
2241
  children: ReactNode;
2237
2242
  viewport?: ViewportState;
@@ -2241,8 +2246,17 @@ interface CanvasProps {
2241
2246
  maxZoom?: number;
2242
2247
  pannable?: boolean;
2243
2248
  zoomable?: boolean;
2249
+ /** Grid spacing in canvas-space pixels. Defaults to 20. */
2244
2250
  gridSize?: number;
2251
+ /** Show/hide the canvas grid. Defaults to false. */
2245
2252
  showGrid?: boolean;
2253
+ /** Grid pattern when shown — dots (default), lines, or none. Setting this
2254
+ * to "none" hides the grid even when `showGrid` is true. */
2255
+ gridStyle?: GridStyle;
2256
+ /** Grid color (any CSS color). Defaults to a faint zinc. */
2257
+ gridColor?: string;
2258
+ /** Snap dragged nodes to the grid. Defaults to false. */
2259
+ snapToGrid?: boolean;
2246
2260
  /** Automatically fit all nodes into view on initial mount */
2247
2261
  fitOnMount?: boolean;
2248
2262
  className?: string;
@@ -2308,7 +2322,7 @@ declare namespace CanvasControls {
2308
2322
  var displayName: string;
2309
2323
  }
2310
2324
 
2311
- declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2325
+ declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, gridStyle, gridSize, gridColor, snapToGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2312
2326
  declare const Canvas: typeof CanvasRoot & {
2313
2327
  Node: typeof CanvasNode;
2314
2328
  Edge: typeof CanvasEdge;
@@ -2474,6 +2488,15 @@ interface TreeNavProps {
2474
2488
  draggable?: boolean;
2475
2489
  /** Callback when a node is moved via drag and drop */
2476
2490
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2491
+ /** Accept drops from outside the tree — OS files, items from other
2492
+ * components, etc. When true, drag-over is allowed for any data source
2493
+ * and `onExternalDrop` fires on drop. (default: false) */
2494
+ acceptExternalDrops?: boolean;
2495
+ /** Called when an external drag (file or cross-component) is dropped on
2496
+ * a node. The raw event lets you read `event.dataTransfer.files` for OS
2497
+ * file drops or `event.dataTransfer.getData(type)` for custom MIME
2498
+ * payloads from other components. */
2499
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2477
2500
  /** Controlled expanded node IDs */
2478
2501
  expandedIds?: string[];
2479
2502
  /** Default expanded node IDs (uncontrolled) */
@@ -2501,6 +2524,8 @@ interface TreeNavContextValue {
2501
2524
  dragState: DragState;
2502
2525
  setDragState: (state: DragState) => void;
2503
2526
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2527
+ acceptExternalDrops: boolean;
2528
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2504
2529
  nodes: TreeNodeData[];
2505
2530
  expandNode: (id: string) => void;
2506
2531
  }
@@ -2515,7 +2540,7 @@ declare namespace TreeNode {
2515
2540
  var displayName: string;
2516
2541
  }
2517
2542
 
2518
- declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2543
+ declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, acceptExternalDrops, onExternalDrop, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2519
2544
  declare namespace TreeNavRoot {
2520
2545
  var displayName: string;
2521
2546
  }
package/dist/index.d.ts CHANGED
@@ -2231,7 +2231,12 @@ interface CanvasContextValue {
2231
2231
  nodeRects: Map<string, NodeRect>;
2232
2232
  registryVersion: number;
2233
2233
  containerRef: React.RefObject<HTMLDivElement | null>;
2234
+ /** Grid spacing in canvas-space pixels (unaffected by zoom). */
2235
+ gridSize: number;
2236
+ /** When true, dragged nodes snap their top-left corner to the grid. */
2237
+ snapToGrid: boolean;
2234
2238
  }
2239
+ type GridStyle = "dots" | "lines" | "none";
2235
2240
  interface CanvasProps {
2236
2241
  children: ReactNode;
2237
2242
  viewport?: ViewportState;
@@ -2241,8 +2246,17 @@ interface CanvasProps {
2241
2246
  maxZoom?: number;
2242
2247
  pannable?: boolean;
2243
2248
  zoomable?: boolean;
2249
+ /** Grid spacing in canvas-space pixels. Defaults to 20. */
2244
2250
  gridSize?: number;
2251
+ /** Show/hide the canvas grid. Defaults to false. */
2245
2252
  showGrid?: boolean;
2253
+ /** Grid pattern when shown — dots (default), lines, or none. Setting this
2254
+ * to "none" hides the grid even when `showGrid` is true. */
2255
+ gridStyle?: GridStyle;
2256
+ /** Grid color (any CSS color). Defaults to a faint zinc. */
2257
+ gridColor?: string;
2258
+ /** Snap dragged nodes to the grid. Defaults to false. */
2259
+ snapToGrid?: boolean;
2246
2260
  /** Automatically fit all nodes into view on initial mount */
2247
2261
  fitOnMount?: boolean;
2248
2262
  className?: string;
@@ -2308,7 +2322,7 @@ declare namespace CanvasControls {
2308
2322
  var displayName: string;
2309
2323
  }
2310
2324
 
2311
- declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2325
+ declare function CanvasRoot({ children, viewport: controlledViewport, defaultViewport, onViewportChange, minZoom, maxZoom, pannable, zoomable, showGrid, gridStyle, gridSize, gridColor, snapToGrid, fitOnMount, className, style, }: CanvasProps): react_jsx_runtime.JSX.Element;
2312
2326
  declare const Canvas: typeof CanvasRoot & {
2313
2327
  Node: typeof CanvasNode;
2314
2328
  Edge: typeof CanvasEdge;
@@ -2474,6 +2488,15 @@ interface TreeNavProps {
2474
2488
  draggable?: boolean;
2475
2489
  /** Callback when a node is moved via drag and drop */
2476
2490
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2491
+ /** Accept drops from outside the tree — OS files, items from other
2492
+ * components, etc. When true, drag-over is allowed for any data source
2493
+ * and `onExternalDrop` fires on drop. (default: false) */
2494
+ acceptExternalDrops?: boolean;
2495
+ /** Called when an external drag (file or cross-component) is dropped on
2496
+ * a node. The raw event lets you read `event.dataTransfer.files` for OS
2497
+ * file drops or `event.dataTransfer.getData(type)` for custom MIME
2498
+ * payloads from other components. */
2499
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2477
2500
  /** Controlled expanded node IDs */
2478
2501
  expandedIds?: string[];
2479
2502
  /** Default expanded node IDs (uncontrolled) */
@@ -2501,6 +2524,8 @@ interface TreeNavContextValue {
2501
2524
  dragState: DragState;
2502
2525
  setDragState: (state: DragState) => void;
2503
2526
  onNodeMove?: (sourceId: string, targetId: string, position: DropPosition) => void;
2527
+ acceptExternalDrops: boolean;
2528
+ onExternalDrop?: (event: React.DragEvent, target: TreeNodeData, position: DropPosition) => void;
2504
2529
  nodes: TreeNodeData[];
2505
2530
  expandNode: (id: string) => void;
2506
2531
  }
@@ -2515,7 +2540,7 @@ declare namespace TreeNode {
2515
2540
  var displayName: string;
2516
2541
  }
2517
2542
 
2518
- declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2543
+ declare function TreeNavRoot({ nodes, selectedId, onSelect, onNodeContextMenu, draggable, onNodeMove, acceptExternalDrops, onExternalDrop, expandedIds: controlledExpanded, defaultExpandedIds, onExpandedChange, defaultExpandAll, indentSize, showIcons, className, }: TreeNavProps): react_jsx_runtime.JSX.Element;
2519
2544
  declare namespace TreeNavRoot {
2520
2545
  var displayName: string;
2521
2546
  }
package/dist/index.js CHANGED
@@ -11152,12 +11152,30 @@ function countCardChildren(children) {
11152
11152
  n += 1;
11153
11153
  return;
11154
11154
  }
11155
- if (child.type === Fragment$1) {
11156
- n += countCardChildren(child.props.children);
11157
- }
11155
+ const inner = child.props.children;
11156
+ if (inner !== void 0) n += countCardChildren(inner);
11158
11157
  });
11159
11158
  return n;
11160
11159
  }
11160
+ function findCardIndex(children, cardId) {
11161
+ let idx = -1;
11162
+ let i = 0;
11163
+ function walk2(nodes) {
11164
+ Children.forEach(nodes, (child) => {
11165
+ if (idx !== -1) return;
11166
+ if (!isValidElement(child)) return;
11167
+ if (child.type === KanbanCard) {
11168
+ if (child.props.id === cardId) idx = i;
11169
+ i += 1;
11170
+ return;
11171
+ }
11172
+ const inner = child.props.children;
11173
+ if (inner !== void 0) walk2(inner);
11174
+ });
11175
+ }
11176
+ walk2(children);
11177
+ return idx;
11178
+ }
11161
11179
  function KanbanColumn({
11162
11180
  children,
11163
11181
  id,
@@ -11170,26 +11188,46 @@ function KanbanColumn({
11170
11188
  const { onCardMove, draggedCard, dragSource, registerColumn } = useKanban();
11171
11189
  const [dragOver, setDragOver] = useState(false);
11172
11190
  const [dropIndex, setDropIndex] = useState(null);
11191
+ const [dropY, setDropY] = useState(null);
11173
11192
  const cardsRef = useRef(null);
11174
11193
  useEffect(() => registerColumn(id), [id, registerColumn]);
11175
- const updateDropIndex = useCallback((clientY) => {
11194
+ const updateDrop = useCallback((clientY) => {
11176
11195
  const container = cardsRef.current;
11177
11196
  if (!container) {
11178
11197
  setDropIndex(null);
11198
+ setDropY(null);
11179
11199
  return;
11180
11200
  }
11181
- const cards = container.querySelectorAll(
11182
- ":scope > [data-react-fancy-kanban-card]"
11201
+ const all = Array.from(
11202
+ container.querySelectorAll(
11203
+ "[data-react-fancy-kanban-card]"
11204
+ )
11205
+ );
11206
+ const cards = all.filter(
11207
+ (el) => el.closest("[data-react-fancy-kanban-column]") === cardsRef.current?.closest("[data-react-fancy-kanban-column]")
11183
11208
  );
11209
+ const containerRect = container.getBoundingClientRect();
11210
+ if (cards.length === 0) {
11211
+ setDropIndex(0);
11212
+ setDropY(0);
11213
+ return;
11214
+ }
11184
11215
  let idx = cards.length;
11216
+ let yRel = 0;
11185
11217
  for (let i = 0; i < cards.length; i++) {
11186
11218
  const rect = cards[i].getBoundingClientRect();
11187
11219
  if (clientY < rect.top + rect.height / 2) {
11188
11220
  idx = i;
11221
+ yRel = rect.top - containerRect.top;
11189
11222
  break;
11190
11223
  }
11191
11224
  }
11225
+ if (idx === cards.length) {
11226
+ const last = cards[cards.length - 1].getBoundingClientRect();
11227
+ yRel = last.bottom - containerRect.top;
11228
+ }
11192
11229
  setDropIndex(idx);
11230
+ setDropY(yRel);
11193
11231
  }, []);
11194
11232
  const handleDragOver = useCallback(
11195
11233
  (e) => {
@@ -11197,14 +11235,15 @@ function KanbanColumn({
11197
11235
  e.preventDefault();
11198
11236
  e.stopPropagation();
11199
11237
  setDragOver(true);
11200
- updateDropIndex(e.clientY);
11238
+ updateDrop(e.clientY);
11201
11239
  },
11202
- [draggedCard, updateDropIndex]
11240
+ [draggedCard, updateDrop]
11203
11241
  );
11204
11242
  const handleDragLeave = useCallback((e) => {
11205
11243
  if (e.currentTarget.contains(e.relatedTarget)) return;
11206
11244
  setDragOver(false);
11207
11245
  setDropIndex(null);
11246
+ setDropY(null);
11208
11247
  }, []);
11209
11248
  const handleDrop = useCallback(
11210
11249
  (e) => {
@@ -11212,16 +11251,15 @@ function KanbanColumn({
11212
11251
  e.preventDefault();
11213
11252
  e.stopPropagation();
11214
11253
  const target = dropIndex ?? 0;
11215
- if (dragSource && draggedCard) {
11254
+ if (dragSource) {
11216
11255
  let finalIdx = target;
11217
11256
  if (dragSource === id) {
11218
11257
  const srcIdx = findCardIndex(children, draggedCard);
11219
- if (srcIdx !== -1 && target > srcIdx) {
11220
- finalIdx = target - 1;
11221
- }
11258
+ if (srcIdx !== -1 && target > srcIdx) finalIdx = target - 1;
11222
11259
  if (srcIdx === finalIdx) {
11223
11260
  setDragOver(false);
11224
11261
  setDropIndex(null);
11262
+ setDropY(null);
11225
11263
  return;
11226
11264
  }
11227
11265
  }
@@ -11229,27 +11267,13 @@ function KanbanColumn({
11229
11267
  }
11230
11268
  setDragOver(false);
11231
11269
  setDropIndex(null);
11270
+ setDropY(null);
11232
11271
  },
11233
11272
  [draggedCard, dragSource, dropIndex, id, onCardMove, children]
11234
11273
  );
11235
11274
  const cardCount = countCardChildren(children);
11236
- if (hideWhenEmpty && cardCount === 0 && !draggedCard) {
11237
- return null;
11238
- }
11239
- let cardSeen = 0;
11240
- const showIndicator = draggedCard !== null && dropIndex !== null && dragOver;
11241
- const renderedChildren = Children.toArray(children).map((child, i) => {
11242
- const isCard = isValidElement(child) && child.type === KanbanCard;
11243
- const indicator = showIndicator && isCard && cardSeen === dropIndex ? /* @__PURE__ */ jsx(DropIndicator, {}, `drop-${i}`) : null;
11244
- if (isCard) cardSeen += 1;
11245
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
11246
- indicator,
11247
- child
11248
- ] }, i);
11249
- });
11250
- if (showIndicator && dropIndex === cardCount) {
11251
- renderedChildren.push(/* @__PURE__ */ jsx(DropIndicator, {}, "drop-end"));
11252
- }
11275
+ if (hideWhenEmpty && cardCount === 0 && !draggedCard) return null;
11276
+ const showIndicator = draggedCard !== null && dropIndex !== null && dropY !== null && dragOver;
11253
11277
  const overWip = wipLimit !== void 0 && cardCount > wipLimit;
11254
11278
  return /* @__PURE__ */ jsx(KanbanColumnContext.Provider, { value: id, children: /* @__PURE__ */ jsxs(
11255
11279
  "div",
@@ -11284,36 +11308,22 @@ function KanbanColumn({
11284
11308
  }
11285
11309
  )
11286
11310
  ] }),
11287
- /* @__PURE__ */ jsx("div", { ref: cardsRef, className: "flex flex-1 flex-col gap-2", children: renderedChildren })
11311
+ /* @__PURE__ */ jsxs("div", { ref: cardsRef, className: "relative flex flex-1 flex-col gap-2", children: [
11312
+ children,
11313
+ showIndicator && /* @__PURE__ */ jsx(
11314
+ "div",
11315
+ {
11316
+ "data-react-fancy-kanban-drop-indicator": "",
11317
+ style: { top: dropY },
11318
+ className: "pointer-events-none absolute left-0 right-0 h-0.5 -translate-y-1/2 rounded-full bg-blue-500/80 shadow-[0_0_0_3px_rgba(59,130,246,0.15)]"
11319
+ }
11320
+ )
11321
+ ] })
11288
11322
  ]
11289
11323
  }
11290
11324
  ) });
11291
11325
  }
11292
11326
  KanbanColumn.displayName = "KanbanColumn";
11293
- function DropIndicator() {
11294
- return /* @__PURE__ */ jsx(
11295
- "div",
11296
- {
11297
- "data-react-fancy-kanban-drop-indicator": "",
11298
- className: "h-0.5 -my-1 rounded-full bg-blue-500/80"
11299
- }
11300
- );
11301
- }
11302
- function findCardIndex(children, cardId) {
11303
- let idx = -1;
11304
- let i = 0;
11305
- Children.forEach(children, (child) => {
11306
- if (idx !== -1) return;
11307
- if (!isValidElement(child)) return;
11308
- if (child.type === KanbanCard) {
11309
- if (child.props.id === cardId) {
11310
- idx = i;
11311
- }
11312
- i += 1;
11313
- }
11314
- });
11315
- return idx;
11316
- }
11317
11327
  function KanbanColumnHandle({
11318
11328
  children,
11319
11329
  className
@@ -11451,7 +11461,7 @@ function useCanvas() {
11451
11461
  return ctx;
11452
11462
  }
11453
11463
  function CanvasNode({ children, id, x, y, draggable, onPositionChange, className, style }) {
11454
- const { registerNode, unregisterNode, viewport } = useCanvas();
11464
+ const { registerNode, unregisterNode, viewport, gridSize, snapToGrid } = useCanvas();
11455
11465
  const nodeRef = useRef(null);
11456
11466
  const isDragging = useRef(false);
11457
11467
  const dragStart = useRef({ mouseX: 0, mouseY: 0, nodeX: 0, nodeY: 0 });
@@ -11484,9 +11494,15 @@ function CanvasNode({ children, id, x, y, draggable, onPositionChange, className
11484
11494
  if (!isDragging.current) return;
11485
11495
  const dx = (e.clientX - dragStart.current.mouseX) / viewport.zoom;
11486
11496
  const dy = (e.clientY - dragStart.current.mouseY) / viewport.zoom;
11487
- onPositionChange?.(dragStart.current.nodeX + dx, dragStart.current.nodeY + dy);
11497
+ let nx = dragStart.current.nodeX + dx;
11498
+ let ny = dragStart.current.nodeY + dy;
11499
+ if (snapToGrid && gridSize > 0) {
11500
+ nx = Math.round(nx / gridSize) * gridSize;
11501
+ ny = Math.round(ny / gridSize) * gridSize;
11502
+ }
11503
+ onPositionChange?.(nx, ny);
11488
11504
  },
11489
- [viewport.zoom, onPositionChange]
11505
+ [viewport.zoom, onPositionChange, snapToGrid, gridSize]
11490
11506
  );
11491
11507
  const handlePointerUp = useCallback(() => {
11492
11508
  isDragging.current = false;
@@ -11734,6 +11750,10 @@ function CanvasRoot({
11734
11750
  pannable = true,
11735
11751
  zoomable = true,
11736
11752
  showGrid = false,
11753
+ gridStyle = "dots",
11754
+ gridSize = 20,
11755
+ gridColor = "rgb(161 161 170 / 0.3)",
11756
+ snapToGrid = false,
11737
11757
  fitOnMount = false,
11738
11758
  className,
11739
11759
  style
@@ -11751,8 +11771,8 @@ function CanvasRoot({
11751
11771
  containerRef
11752
11772
  });
11753
11773
  const ctx = useMemo(
11754
- () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef }),
11755
- [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion]
11774
+ () => ({ viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, containerRef, gridSize, snapToGrid }),
11775
+ [viewport, setViewport, registerNode, unregisterNode, nodeRects, registryVersion, gridSize, snapToGrid]
11756
11776
  );
11757
11777
  const hasFitted = useRef(false);
11758
11778
  useEffect(() => {
@@ -11808,9 +11828,13 @@ function CanvasRoot({
11808
11828
  {
11809
11829
  "data-canvas-bg": "",
11810
11830
  className: "absolute inset-0",
11811
- style: showGrid ? {
11812
- backgroundImage: `radial-gradient(circle, rgb(161 161 170 / 0.3) 1px, transparent 1px)`,
11813
- backgroundSize: `${20 * viewport.zoom}px ${20 * viewport.zoom}px`,
11831
+ style: showGrid && gridStyle !== "none" ? gridStyle === "lines" ? {
11832
+ backgroundImage: `linear-gradient(to right, ${gridColor} 1px, transparent 1px), linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`,
11833
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
11834
+ backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
11835
+ } : {
11836
+ backgroundImage: `radial-gradient(circle, ${gridColor} 1px, transparent 1px)`,
11837
+ backgroundSize: `${gridSize * viewport.zoom}px ${gridSize * viewport.zoom}px`,
11814
11838
  backgroundPosition: `${viewport.panX}px ${viewport.panY}px`
11815
11839
  } : void 0
11816
11840
  }
@@ -12527,6 +12551,8 @@ function TreeNode({ node, depth }) {
12527
12551
  dragState,
12528
12552
  setDragState,
12529
12553
  onNodeMove,
12554
+ acceptExternalDrops,
12555
+ onExternalDrop,
12530
12556
  nodes,
12531
12557
  expandNode
12532
12558
  } = useTreeNav();
@@ -12569,14 +12595,18 @@ function TreeNode({ node, depth }) {
12569
12595
  clearAutoExpand();
12570
12596
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12571
12597
  }, [clearAutoExpand, setDragState]);
12598
+ const isExternalDrag = !dragState.draggedNodeId;
12572
12599
  const handleDragOver = useCallback((e) => {
12573
- if (!dragState.draggedNodeId) return;
12574
- const sourceId = dragState.draggedNodeId;
12575
- if (sourceId === node.id) return;
12576
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12600
+ if (isExternalDrag) {
12601
+ if (!acceptExternalDrops) return;
12602
+ } else {
12603
+ const sourceId = dragState.draggedNodeId;
12604
+ if (sourceId === node.id) return;
12605
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12606
+ }
12577
12607
  e.preventDefault();
12578
12608
  e.stopPropagation();
12579
- e.dataTransfer.dropEffect = "move";
12609
+ e.dataTransfer.dropEffect = isExternalDrag ? "copy" : "move";
12580
12610
  const position = computeDropPosition(e, !!isFolder);
12581
12611
  if (isFolder && !isExpanded && position === "inside") {
12582
12612
  if (!autoExpandTimer.current) {
@@ -12589,9 +12619,9 @@ function TreeNode({ node, depth }) {
12589
12619
  clearAutoExpand();
12590
12620
  }
12591
12621
  if (dragState.dropTargetId !== node.id || dragState.dropPosition !== position) {
12592
- setDragState({ draggedNodeId: sourceId, dropTargetId: node.id, dropPosition: position });
12622
+ setDragState({ draggedNodeId: dragState.draggedNodeId, dropTargetId: node.id, dropPosition: position });
12593
12623
  }
12594
- }, [dragState, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12624
+ }, [dragState, isExternalDrag, acceptExternalDrops, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
12595
12625
  const handleDragLeave = useCallback((e) => {
12596
12626
  if (!e.currentTarget.contains(e.relatedTarget)) {
12597
12627
  clearAutoExpand();
@@ -12605,21 +12635,25 @@ function TreeNode({ node, depth }) {
12605
12635
  e.stopPropagation();
12606
12636
  clearAutoExpand();
12607
12637
  const sourceId = dragState.draggedNodeId;
12608
- const position = dragState.dropPosition;
12609
- if (!sourceId || !position) return;
12610
- if (sourceId === node.id) return;
12611
- if (isDescendantOf(nodes, sourceId, node.id)) return;
12612
- onNodeMove?.(sourceId, node.id, position);
12638
+ const position = dragState.dropPosition ?? computeDropPosition(e, !!isFolder);
12639
+ if (sourceId) {
12640
+ if (sourceId === node.id) return;
12641
+ if (isDescendantOf(nodes, sourceId, node.id)) return;
12642
+ onNodeMove?.(sourceId, node.id, position);
12643
+ } else if (acceptExternalDrops) {
12644
+ onExternalDrop?.(e, node, position);
12645
+ }
12613
12646
  setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
12614
- }, [dragState, node.id, nodes, onNodeMove, setDragState, clearAutoExpand]);
12647
+ }, [dragState, node, isFolder, nodes, onNodeMove, acceptExternalDrops, onExternalDrop, setDragState, clearAutoExpand]);
12615
12648
  const canDrag = draggable && !node.disabled;
12649
+ const dropEnabled = draggable || acceptExternalDrops;
12616
12650
  return /* @__PURE__ */ jsxs(
12617
12651
  "div",
12618
12652
  {
12619
12653
  "data-react-fancy-tree-node": "",
12620
- onDragOver: draggable ? handleDragOver : void 0,
12621
- onDragLeave: draggable ? handleDragLeave : void 0,
12622
- onDrop: draggable ? handleDrop : void 0,
12654
+ onDragOver: dropEnabled ? handleDragOver : void 0,
12655
+ onDragLeave: dropEnabled ? handleDragLeave : void 0,
12656
+ onDrop: dropEnabled ? handleDrop : void 0,
12623
12657
  children: [
12624
12658
  isDropTarget && dropPosition === "before" && /* @__PURE__ */ jsx(
12625
12659
  "div",
@@ -12692,6 +12726,8 @@ function TreeNavRoot({
12692
12726
  onNodeContextMenu,
12693
12727
  draggable = false,
12694
12728
  onNodeMove,
12729
+ acceptExternalDrops = false,
12730
+ onExternalDrop,
12695
12731
  expandedIds: controlledExpanded,
12696
12732
  defaultExpandedIds,
12697
12733
  onExpandedChange,
@@ -12745,6 +12781,8 @@ function TreeNavRoot({
12745
12781
  dragState,
12746
12782
  setDragState,
12747
12783
  onNodeMove,
12784
+ acceptExternalDrops,
12785
+ onExternalDrop,
12748
12786
  nodes,
12749
12787
  expandNode
12750
12788
  }),
@@ -12759,16 +12797,19 @@ function TreeNavRoot({
12759
12797
  draggable,
12760
12798
  dragState,
12761
12799
  onNodeMove,
12800
+ acceptExternalDrops,
12801
+ onExternalDrop,
12762
12802
  nodes,
12763
12803
  expandNode
12764
12804
  ]
12765
12805
  );
12806
+ const dropEnabled = draggable || acceptExternalDrops;
12766
12807
  return /* @__PURE__ */ jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
12767
12808
  "nav",
12768
12809
  {
12769
12810
  "data-react-fancy-tree-nav": "",
12770
12811
  className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
12771
- onDragEnd: draggable ? handleDragEnd : void 0,
12812
+ onDragEnd: dropEnabled ? handleDragEnd : void 0,
12772
12813
  children: nodes.map((node) => /* @__PURE__ */ jsx(TreeNode, { node, depth: 0 }, node.id))
12773
12814
  }
12774
12815
  ) });