react-native-reanimated-dnd 1.0.1

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +633 -0
  3. package/lib/components/Draggable.d.ts +5 -0
  4. package/lib/components/Draggable.js +265 -0
  5. package/lib/components/Droppable.d.ts +264 -0
  6. package/lib/components/Droppable.js +284 -0
  7. package/lib/components/Sortable.d.ts +184 -0
  8. package/lib/components/Sortable.js +225 -0
  9. package/lib/components/SortableItem.d.ts +158 -0
  10. package/lib/components/SortableItem.js +251 -0
  11. package/lib/components/sortableUtils.d.ts +21 -0
  12. package/lib/components/sortableUtils.js +50 -0
  13. package/lib/context/DropContext.d.ts +118 -0
  14. package/lib/context/DropContext.js +233 -0
  15. package/lib/hooks/index.d.ts +4 -0
  16. package/lib/hooks/index.js +5 -0
  17. package/lib/hooks/useDraggable.d.ts +101 -0
  18. package/lib/hooks/useDraggable.js +567 -0
  19. package/lib/hooks/useDroppable.d.ts +129 -0
  20. package/lib/hooks/useDroppable.js +261 -0
  21. package/lib/hooks/useSortable.d.ts +174 -0
  22. package/lib/hooks/useSortable.js +361 -0
  23. package/lib/hooks/useSortableList.d.ts +182 -0
  24. package/lib/hooks/useSortableList.js +211 -0
  25. package/lib/index.d.ts +11 -0
  26. package/lib/index.js +16 -0
  27. package/lib/types/context.d.ts +166 -0
  28. package/lib/types/context.js +80 -0
  29. package/lib/types/draggable.d.ts +313 -0
  30. package/lib/types/draggable.js +31 -0
  31. package/lib/types/droppable.d.ts +197 -0
  32. package/lib/types/droppable.js +1 -0
  33. package/lib/types/index.d.ts +4 -0
  34. package/lib/types/index.js +8 -0
  35. package/lib/types/sortable.d.ts +432 -0
  36. package/lib/types/sortable.js +6 -0
  37. package/package.json +59 -0
