@noya-app/noya-designsystem 0.1.55 → 0.1.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "@noya-app/noya-colorpicker": "0.1.21",
26
26
  "@noya-app/noya-utils": "0.1.5",
27
27
  "@noya-app/noya-geometry": "0.1.11",
28
- "@noya-app/noya-icons": "0.1.9",
28
+ "@noya-app/noya-icons": "0.1.10",
29
29
  "@noya-app/noya-keymap": "0.1.3",
30
30
  "@noya-app/noya-tailwind-config": "0.1.3",
31
31
  "@radix-ui/primitive": "^1.0.0",
@@ -2,46 +2,46 @@ import { describe, expect, it } from "bun:test";
2
2
  import {
3
3
  defaultAcceptsDrop,
4
4
  validateDropIndicator,
5
- } from "../components/Sortable";
5
+ } from "../components/sorting/sorting";
6
6
 
7
7
  describe("defaultAcceptsDrop", () => {
8
8
  it("should return false if the active item is the same as the over item", () => {
9
- const result = defaultAcceptsDrop(0, 0, "inside");
9
+ const result = defaultAcceptsDrop(0, 0, "inside", "", "");
10
10
  expect(result).toBe(false);
11
11
  });
12
12
 
13
13
  it("should return false for 'inside' position", () => {
14
- const result = defaultAcceptsDrop(0, 1, "inside");
14
+ const result = defaultAcceptsDrop(0, 1, "inside", "", "");
15
15
  expect(result).toBe(false);
16
16
  });
17
17
 
18
18
  it("should return false when dropping above the same item", () => {
19
- const result = defaultAcceptsDrop(0, 0, "above");
19
+ const result = defaultAcceptsDrop(0, 0, "above", "", "");
20
20
  expect(result).toBe(false);
21
21
  });
22
22
 
23
23
  it("should return false when dropping below the same item", () => {
24
- const result = defaultAcceptsDrop(0, 0, "below");
24
+ const result = defaultAcceptsDrop(0, 0, "below", "", "");
25
25
  expect(result).toBe(false);
26
26
  });
27
27
 
28
28
  it("should return false when dropping above the item below the source", () => {
29
- const result = defaultAcceptsDrop(0, 1, "above");
29
+ const result = defaultAcceptsDrop(0, 1, "above", "", "");
30
30
  expect(result).toBe(false);
31
31
  });
32
32
 
33
33
  it("should return false when dropping below the item above the source", () => {
34
- const result = defaultAcceptsDrop(1, 0, "below");
34
+ const result = defaultAcceptsDrop(1, 0, "below", "", "");
35
35
  expect(result).toBe(false);
36
36
  });
37
37
 
38
38
  it("should return true when dropping above a valid target", () => {
39
- const result = defaultAcceptsDrop(0, 2, "above");
39
+ const result = defaultAcceptsDrop(0, 2, "above", "", "");
40
40
  expect(result).toBe(true);
41
41
  });
42
42
 
43
43
  it("should return true when dropping below a valid target", () => {
44
- const result = defaultAcceptsDrop(2, 0, "below");
44
+ const result = defaultAcceptsDrop(2, 0, "below", "", "");
45
45
  expect(result).toBe(true);
46
46
  });
47
47
  });
@@ -97,6 +97,8 @@ describe("validateDropIndicator", () => {
97
97
  offsetStart: 50, // offsetStart in middle third
98
98
  elementStart: 0, // elementStart
99
99
  elementSize: 100, // elementSize
100
+ sourceListId: "",
101
+ targetListId: "",
100
102
  });
101
103
  expect(result).toBe("inside");
102
104
  });
@@ -110,6 +112,8 @@ describe("validateDropIndicator", () => {
110
112
  offsetStart: 20, // offsetStart in top third
111
113
  elementStart: 0, // elementStart
112
114
  elementSize: 100, // elementSize
115
+ sourceListId: "",
116
+ targetListId: "",
113
117
  });
114
118
  expect(result).toBe("above");
115
119
  });
@@ -123,6 +127,8 @@ describe("validateDropIndicator", () => {
123
127
  offsetStart: 80, // offsetStart in bottom third
124
128
  elementStart: 0, // elementStart
125
129
  elementSize: 100, // elementSize
130
+ sourceListId: "",
131
+ targetListId: "",
126
132
  });
127
133
  expect(result).toBe("inside");
128
134
  });