@@ -0,0 +1,261 @@
1
+ import { useRef, useEffect, useContext, useCallback, useMemo, } from "react";
2
+ import { StyleSheet } from "react-native";
3
+ import { useAnimatedRef, measure, runOnUI, runOnJS, } from "react-native-reanimated";
4
+ import { SlotsContext, } from "../types/context";
5
+ import { _getUniqueDroppableId } from "../components/Droppable";
6
+ /**
7
+ * A hook for creating drop zones that can receive draggable items.
8
+ *
9
+ * This hook handles the registration of drop zones, collision detection with draggable items,
10
+ * visual feedback during hover states, and proper positioning of dropped items within the zone.
11
+ * It integrates seamlessly with the drag-and-drop context to provide a complete solution.
12
+ *
13
+ * @template TData - The type of data that can be dropped on this droppable
14
+ * @param options - Configuration options for the droppable behavior
15
+ * @returns Object containing view props, active state, and internal references
16
+ *
17
+ * @example
18
+ * Basic drop zone:
19
+ * ```typescript
20
+ * import { useDroppable } from './hooks/useDroppable';
21
+ *
22
+ * function BasicDropZone() {
23
+ * const { viewProps, isActive } = useDroppable({
24
+ * onDrop: (data) => {
25
+ * console.log('Item dropped:', data);
26
+ * // Handle the dropped item
27
+ * }
28
+ * });
29
+ *
30
+ * return (
31
+ * <Animated.View
32
+ * {...viewProps}
33
+ * style={[
34
+ * styles.dropZone,
35
+ * viewProps.style, // Important: include the active style
36
+ * isActive && styles.highlighted
37
+ * ]}
38
+ * >
39
+ * <Text>Drop items here</Text>
40
+ * </Animated.View>
41
+ * );
42
+ * }
43
+ * ```
44
+ *
45
+ * @example
46
+ * Drop zone with custom alignment and capacity:
47
+ * ```typescript
48
+ * function TaskColumn() {
49
+ * const [tasks, setTasks] = useState<Task[]>([]);
50
+ *
51
+ * const { viewProps, isActive } = useDroppable({
52
+ * droppableId: 'in-progress-column',
53
+ * onDrop: (task: Task) => {
54
+ * setTasks(prev => [...prev, task]);
55
+ * updateTaskStatus(task.id, 'in-progress');
56
+ * },
57
+ * dropAlignment: 'top-center',
58
+ * dropOffset: { x: 0, y: 10 },
59
+ * capacity: 10, // Max 10 tasks in this column
60
+ * activeStyle: {
61
+ * backgroundColor: 'rgba(59, 130, 246, 0.1)',
62
+ * borderColor: '#3b82f6',
63
+ * borderWidth: 2,
64
+ * borderStyle: 'dashed'
65
+ * }
66
+ * });
67
+ *
68
+ * return (
69
+ * <Animated.View {...viewProps} style={[styles.column, viewProps.style]}>
70
+ * <Text style={styles.columnTitle}>In Progress ({tasks.length}/10)</Text>
71
+ * {tasks.map(task => (
72
+ * <TaskCard key={task.id} task={task} />
73
+ * ))}
74
+ * {isActive && (
75
+ * <Text style={styles.dropHint}>Release to add task</Text>
76
+ * )}
77
+ * </Animated.View>
78
+ * );
79
+ * }
80
+ * ```
81
+ *
82
+ * @example
83
+ * Conditional drop zone with validation:
84
+ * ```typescript
85
+ * function RestrictedDropZone() {
86
+ * const [canAcceptItems, setCanAcceptItems] = useState(true);
87
+ *
88
+ * const { viewProps, isActive } = useDroppable({
89
+ * onDrop: (data: FileData) => {
90
+ * if (data.type === 'image' && data.size < 5000000) {
91
+ * uploadFile(data);
92
+ * } else {
93
+ * showError('Only images under 5MB allowed');
94
+ * }
95
+ * },
96
+ * dropDisabled: !canAcceptItems,
97
+ * onActiveChange: (active) => {
98
+ * if (active) {
99
+ * setHoverFeedback('Drop your image here');
100
+ * } else {
101
+ * setHoverFeedback('');
102
+ * }
103
+ * },
104
+ * activeStyle: {
105
+ * backgroundColor: canAcceptItems ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)',
106
+ * borderColor: canAcceptItems ? '#22c55e' : '#ef4444'
107
+ * }
108
+ * });
109
+ *
110
+ * return (
111
+ * <Animated.View
112
+ * {...viewProps}
113
+ * style={[
114
+ * styles.uploadZone,
115
+ * viewProps.style,
116
+ * !canAcceptItems && styles.disabled
117
+ * ]}
118
+ * >
119
+ * <Text>
120
+ * {canAcceptItems ? 'Drop images here' : 'Upload disabled'}
121
+ * </Text>
122
+ * {isActive && <Text>Release to upload</Text>}
123
+ * </Animated.View>
124
+ * );
125
+ * }
126
+ * ```
127
+ *
128
+ * @see {@link DropAlignment} for alignment options
129
+ * @see {@link DropOffset} for offset configuration
130
+ * @see {@link UseDroppableOptions} for configuration options
131
+ * @see {@link UseDroppableReturn} for return value details
132
+ */
133
+ export const useDroppable = (options) => {
134
+ const { onDrop, dropDisabled, onActiveChange, dropAlignment, dropOffset, activeStyle, droppableId, capacity, } = options;
135
+ // Create animated ref first
136
+ const animatedViewRef = useAnimatedRef();
137
+ const id = useRef(_getUniqueDroppableId()).current;
138
+ const stringId = useRef(droppableId || `droppable-${id}`).current;
139
+ const instanceId = useRef(`droppable-${id}-${Math.random().toString(36).substr(2, 9)}`).current;
140
+ const { register, unregister, isRegistered, activeHoverSlotId: contextActiveHoverSlotId, registerPositionUpdateListener, unregisterPositionUpdateListener, } = useContext(SlotsContext);
141
+ const isActive = contextActiveHoverSlotId === id;
142
+ // Process active style to separate transforms from other styles
143
+ const { processedActiveStyle, activeTransforms } = useMemo(() => {
144
+ if (!isActive || !activeStyle) {
145
+ return { processedActiveStyle: null, activeTransforms: [] };
146
+ }
147
+ const flattenedStyle = StyleSheet.flatten(activeStyle);
148
+ let processedStyle = { ...flattenedStyle };
149
+ let transforms = [];
150
+ // Extract and process transforms if present
151
+ if (flattenedStyle.transform) {
152
+ if (Array.isArray(flattenedStyle.transform)) {
153
+ transforms = [...flattenedStyle.transform];
154
+ }
155
+ // Remove transform from the main style to avoid conflicts
156
+ delete processedStyle.transform;
157
+ }
158
+ return {
159
+ processedActiveStyle: processedStyle,
160
+ activeTransforms: transforms,
161
+ };
162
+ }, [isActive, activeStyle]);
163
+ // Create the final style with transforms properly handled
164
+ const combinedActiveStyle = useMemo(() => {
165
+ if (!isActive || !activeStyle) {
166
+ return undefined;
167
+ }
168
+ // If there are no transforms, just return the processed style
169
+ if (activeTransforms.length === 0) {
170
+ return processedActiveStyle;
171
+ }
172
+ // Add transforms to the style
173
+ return {
174
+ ...processedActiveStyle,
175
+ transform: activeTransforms,
176
+ };
177
+ }, [isActive, activeStyle, processedActiveStyle, activeTransforms]);
178
+ useEffect(() => {
179
+ onActiveChange === null || onActiveChange === void 0 ? void 0 : onActiveChange(isActive);
180
+ }, [isActive, onActiveChange]);
181
+ useEffect(() => {
182
+ console.log(`Droppable ${id} using string ID: ${stringId}, provided ID: ${droppableId || "none"}`);
183
+ }, [id, stringId, droppableId]);
184
+ const updateDroppablePosition = useCallback(() => {
185
+ runOnUI(() => {
186
+ "worklet";
187
+ const measurement = measure(animatedViewRef);
188
+ if (measurement === null) {
189
+ return;
190
+ }
191
+ if (measurement.width > 0 && measurement.height > 0) {
192
+ // Ensure valid dimensions before registering
193
+ runOnJS(register)(id, {
194
+ id: droppableId || `droppable-${id}`,
195
+ x: measurement.pageX,
196
+ y: measurement.pageY,
197
+ width: measurement.width,
198
+ height: measurement.height,
199
+ onDrop,
200
+ dropAlignment: dropAlignment || "center",
201
+ dropOffset: dropOffset || { x: 0, y: 0 },
202
+ capacity,
203
+ });
204
+ }
205
+ })();
206
+ }, [
207
+ id,
208
+ droppableId,
209
+ onDrop,
210
+ register,
211
+ animatedViewRef,
212
+ dropAlignment,
213
+ dropOffset,
214
+ capacity,
215
+ ]);
216
+ const handleLayoutHandler = useCallback((_event) => {
217
+ updateDroppablePosition();
218
+ }, [updateDroppablePosition]);
219
+ useEffect(() => {
220
+ registerPositionUpdateListener(instanceId, updateDroppablePosition);
221
+ return () => {
222
+ unregisterPositionUpdateListener(instanceId);
223
+ };
224
+ }, [
225
+ instanceId,
226
+ registerPositionUpdateListener,
227
+ unregisterPositionUpdateListener,
228
+ updateDroppablePosition,
229
+ ]);
230
+ useEffect(() => {
231
+ if (dropDisabled) {
232
+ unregister(id);
233
+ }
234
+ else {
235
+ // Initial registration or re-registration if it became enabled
236
+ updateDroppablePosition();
237
+ }
238
+ // Not relying on isRegistered here for initial registration to ensure it always attempts
239
+ // to register if not disabled. The measure call inside updateDroppablePosition is the gatekeeper.
240
+ }, [
241
+ dropDisabled,
242
+ id,
243
+ unregister, // only unregister is truly a dependency for the disabled case
244
+ updateDroppablePosition, // for the enabled case
245
+ ]);
246
+ useEffect(() => {
247
+ // Cleanup on unmount
248
+ return () => {
249
+ unregister(id);
250
+ };
251
+ }, [id, unregister]);
252
+ return {
253
+ viewProps: {
254
+ onLayout: handleLayoutHandler,
255
+ style: combinedActiveStyle,
256
+ },
257
+ isActive,
258
+ activeStyle,
259
+ animatedViewRef,
260
+ };
261
+ };
@@ -0,0 +1,174 @@
1
+ import { StyleProp, ViewStyle } from "react-native";
2
+ import React from "react";
3
+ import { SharedValue } from "react-native-reanimated";
4
+ export declare enum ScrollDirection {
5
+ None = "none",
6
+ Up = "up",
7
+ Down = "down"
8
+ }
9
+ export declare function clamp(value: number, lowerBound: number, upperBound: number): number;
10
+ export declare function objectMove(object: {
11
+ [id: string]: number;
12
+ }, from: number, to: number): {
13
+ [id: string]: number;
14
+ };
15
+ export declare function listToObject<T extends {
16
+ id: string;
17
+ }>(list: T[]): {
18
+ [id: string]: number;
19
+ };
20
+ export declare function setPosition(positionY: number, itemsCount: number, positions: SharedValue<{
21
+ [id: string]: number;
22
+ }>, id: string, itemHeight: number): void;
23
+ export declare function setAutoScroll(positionY: number, lowerBound: number, upperBound: number, scrollThreshold: number, autoScroll: SharedValue<ScrollDirection>): void;
24
+ /**
25
+ * @see {@link UseSortableOptions} for configuration options
26
+ * @see {@link UseSortableReturn} for return value details
27
+ * @see {@link useSortableList} for list-level management
28
+ * @see {@link SortableItem} for component implementation
29
+ * @see {@link Sortable} for high-level sortable list component
30
+ */
31
+ export interface UseSortableOptions<T> {
32
+ id: string;
33
+ positions: SharedValue<{
34
+ [id: string]: number;
35
+ }>;
36
+ lowerBound: SharedValue<number>;
37
+ autoScrollDirection: SharedValue<ScrollDirection>;
38
+ itemsCount: number;
39
+ itemHeight: number;
40
+ containerHeight?: number;
41
+ onMove?: (id: string, from: number, to: number) => void;
42
+ onDragStart?: (id: string, position: number) => void;
43
+ onDrop?: (id: string, position: number) => void;
44
+ onDragging?: (id: string, overItemId: string | null, yPosition: number) => void;
45
+ children?: React.ReactNode;
46
+ handleComponent?: React.ComponentType<any>;
47
+ }
48
+ export interface UseSortableReturn {
49
+ animatedStyle: StyleProp<ViewStyle>;
50
+ panGestureHandler: any;
51
+ isMoving: boolean;
52
+ hasHandle: boolean;
53
+ }
54
+ /**
55
+ * A hook for creating sortable list items with drag-and-drop reordering capabilities.
56
+ *
57
+ * This hook provides the core functionality for individual items within a sortable list,
58
+ * handling drag gestures, position animations, auto-scrolling, and reordering logic.
59
+ * It works in conjunction with useSortableList to provide a complete sortable solution.
60
+ *
61
+ * @template T - The type of data associated with the sortable item
62
+ * @param options - Configuration options for the sortable item behavior
63
+ * @returns Object containing animated styles, gesture handlers, and state for the sortable item
64
+ *
65
+ * @example
66
+ * Basic sortable item:
67
+ * ```typescript
68
+ * import { useSortable } from './hooks/useSortable';
69
+ *
70
+ * function SortableTaskItem({ task, positions, ...sortableProps }) {
71
+ * const { animatedStyle, panGestureHandler, isMoving } = useSortable({
72
+ * id: task.id,
73
+ * positions,
74
+ * ...sortableProps,
75
+ * onMove: (id, from, to) => {
76
+ * console.log(`Task ${id} moved from ${from} to ${to}`);
77
+ * reorderTasks(id, from, to);
78
+ * }
79
+ * });
80
+ *
81
+ * return (
82
+ * <PanGestureHandler {...panGestureHandler}>
83
+ * <Animated.View style={[styles.taskItem, animatedStyle]}>
84
+ * <Text style={[styles.taskText, isMoving && styles.dragging]}>
85
+ * {task.title}
86
+ * </Text>
87
+ * </Animated.View>
88
+ * </PanGestureHandler>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * Sortable item with drag handle:
95
+ * ```typescript
96
+ * import { useSortable } from './hooks/useSortable';
97
+ * import { SortableHandle } from './components/SortableItem';
98
+ *
99
+ * function TaskWithHandle({ task, ...sortableProps }) {
100
+ * const { animatedStyle, panGestureHandler, hasHandle } = useSortable({
101
+ * id: task.id,
102
+ * ...sortableProps,
103
+ * children: (
104
+ * <View style={styles.taskContent}>
105
+ * <Text>{task.title}</Text>
106
+ * <SortableHandle>
107
+ * <Icon name="drag-handle" />
108
+ * </SortableHandle>
109
+ * </View>
110
+ * )
111
+ * });
112
+ *
113
+ * return (
114
+ * <PanGestureHandler {...panGestureHandler}>
115
+ * <Animated.View style={[styles.taskItem, animatedStyle]}>
116
+ * <View style={styles.taskContent}>
117
+ * <Text>{task.title}</Text>
118
+ * <SortableHandle>
119
+ * <Icon name="drag-handle" />
120
+ * </SortableHandle>
121
+ * </View>
122
+ * </Animated.View>
123
+ * </PanGestureHandler>
124
+ * );
125
+ * }
126
+ * ```
127
+ *
128
+ * @example
129
+ * Sortable item with callbacks and state tracking:
130
+ * ```typescript
131
+ * function AdvancedSortableItem({ item, ...sortableProps }) {
132
+ * const [isDragging, setIsDragging] = useState(false);
133
+ *
134
+ * const { animatedStyle, panGestureHandler } = useSortable({
135
+ * id: item.id,
136
+ * ...sortableProps,
137
+ * onDragStart: (id, position) => {
138
+ * setIsDragging(true);
139
+ * analytics.track('drag_start', { itemId: id, position });
140
+ * },
141
+ * onDrop: (id, position) => {
142
+ * setIsDragging(false);
143
+ * analytics.track('drag_end', { itemId: id, position });
144
+ * },
145
+ * onDragging: (id, overItemId, yPosition) => {
146
+ * if (overItemId) {
147
+ * showDropPreview(overItemId);
148
+ * }
149
+ * }
150
+ * });
151
+ *
152
+ * return (
153
+ * <PanGestureHandler {...panGestureHandler}>
154
+ * <Animated.View
155
+ * style={[
156
+ * styles.item,
157
+ * animatedStyle,
158
+ * isDragging && styles.dragging
159
+ * ]}
160
+ * >
161
+ * <Text>{item.title}</Text>
162
+ * </Animated.View>
163
+ * </PanGestureHandler>
164
+ * );
165
+ * }
166
+ * ```
167
+ *
168
+ * @see {@link UseSortableOptions} for configuration options
169
+ * @see {@link UseSortableReturn} for return value details
170
+ * @see {@link useSortableList} for list-level management
171
+ * @see {@link SortableItem} for component implementation
172
+ * @see {@link Sortable} for high-level sortable list component
173
+ */
174
+ export declare function useSortable<T>(options: UseSortableOptions<T>): UseSortableReturn;