@@ -138,6 +144,8 @@ describe("validateDropIndicator", () => {
138
144
  offsetStart: 50, // offsetStart in middle third
139
145
  elementStart: 0, // elementStart
140
146
  elementSize: 100, // elementSize
147
+ sourceListId: "",
148
+ targetListId: "",
141
149
  });
142
150
  expect(result).toBe("inside");
143
151
  });
@@ -151,6 +159,8 @@ describe("validateDropIndicator", () => {
151
159
  offsetStart: 20, // offsetStart in top third
152
160
  elementStart: 0, // elementStart
153
161
  elementSize: 100, // elementSize
162
+ sourceListId: "",
163
+ targetListId: "",
154
164
  });
155
165
  expect(result).toBe("above");
156
166
  });
@@ -164,6 +174,8 @@ describe("validateDropIndicator", () => {
164
174
  offsetStart: 80, // offsetStart in bottom third
165
175
  elementStart: 0, // elementStart
166
176
  elementSize: 100, // elementSize
177
+ sourceListId: "",
178
+ targetListId: "",
167
179
  });
168
180
  expect(result).toBe("below");
169
181
  });
@@ -177,6 +189,8 @@ describe("validateDropIndicator", () => {
177
189
  offsetStart: 120, // offsetStart in middle third relative to elementStart
178
190
  elementStart: 100, // elementStart
179
191
  elementSize: 60, // elementSize
192
+ sourceListId: "",
193
+ targetListId: "",
180
194
  });
181
195
  expect(result).toBe("inside");
182
196
  });
@@ -191,6 +205,8 @@ describe("validateDropIndicator", () => {
191
205
  offsetStart: 50, // offsetStart in middle third
192
206
  elementStart: 0, // elementStart
193
207
  elementSize: 100, // elementSize
208
+ sourceListId: "",
209
+ targetListId: "",
194
210
  });
195
211
  expect(result).toBe("inside");
196
212
  });
@@ -204,6 +220,8 @@ describe("validateDropIndicator", () => {
204
220
  offsetStart: 20, // offsetStart in top half
205
221
  elementStart: 0, // elementStart
206
222
  elementSize: 100, // elementSize
223
+ sourceListId: "",
224
+ targetListId: "",
207
225
  });
208
226
  expect(result).toBe("above");
209
227
  });
@@ -217,6 +235,8 @@ describe("validateDropIndicator", () => {
217
235
  offsetStart: 70, // offsetStart in bottom half
218
236
  elementStart: 0, // elementStart
219
237
  elementSize: 100, // elementSize
238
+ sourceListId: "",
239
+ targetListId: "",
220
240
  });
221
241
  expect(result).toBe("below");
222
242
  });
@@ -230,6 +250,8 @@ describe("validateDropIndicator", () => {
230
250
  offsetStart: 20, // offsetStart in top half
231
251
  elementStart: 0, // elementStart
232
252
  elementSize: 100, // elementSize
253
+ sourceListId: "",
254
+ targetListId: "",
233
255
  });
234
256
  expect(result).toBe("inside");
235
257
  });
@@ -244,6 +266,8 @@ describe("validateDropIndicator", () => {
244
266
  offsetStart: 50,
245
267
  elementStart: 0,
246
268
  elementSize: 100,
269
+ sourceListId: "",
270
+ targetListId: "",
247
271
  });
248
272
  expect(result).toBeUndefined();
249
273
  });
@@ -257,6 +281,8 @@ describe("validateDropIndicator", () => {
257
281
  offsetStart: 50,
258
282
  elementStart: 0,
259
283
  elementSize: 100,
284
+ sourceListId: "",
285
+ targetListId: "",
260
286
  });
261
287
  expect(result).toBeUndefined();
262
288
  });
@@ -31,13 +31,14 @@ import { ContextMenu } from "./ContextMenu";
31
31
  import { InputField } from "./InputField";
32
32
  import { MenuItem } from "./internal/Menu";
33
33
  import { ScrollArea } from "./ScrollArea";
34
+ import { Sortable } from "./sorting/Sortable";
34
35
  import {
35
36
  DropValidator,
37
+ MoveDragItemHandler,
36
38
  normalizeListTargetIndex,
37
39
  RelativeDropPosition,
38
- Sortable,
39
- type SortableItemContextProps,
40
- } from "./Sortable";
40
+ SortableItemContextProps,
41
+ } from "./sorting/sorting";
41
42
  import { Spacer } from "./Spacer";
42
43
 
43
44
  export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
@@ -776,11 +777,7 @@ export type ListViewRootProps = {
776
777
  onPress?: () => void;
777
778
  scrollable?: boolean;
778
779
  expandable?: boolean;
779
- onMoveItem?: (
780
- sourceIndex: number,
781
- targetIndex: number,
782
- position: RelativeDropPosition
783
- ) => void;
780
+ onMoveItem?: MoveDragItemHandler;
784
781
  indentation?: number;
785
782
  acceptsDrop?: DropValidator;
786
783
  pressEventName?: PressEventName;
@@ -1,5 +1,5 @@
1
1
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
2
- import React from "react";
2
+ import React, { ComponentProps } from "react";
3
3
 
4
4
  import { memoGeneric } from "@noya-app/react-utils";
5
5
  import { BaseToolbar } from "./BaseToolbar";
@@ -60,16 +60,20 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
60
60
  >({
61
61
  item,
62
62
  onSelectMenuItem,
63
+ as,
63
64
  }: {
64
65
  item: SelectableMenuItem<T>;
65
66
  onSelectMenuItem?: (value: T) => void;
67
+ as?: ComponentProps<typeof Button>["as"];
66
68
  }) {
67
69
  const content = (
68
70
  <Button
71
+ as={as}
69
72
  disabled={item.disabled}
70
73
  active={item.checked}
71
74
  icon={item.icon}
72
75
  style={item.icon && !item.title ? iconButtonStyle : undefined}
76
+ {...(as === "a" && { href: item.value })}
73
77
  onClick={() => {
74
78
  if (onSelectMenuItem && item.value) {
75
79
  onSelectMenuItem(item.value);
@@ -94,9 +98,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
94
98
  >({
95
99
  item,
96
100
  onSelectMenuItem,
101
+ buttonAs,
97
102
  }: {
98
103
  item: MenuItem<T>;
99
104
  onSelectMenuItem?: (value: T) => void;
105
+ buttonAs?: ComponentProps<typeof Button>["as"];
100
106
  }) {
101
107
  if (item.type === "sectionHeader") return null;
102
108
 
@@ -106,7 +112,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
106
112
 
107
113
  if (isSelectableMenuItem(item)) {
108
114
  return (
109
- <ToolbarMenuButton item={item} onSelectMenuItem={onSelectMenuItem} />
115
+ <ToolbarMenuButton
116
+ item={item}
117
+ onSelectMenuItem={onSelectMenuItem}
118
+ as={buttonAs}
119
+ />
110
120
  );
111
121
  }
112
122
 
@@ -118,9 +128,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
118
128
  export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
119
129
  items,
120
130
  onSelectMenuItem,
131
+ buttonAs,
121
132
  }: {
122
133
  items: MenuItem<T>[];
123
134
  onSelectMenuItem?: (value: T) => void;
135
+ buttonAs?: ComponentProps<typeof Button>["as"];
124
136
  }) {
125
137
  return (
126
138
  <>
@@ -130,6 +142,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
130
142
  key={i}
131
143
  item={item}
132
144
  onSelectMenuItem={onSelectMenuItem}
145
+ buttonAs={buttonAs}
133
146
  />
134
147
  );
135
148
  })}
@@ -0,0 +1,110 @@
1
+ import {
2
+ AcceptsDrop,
3
+ MoveDragItemHandler,
4
+ RelativeDropPosition,
5
+ } from "./sorting";
6
+
7
+ import { Active, Over, UniqueIdentifier } from "@dnd-kit/core";
8
+ import { AcceptsFromList } from "./sorting";
9
+
10
+ import { Point } from "@noya-app/noya-geometry";
11
+ import React from "react";
12
+ import { DragItem } from "./sorting";
13
+
14
+ export type GetDropIndicatorParameters = {
15
+ absolutePosition: Point;
16
+ active: Active;
17
+ over: Over;
18
+ sourceListId: string;
19
+ targetListId: string;
20
+ };
21
+
22
+ export interface RegisteredList {
23
+ id: string;
24
+ keys: UniqueIdentifier[];
25
+ onMoveItem: MoveDragItemHandler;
26
+ renderOverlay?: (id: UniqueIdentifier) => React.ReactNode;
27
+ acceptsFromList: AcceptsFromList;
28
+ acceptsDrop: AcceptsDrop;
29
+ getDropIndicator: (
30
+ parameters: GetDropIndicatorParameters
31
+ ) => RelativeDropPosition | undefined;
32
+ }
33
+
34
+ export interface DragRegistrationContextType {
35
+ registerList: (list: RegisteredList) => void;
36
+ unregisterList: (listId: string) => void;
37
+ }
38
+
39
+ export const DragRegistrationContext =
40
+ React.createContext<DragRegistrationContextType | null>(null);
41
+
42
+ export function useDragRegistration(
43
+ context: React.Context<DragRegistrationContextType | null>
44
+ ): DragRegistrationContextType {
45
+ const value = React.useContext(context);
46
+
47
+ if (!value) {
48
+ throw new Error(
49
+ "useDragRegistration must be used within a DragRegistrationProvider"
50
+ );
51
+ }
52
+
53
+ return value;
54
+ }
55
+
56
+ export function useDragRegistrationManager() {
57
+ const [registeredLists, setRegisteredLists] = React.useState<
58
+ Map<string, RegisteredList>
59
+ >(new Map());
60
+
61
+ const registerList = React.useCallback((list: RegisteredList) => {
62
+ setRegisteredLists((prev) => new Map(prev).set(list.id, list));
63
+ }, []);
64
+
65
+ const unregisterList = React.useCallback((listId: string) => {
66
+ setRegisteredLists((prev) => {
67
+ const newMap = new Map(prev);
68
+ newMap.delete(listId);
69
+ return newMap;
70
+ });
71
+ }, []);
72
+
73
+ return { registerList, unregisterList, registeredLists };
74
+ }
75
+
76
+ export type ActiveDragContextType = {
77
+ activeItem: DragItem | null;
78
+ delta: Point;
79
+ };
80
+
81
+ export const ActiveDragContext =
82
+ React.createContext<ActiveDragContextType | null>(null);
83
+
84
+ export function useActiveDrag(
85
+ context: React.Context<ActiveDragContextType | null>
86
+ ): ActiveDragContextType {
87
+ const value = React.useContext(context);
88
+
89
+ if (!value) {
90
+ throw new Error("useActiveDrag must be used within a ActiveDragProvider");
91
+ }
92
+
93
+ return value;
94
+ }
95
+
96
+ /**
97
+ * We use this context to automatically create a drag context if not exists.
98
+ * We do this separately from the contexts that actually store data since for those
99
+ * we want to allow a custom context to be passed and not get overwritten by nested contexts.
100
+ */
101
+ export const AnyDragContext = React.createContext<boolean>(false);
102
+
103
+ export function useAnyDragContext() {
104
+ return React.useContext(AnyDragContext);
105
+ }
106
+
107
+ export type SharedDragProviderContexts = {
108
+ dragRegistrationContext: React.Context<DragRegistrationContextType | null>;
109
+ activeDragContext: React.Context<ActiveDragContextType | null>;
110
+ };
@@ -0,0 +1,307 @@
1
+ import {
2
+ closestCenter,
3
+ CollisionDetection,
4
+ DndContext,
5
+ DragEndEvent,
6
+ DragMoveEvent,
7
+ DragOverlay,
8
+ DragStartEvent,
9
+ PointerSensor,
10
+ pointerWithin,
11
+ useSensor,
12
+ useSensors,
13
+ } from "@dnd-kit/core";
14
+ import * as React from "react";
15
+ import { useMemo } from "react";
16
+ import { createPortal } from "react-dom";
17
+ import {
18
+ ActiveDragContext,
19
+ ActiveDragContextType,
20
+ AnyDragContext,
21
+ DragRegistrationContext,
22
+ DragRegistrationContextType,
23
+ SharedDragProviderContexts,
24
+ useDragRegistrationManager,
25
+ } from "./DragRegistration";
26
+ import { DragItem } from "./sorting";
27
+
28
+ type SharedDragProviderProps = {
29
+ children: React.ReactNode;
30
+ contexts?: SharedDragProviderContexts;
31
+ };
32
+
33
+ export function SharedDragProvider({
34
+ children,
35
+ contexts = {
36
+ dragRegistrationContext: DragRegistrationContext,
37
+ activeDragContext: ActiveDragContext,
38
+ },
39
+ }: SharedDragProviderProps) {
40
+ const sensors = useSensors(
41
+ useSensor(PointerSensor, {
42
+ activationConstraint: {
43
+ distance: 4,
44
+ },
45
+ })
46
+ );
47
+
48
+ const mounted = useMounted();
49
+ const [activeItem, setActiveItem] = React.useState<DragItem | null>(null);
50
+ const [delta, setDelta] = React.useState({ x: 0, y: 0 });
51
+ const { registerList, unregisterList, registeredLists } =
52
+ useDragRegistrationManager();
53
+
54
+ const activatorEventRef = React.useRef<PointerEvent | null>(null);
55
+
56
+ const acceptsListDrop = React.useCallback(
57
+ (sourceListId: string, targetListId: string) => {
58
+ const sourceList = registeredLists.get(sourceListId);
59
+ const targetList = registeredLists.get(targetListId);
60
+
61
+ if (!sourceList || !targetList) return false;
62
+
63
+ return targetList.acceptsFromList({
64
+ sourceListId,
65
+ targetListId,
66
+ });
67
+ },
68
+ [registeredLists]
69
+ );
70
+
71
+ const handleDragStart = React.useCallback(
72
+ (event: DragStartEvent) => {
73
+ // Capture the activator event for position calculations
74
+ activatorEventRef.current = event.activatorEvent as PointerEvent;
75
+
76
+ // Find which list contains this item
77
+ for (const [listId, list] of registeredLists) {
78
+ const itemIndex = list.keys.findIndex((key) => key === event.active.id);
79
+
80
+ if (itemIndex >= 0) {
81
+ const dragItem: DragItem = {
82
+ id: event.active.id,
83
+ listId,
84
+ index: itemIndex,
85
+ };
86
+
87
+ setActiveItem(dragItem);
88
+ break;
89
+ }
90
+ }
91
+ },
92
+ [registeredLists]
93
+ );
94
+
95
+ const handleDragMove = React.useCallback((event: DragMoveEvent) => {
96
+ setDelta({ x: event.delta.x, y: event.delta.y });
97
+ }, []);
98
+
99
+ const handleDragEnd = React.useCallback(
100
+ (event: DragEndEvent) => {
101
+ const { active, over } = event;
102
+ const activatorEventPoint = activatorEventRef.current
103
+ ? {
104
+ x: activatorEventRef.current.clientX,
105
+ y: activatorEventRef.current.clientY,
106
+ }
107
+ : null;
108
+
109
+ setActiveItem(null);
110
+ activatorEventRef.current = null;
111
+
112
+ if (!activeItem || !over || active.id === over.id) {
113
+ return;
114
+ }
115
+
116
+ // Find target list and item
117
+ let targetListId: string | null = null;
118
+ let targetIndex: number = -1;
119
+
120
+ for (const [listId, list] of registeredLists) {
121
+ const itemIndex = list.keys.findIndex((key) => key === over.id);
122
+
123
+ if (itemIndex >= 0) {
124
+ targetListId = listId;
125
+ targetIndex = itemIndex;
126
+ break;
127
+ }
128
+
129
+ // Also check if dropping on the list container itself
130
+ if (over.id === listId) {
131
+ targetListId = listId;
132
+ targetIndex = list.keys.length; // Append to end
133
+ break;
134
+ }
135
+ }
136
+
137
+ if (!targetListId || !acceptsListDrop(activeItem.listId, targetListId)) {
138
+ setActiveItem(null);
139
+ activatorEventRef.current = null;
140
+ return;
141
+ }
142
+
143
+ const targetList = registeredLists.get(targetListId);
144
+ const sourceList = registeredLists.get(activeItem.listId);
145
+
146
+ if (!targetList || !sourceList) {
147
+ return;
148
+ }
149
+
150
+ // Handle drops into items within the list
151
+ if (targetIndex < targetList.keys.length && activatorEventPoint) {
152
+ // Calculate absolute cursor position
153
+ const eventX = activatorEventPoint.x;
154
+ const eventY = activatorEventPoint.y;
155
+ const currentX = eventX + delta.x;
156
+ const currentY = eventY + delta.y;
157
+ const absolutePosition = { x: currentX, y: currentY };
158
+
159
+ const indicator = targetList.getDropIndicator({
160
+ absolutePosition,
161
+ active,
162
+ over,
163
+ sourceListId: activeItem.listId,
164
+ targetListId,
165
+ });
166
+
167
+ if (!indicator) return;
168
+
169
+ // Notify the source list to handle the move
170
+ sourceList.onMoveItem(
171
+ activeItem.index,
172
+ targetIndex,
173
+ indicator,
174
+ activeItem.listId,
175
+ targetListId
176
+ );
177
+ }
178
+ // Handle drops into empty lists or at the end of lists
179
+ else if (targetIndex >= targetList.keys.length) {
180
+ // For empty lists or drops at the end, we don't need detailed position validation
181
+ // Just check if the drop is acceptable with a "below" position
182
+ const canDrop = targetList.acceptsDrop(
183
+ activeItem.index,
184
+ Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
185
+ "below",
186
+ activeItem.listId,
187
+ targetListId
188
+ );
189
+
190
+ if (canDrop) {
191
+ sourceList.onMoveItem(
192
+ activeItem.index,
193
+ targetIndex,
194
+ "below",
195
+ activeItem.listId,
196
+ targetListId
197
+ );
198
+ }
199
+ }
200
+ },
201
+ [activeItem, registeredLists, acceptsListDrop, delta]
202
+ );
203
+
204
+ const contextValue: DragRegistrationContextType = React.useMemo(
205
+ () => ({
206
+ activeItem,
207
+ delta,
208
+ registerList,
209
+ unregisterList,
210
+ }),
211
+ [activeItem, delta, registerList, unregisterList]
212
+ );
213
+
214
+ const activeList = activeItem ? registeredLists.get(activeItem.listId) : null;
215
+ const activeDragContextValue: ActiveDragContextType = React.useMemo(
216
+ () => ({ activeItem, delta }),
217
+ [activeItem, delta]
218
+ );
219
+
220
+ const collisionDetection = useMemo(() => {
221
+ return getItemFirstCollisionDetection(Array.from(registeredLists.keys()));
222
+ }, [registeredLists]);
223
+
224
+ return (
225
+ <AnyDragContext.Provider value={true}>
226
+ <contexts.dragRegistrationContext.Provider value={contextValue}>
227
+ <contexts.activeDragContext.Provider value={activeDragContextValue}>
228
+ <DndContext
229
+ sensors={sensors}
230
+ collisionDetection={collisionDetection}
231
+ onDragStart={handleDragStart}
232
+ onDragMove={handleDragMove}
233
+ onDragEnd={handleDragEnd}
234
+ >
235
+ {children}
236
+ {mounted &&
237
+ activeList?.renderOverlay &&
238
+ activeItem &&
239
+ createPortal(
240
+ <DragOverlay dropAnimation={null}>
241
+ {activeList.renderOverlay(activeItem.id)}
242
+ </DragOverlay>,
243
+ document.body
244
+ )}
245
+ </DndContext>
246
+ </contexts.activeDragContext.Provider>
247
+ </contexts.dragRegistrationContext.Provider>
248
+ </AnyDragContext.Provider>
249
+ );
250
+ }
251
+
252
+ function useMounted() {
253
+ const [mounted, setMounted] = React.useState(false);
254
+
255
+ React.useEffect(() => {
256
+ setMounted(true);
257
+ }, []);
258
+
259
+ return mounted;
260
+ }
261
+
262
+ export function withDragProvider<T extends object>(
263
+ Component: React.ComponentType<T>
264
+ ) {
265
+ return function WithDragProvider(props: T) {
266
+ const anyDragContext = React.useContext(AnyDragContext);
267
+
268
+ if (!anyDragContext) {
269
+ return (
270
+ <SharedDragProvider>
271
+ <Component {...props} />
272
+ </SharedDragProvider>
273
+ );
274
+ }
275
+
276
+ return <Component {...props} />;
277
+ };
278
+ }
279
+
280
+ // Custom collision detection that prioritizes items over containers
281
+ export const getItemFirstCollisionDetection =
282
+ (registeredListIds: string[]): CollisionDetection =>
283
+ (parameters) => {
284
+ const pointerCollisions = pointerWithin(parameters);
285
+
286
+ if (pointerCollisions.length > 0) {
287
+ const itemCollisions = pointerCollisions.filter((collision) => {
288
+ for (const listId of registeredListIds) {
289
+ if (collision.id === listId) {
290
+ return false;
291
+ }
292
+ }
293
+
294
+ return true;
295
+ });
296
+
297
+ if (itemCollisions.length > 0) {
298
+ return itemCollisions;
299
+ }
300
+ }
301
+
302
+ // Fall back to pointer-based detection (includes containers)
303
+ // This handles empty lists and edge cases
304
+ return pointerCollisions.length > 0
305
+ ? pointerCollisions
306
+ : closestCenter(parameters);
307
+ };