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,567 @@
1
+ // hooks/useDraggable.ts
2
+ import React, { useRef, useCallback, useContext, useEffect, useState, } from "react";
3
+ import { useSharedValue, useAnimatedStyle, withSpring, runOnJS, runOnUI, useAnimatedReaction, useAnimatedRef, measure, } from "react-native-reanimated";
4
+ import { Gesture, } from "react-native-gesture-handler";
5
+ import { SlotsContext, } from "../types/context";
6
+ import { DraggableState, } from "../types/draggable";
7
+ /**
8
+ * A powerful hook for creating draggable components with advanced features like
9
+ * collision detection, bounded dragging, axis constraints, and custom animations.
10
+ *
11
+ * This hook provides the core functionality for drag-and-drop interactions,
12
+ * handling gesture recognition, position tracking, collision detection with drop zones,
13
+ * and smooth animations.
14
+ *
15
+ * @template TData - The type of data associated with the draggable item
16
+ * @param options - Configuration options for the draggable behavior
17
+ * @returns Object containing props, gesture handlers, and state for the draggable component
18
+ *
19
+ * @example
20
+ * Basic draggable component:
21
+ * ```typescript
22
+ * import { useDraggable } from './hooks/useDraggable';
23
+ *
24
+ * function MyDraggable() {
25
+ * const { animatedViewProps, gesture, state } = useDraggable({
26
+ * data: { id: '1', name: 'Draggable Item' },
27
+ * onDragStart: (data) => console.log('Started dragging:', data.name),
28
+ * onDragEnd: (data) => console.log('Finished dragging:', data.name),
29
+ * });
30
+ *
31
+ * return (
32
+ * <GestureDetector gesture={gesture}>
33
+ * <Animated.View {...animatedViewProps}>
34
+ * <Text>Drag me!</Text>
35
+ * </Animated.View>
36
+ * </GestureDetector>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @example
42
+ * Draggable with custom animation and bounds:
43
+ * ```typescript
44
+ * function BoundedDraggable() {
45
+ * const boundsRef = useRef<View>(null);
46
+ *
47
+ * const { animatedViewProps, gesture } = useDraggable({
48
+ * data: { id: '2', type: 'bounded' },
49
+ * dragBoundsRef: boundsRef,
50
+ * dragAxis: 'x', // Only horizontal movement
51
+ * animationFunction: (toValue) => {
52
+ * 'worklet';
53
+ * return withTiming(toValue, { duration: 300 });
54
+ * },
55
+ * collisionAlgorithm: 'center',
56
+ * });
57
+ *
58
+ * return (
59
+ * <View ref={boundsRef} style={styles.container}>
60
+ * <GestureDetector gesture={gesture}>
61
+ * <Animated.View {...animatedViewProps}>
62
+ * <Text>Bounded horizontal draggable</Text>
63
+ * </Animated.View>
64
+ * </GestureDetector>
65
+ * </View>
66
+ * );
67
+ * }
68
+ * ```
69
+ *
70
+ * @example
71
+ * Draggable with state tracking:
72
+ * ```typescript
73
+ * function StatefulDraggable() {
74
+ * const [dragState, setDragState] = useState(DraggableState.IDLE);
75
+ *
76
+ * const { animatedViewProps, gesture } = useDraggable({
77
+ * data: { id: '3', status: 'active' },
78
+ * onStateChange: setDragState,
79
+ * onDragging: ({ x, y, tx, ty }) => {
80
+ * console.log(`Position: (${x + tx}, ${y + ty})`);
81
+ * },
82
+ * });
83
+ *
84
+ * return (
85
+ * <GestureDetector gesture={gesture}>
86
+ * <Animated.View
87
+ * {...animatedViewProps}
88
+ * style={[
89
+ * animatedViewProps.style,
90
+ * { opacity: dragState === DraggableState.DRAGGING ? 0.7 : 1 }
91
+ * ]}
92
+ * >
93
+ * <Text>State: {dragState}</Text>
94
+ * </Animated.View>
95
+ * </GestureDetector>
96
+ * );
97
+ * }
98
+ * ```
99
+ *
100
+ * @see {@link DraggableState} for state management
101
+ * @see {@link CollisionAlgorithm} for collision detection options
102
+ * @see {@link AnimationFunction} for custom animations
103
+ * @see {@link UseDraggableOptions} for configuration options
104
+ * @see {@link UseDraggableReturn} for return value details
105
+ */
106
+ export const useDraggable = (options) => {
107
+ const { data, draggableId, dragDisabled = false, onDragStart, onDragEnd, onDragging, onStateChange, animationFunction, dragBoundsRef, dragAxis = "both", collisionAlgorithm = "intersect", children, handleComponent, } = options;
108
+ // Create animated ref first
109
+ const animatedViewRef = useAnimatedRef();
110
+ // Add state management
111
+ const [state, setState] = useState(DraggableState.IDLE);
112
+ const [hasHandle, setHasHandle] = useState(false);
113
+ // Check if any child is a Handle component
114
+ useEffect(() => {
115
+ if (!children || !handleComponent) {
116
+ setHasHandle(false);
117
+ return;
118
+ }
119
+ // Check if children contain a Handle component
120
+ const checkForHandle = (child) => {
121
+ if (React.isValidElement(child)) {
122
+ // Check for direct component type match
123
+ if (child.type === handleComponent) {
124
+ return true;
125
+ }
126
+ // Check children recursively
127
+ if (child.props && child.props.children) {
128
+ if (React.Children.toArray(child.props.children).some(checkForHandle)) {
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+ return false;
134
+ };
135
+ setHasHandle(React.Children.toArray(children).some(checkForHandle));
136
+ }, [children, handleComponent]);
137
+ useEffect(() => {
138
+ onStateChange === null || onStateChange === void 0 ? void 0 : onStateChange(state);
139
+ }, [state, onStateChange]);
140
+ const tx = useSharedValue(0);
141
+ const ty = useSharedValue(0);
142
+ const offsetX = useSharedValue(0);
143
+ const offsetY = useSharedValue(0);
144
+ const dragDisabledShared = useSharedValue(dragDisabled);
145
+ const dragAxisShared = useSharedValue(dragAxis);
146
+ const originX = useSharedValue(0);
147
+ const originY = useSharedValue(0);
148
+ const itemW = useSharedValue(0);
149
+ const itemH = useSharedValue(0);
150
+ const isOriginSet = useRef(false);
151
+ const internalDraggableId = useRef(draggableId || `draggable-${Math.random().toString(36).substr(2, 9)}`).current;
152
+ const boundsX = useSharedValue(0);
153
+ const boundsY = useSharedValue(0);
154
+ const boundsWidth = useSharedValue(0);
155
+ const boundsHeight = useSharedValue(0);
156
+ const boundsAreSet = useSharedValue(false);
157
+ const { getSlots, setActiveHoverSlot, activeHoverSlotId, registerPositionUpdateListener, unregisterPositionUpdateListener, registerDroppedItem, unregisterDroppedItem, hasAvailableCapacity, onDragging: contextOnDragging, onDragStart: contextOnDragStart, onDragEnd: contextOnDragEnd, } = useContext(SlotsContext);
158
+ useEffect(() => {
159
+ dragDisabledShared.value = dragDisabled;
160
+ }, [dragDisabled, dragDisabledShared]);
161
+ useEffect(() => {
162
+ dragAxisShared.value = dragAxis;
163
+ }, [dragAxis, dragAxisShared]);
164
+ const updateDraggablePosition = useCallback(() => {
165
+ runOnUI(() => {
166
+ "worklet";
167
+ const measurement = measure(animatedViewRef);
168
+ if (measurement === null) {
169
+ return;
170
+ }
171
+ const currentTx = tx.value;
172
+ const currentTy = ty.value;
173
+ //only update the origin if the tx and ty are 0
174
+ if (currentTx === 0 && currentTy === 0) {
175
+ const newOriginX = measurement.pageX - currentTx;
176
+ const newOriginY = measurement.pageY - currentTy;
177
+ originX.value = newOriginX;
178
+ originY.value = newOriginY;
179
+ }
180
+ itemW.value = measurement.width;
181
+ itemH.value = measurement.height;
182
+ if (!isOriginSet.current) {
183
+ isOriginSet.current = true;
184
+ }
185
+ })();
186
+ }, [animatedViewRef, originX, originY, itemW, itemH, tx, ty]);
187
+ // Worklet version for use within UI thread contexts
188
+ const updateDraggablePositionWorklet = useCallback(() => {
189
+ "worklet";
190
+ const measurement = measure(animatedViewRef);
191
+ if (measurement === null) {
192
+ return;
193
+ }
194
+ const currentTx = tx.value;
195
+ const currentTy = ty.value;
196
+ //only update the origin if the tx and ty are 0
197
+ if (currentTx === 0 && currentTy === 0) {
198
+ const newOriginX = measurement.pageX - currentTx;
199
+ const newOriginY = measurement.pageY - currentTy;
200
+ originX.value = newOriginX;
201
+ originY.value = newOriginY;
202
+ }
203
+ itemW.value = measurement.width;
204
+ itemH.value = measurement.height;
205
+ if (!isOriginSet.current) {
206
+ isOriginSet.current = true;
207
+ }
208
+ }, [animatedViewRef, originX, originY, itemW, itemH, tx, ty]);
209
+ const updateBounds = useCallback(() => {
210
+ const currentBoundsView = dragBoundsRef === null || dragBoundsRef === void 0 ? void 0 : dragBoundsRef.current;
211
+ if (currentBoundsView) {
212
+ currentBoundsView.measure((_x, _y, width, height, pageX, pageY) => {
213
+ if (typeof pageX === "number" &&
214
+ typeof pageY === "number" &&
215
+ width > 0 &&
216
+ height > 0) {
217
+ runOnUI(() => {
218
+ "worklet";
219
+ boundsX.value = pageX;
220
+ boundsY.value = pageY;
221
+ boundsWidth.value = width;
222
+ boundsHeight.value = height;
223
+ if (!boundsAreSet.value) {
224
+ boundsAreSet.value = true;
225
+ }
226
+ })();
227
+ }
228
+ else {
229
+ console.warn("useDraggable: dragBoundsRef measurement failed or returned invalid dimensions. Bounds may be stale or item unbounded.");
230
+ }
231
+ });
232
+ }
233
+ else {
234
+ runOnUI(() => {
235
+ "worklet";
236
+ if (boundsAreSet.value) {
237
+ boundsAreSet.value = false;
238
+ }
239
+ })();
240
+ }
241
+ }, [
242
+ dragBoundsRef,
243
+ boundsX,
244
+ boundsY,
245
+ boundsWidth,
246
+ boundsHeight,
247
+ boundsAreSet,
248
+ ]);
249
+ useEffect(() => {
250
+ const handlePositionUpdate = () => {
251
+ updateDraggablePosition();
252
+ updateBounds();
253
+ };
254
+ registerPositionUpdateListener(internalDraggableId, handlePositionUpdate);
255
+ return () => {
256
+ unregisterPositionUpdateListener(internalDraggableId);
257
+ };
258
+ }, [
259
+ internalDraggableId,
260
+ registerPositionUpdateListener,
261
+ unregisterPositionUpdateListener,
262
+ updateDraggablePosition,
263
+ updateBounds,
264
+ ]);
265
+ useEffect(() => {
266
+ updateBounds();
267
+ }, [updateBounds]);
268
+ const handleLayoutHandler = useCallback((event) => {
269
+ updateDraggablePosition();
270
+ }, [updateDraggablePosition]);
271
+ const animateDragEndPosition = useCallback((targetXValue, targetYValue) => {
272
+ "worklet";
273
+ if (animationFunction) {
274
+ tx.value = animationFunction(targetXValue);
275
+ ty.value = animationFunction(targetYValue);
276
+ }
277
+ else {
278
+ tx.value = withSpring(targetXValue);
279
+ ty.value = withSpring(targetYValue);
280
+ }
281
+ }, [animationFunction, tx, ty]);
282
+ const performCollisionCheck = useCallback((draggableX, draggableY, draggableW, draggableH, slot, algo) => {
283
+ if (algo === "intersect") {
284
+ return (draggableX < slot.x + slot.width &&
285
+ draggableX + draggableW > slot.x &&
286
+ draggableY < slot.y + slot.height &&
287
+ draggableY + draggableH > slot.y);
288
+ }
289
+ else if (algo === "contain") {
290
+ return (draggableX >= slot.x &&
291
+ draggableX + draggableW <= slot.x + slot.width &&
292
+ draggableY >= slot.y &&
293
+ draggableY + draggableH <= slot.y + slot.height);
294
+ }
295
+ else {
296
+ const draggableCenterX = draggableX + draggableW / 2;
297
+ const draggableCenterY = draggableY + draggableH / 2;
298
+ return (draggableCenterX >= slot.x &&
299
+ draggableCenterX <= slot.x + slot.width &&
300
+ draggableCenterY >= slot.y &&
301
+ draggableCenterY <= slot.y + slot.height);
302
+ }
303
+ }, []);
304
+ const processDropAndAnimate = useCallback((currentTxVal, currentTyVal, draggableData, currentOriginX, currentOriginY, currentItemW, currentItemH) => {
305
+ const slots = getSlots();
306
+ const currentDraggableX = currentOriginX + currentTxVal;
307
+ const currentDraggableY = currentOriginY + currentTyVal;
308
+ let hitSlotData = null;
309
+ let hitSlotId = null;
310
+ for (const key in slots) {
311
+ const slotId = parseInt(key, 10);
312
+ const s = slots[slotId];
313
+ const isCollision = performCollisionCheck(currentDraggableX, currentDraggableY, currentItemW, currentItemH, s, collisionAlgorithm);
314
+ if (isCollision) {
315
+ const hasCapacity = hasAvailableCapacity(s.id);
316
+ if (hasCapacity) {
317
+ hitSlotData = s;
318
+ hitSlotId = slotId;
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ let finalTxValue;
324
+ let finalTyValue;
325
+ if (hitSlotData && hitSlotId !== null) {
326
+ if (hitSlotData.onDrop) {
327
+ runOnJS(hitSlotData.onDrop)(draggableData);
328
+ }
329
+ runOnJS(registerDroppedItem)(internalDraggableId, hitSlotData.id, draggableData);
330
+ runOnJS(setState)(DraggableState.DROPPED);
331
+ const alignment = hitSlotData.dropAlignment || "center";
332
+ const offset = hitSlotData.dropOffset || { x: 0, y: 0 };
333
+ let targetX = 0;
334
+ let targetY = 0;
335
+ switch (alignment) {
336
+ case "top-left":
337
+ targetX = hitSlotData.x;
338
+ targetY = hitSlotData.y;
339
+ break;
340
+ case "top-center":
341
+ targetX = hitSlotData.x + hitSlotData.width / 2 - currentItemW / 2;
342
+ targetY = hitSlotData.y;
343
+ break;
344
+ case "top-right":
345
+ targetX = hitSlotData.x + hitSlotData.width - currentItemW;
346
+ targetY = hitSlotData.y;
347
+ break;
348
+ case "center-left":
349
+ targetX = hitSlotData.x;
350
+ targetY = hitSlotData.y + hitSlotData.height / 2 - currentItemH / 2;
351
+ break;
352
+ case "center":
353
+ targetX = hitSlotData.x + hitSlotData.width / 2 - currentItemW / 2;
354
+ targetY = hitSlotData.y + hitSlotData.height / 2 - currentItemH / 2;
355
+ break;
356
+ case "center-right":
357
+ targetX = hitSlotData.x + hitSlotData.width - currentItemW;
358
+ targetY = hitSlotData.y + hitSlotData.height / 2 - currentItemH / 2;
359
+ break;
360
+ case "bottom-left":
361
+ targetX = hitSlotData.x;
362
+ targetY = hitSlotData.y + hitSlotData.height - currentItemH;
363
+ break;
364
+ case "bottom-center":
365
+ targetX = hitSlotData.x + hitSlotData.width / 2 - currentItemW / 2;
366
+ targetY = hitSlotData.y + hitSlotData.height - currentItemH;
367
+ break;
368
+ case "bottom-right":
369
+ targetX = hitSlotData.x + hitSlotData.width - currentItemW;
370
+ targetY = hitSlotData.y + hitSlotData.height - currentItemH;
371
+ break;
372
+ default:
373
+ targetX = hitSlotData.x + hitSlotData.width / 2 - currentItemW / 2;
374
+ targetY = hitSlotData.y + hitSlotData.height / 2 - currentItemH / 2;
375
+ }
376
+ const draggableTargetX = targetX + offset.x;
377
+ const draggableTargetY = targetY + offset.y;
378
+ finalTxValue = draggableTargetX - currentOriginX;
379
+ finalTyValue = draggableTargetY - currentOriginY;
380
+ }
381
+ else {
382
+ // No hit slot or no capacity available - reset to original position and set state to IDLE
383
+ finalTxValue = 0;
384
+ finalTyValue = 0;
385
+ runOnJS(setState)(DraggableState.IDLE);
386
+ runOnJS(unregisterDroppedItem)(internalDraggableId);
387
+ }
388
+ runOnUI(animateDragEndPosition)(finalTxValue, finalTyValue);
389
+ }, [
390
+ getSlots,
391
+ animateDragEndPosition,
392
+ collisionAlgorithm,
393
+ performCollisionCheck,
394
+ setState,
395
+ internalDraggableId,
396
+ registerDroppedItem,
397
+ unregisterDroppedItem,
398
+ hasAvailableCapacity,
399
+ ]);
400
+ const updateHoverState = useCallback((currentTxVal, currentTyVal, currentOriginX, currentOriginY, currentItemW, currentItemH) => {
401
+ const slots = getSlots();
402
+ const currentDraggableX = currentOriginX + currentTxVal;
403
+ const currentDraggableY = currentOriginY + currentTyVal;
404
+ let newHoveredSlotId = null;
405
+ for (const key in slots) {
406
+ const slotId = parseInt(key, 10);
407
+ const s = slots[slotId];
408
+ const isCollision = performCollisionCheck(currentDraggableX, currentDraggableY, currentItemW, currentItemH, s, collisionAlgorithm);
409
+ if (isCollision) {
410
+ newHoveredSlotId = slotId;
411
+ break;
412
+ }
413
+ }
414
+ if (activeHoverSlotId !== newHoveredSlotId) {
415
+ setActiveHoverSlot(newHoveredSlotId);
416
+ }
417
+ }, [
418
+ getSlots,
419
+ setActiveHoverSlot,
420
+ activeHoverSlotId,
421
+ collisionAlgorithm,
422
+ performCollisionCheck,
423
+ ]);
424
+ const gesture = React.useMemo(() => Gesture.Pan()
425
+ .onBegin(() => {
426
+ "worklet";
427
+ //first update the position
428
+ updateDraggablePositionWorklet();
429
+ if (dragDisabledShared.value)
430
+ return;
431
+ offsetX.value = tx.value;
432
+ offsetY.value = ty.value;
433
+ // Update state to DRAGGING when drag begins
434
+ runOnJS(setState)(DraggableState.DRAGGING);
435
+ if (onDragStart)
436
+ runOnJS(onDragStart)(data);
437
+ if (contextOnDragStart)
438
+ runOnJS(contextOnDragStart)(data);
439
+ })
440
+ .onUpdate((event) => {
441
+ "worklet";
442
+ if (dragDisabledShared.value)
443
+ return;
444
+ let newTx = offsetX.value + event.translationX;
445
+ let newTy = offsetY.value + event.translationY;
446
+ if (boundsAreSet.value) {
447
+ const currentItemW = itemW.value;
448
+ const currentItemH = itemH.value;
449
+ const minTx = boundsX.value - originX.value;
450
+ const maxTx = boundsX.value + boundsWidth.value - originX.value - currentItemW;
451
+ const minTy = boundsY.value - originY.value;
452
+ const maxTy = boundsY.value + boundsHeight.value - originY.value - currentItemH;
453
+ newTx = Math.max(minTx, Math.min(newTx, maxTx));
454
+ newTy = Math.max(minTy, Math.min(newTy, maxTy));
455
+ }
456
+ if (dragAxisShared.value === "x") {
457
+ tx.value = newTx;
458
+ }
459
+ else if (dragAxisShared.value === "y") {
460
+ ty.value = newTy;
461
+ }
462
+ else {
463
+ tx.value = newTx;
464
+ ty.value = newTy;
465
+ }
466
+ if (onDragging) {
467
+ runOnJS(onDragging)({
468
+ x: originX.value,
469
+ y: originY.value,
470
+ tx: tx.value,
471
+ ty: ty.value,
472
+ itemData: data,
473
+ });
474
+ }
475
+ if (contextOnDragging) {
476
+ runOnJS(contextOnDragging)({
477
+ x: originX.value,
478
+ y: originY.value,
479
+ tx: tx.value,
480
+ ty: ty.value,
481
+ itemData: data,
482
+ });
483
+ }
484
+ runOnJS(updateHoverState)(tx.value, ty.value, originX.value, originY.value, itemW.value, itemH.value);
485
+ })
486
+ .onEnd(() => {
487
+ "worklet";
488
+ if (dragDisabledShared.value)
489
+ return;
490
+ if (onDragEnd)
491
+ runOnJS(onDragEnd)(data);
492
+ if (contextOnDragEnd)
493
+ runOnJS(contextOnDragEnd)(data);
494
+ runOnJS(processDropAndAnimate)(tx.value, ty.value, data, originX.value, originY.value, itemW.value, itemH.value);
495
+ runOnJS(setActiveHoverSlot)(null);
496
+ }), [
497
+ dragDisabledShared,
498
+ offsetX,
499
+ offsetY,
500
+ tx,
501
+ ty,
502
+ originX,
503
+ originY,
504
+ itemW,
505
+ itemH,
506
+ onDragStart,
507
+ onDragEnd,
508
+ data,
509
+ processDropAndAnimate,
510
+ updateHoverState,
511
+ setActiveHoverSlot,
512
+ animationFunction,
513
+ onDragging,
514
+ boundsAreSet,
515
+ boundsX,
516
+ boundsY,
517
+ boundsWidth,
518
+ boundsHeight,
519
+ dragAxisShared,
520
+ setState,
521
+ updateDraggablePositionWorklet,
522
+ contextOnDragging,
523
+ contextOnDragStart,
524
+ contextOnDragEnd,
525
+ ]);
526
+ const animatedStyleProp = useAnimatedStyle(() => {
527
+ "worklet";
528
+ return {
529
+ transform: [{ translateX: tx.value }, { translateY: ty.value }],
530
+ };
531
+ }, [tx, ty]);
532
+ // Replace the React useEffect with useAnimatedReaction to properly handle shared values
533
+ useAnimatedReaction(() => {
534
+ // This runs on the UI thread and detects when position is back to origin
535
+ // and state needs to be reset
536
+ return {
537
+ txValue: tx.value,
538
+ tyValue: ty.value,
539
+ isZero: tx.value === 0 && ty.value === 0,
540
+ };
541
+ }, (result, previous) => {
542
+ // Only trigger when values change to zero (returned to original position)
543
+ if (result.isZero && previous && !previous.isZero) {
544
+ // Use runOnJS to call setState from the UI thread
545
+ runOnJS(setState)(DraggableState.IDLE);
546
+ // When returning to origin position, we know we're no longer dropped
547
+ runOnJS(unregisterDroppedItem)(internalDraggableId);
548
+ }
549
+ }, [setState, unregisterDroppedItem, internalDraggableId]);
550
+ // Clean up on unmount
551
+ useEffect(() => {
552
+ return () => {
553
+ // Clean up any registered drops when unmounting
554
+ unregisterDroppedItem(internalDraggableId);
555
+ };
556
+ }, [internalDraggableId, unregisterDroppedItem]);
557
+ return {
558
+ animatedViewProps: {
559
+ style: animatedStyleProp,
560
+ onLayout: handleLayoutHandler,
561
+ },
562
+ gesture,
563
+ state,
564
+ animatedViewRef,
565
+ hasHandle,
566
+ };
567
+ };
@@ -0,0 +1,129 @@
1
+ import { UseDroppableOptions, UseDroppableReturn } from "../types/droppable";
2
+ /**
3
+ * A hook for creating drop zones that can receive draggable items.
4
+ *
5
+ * This hook handles the registration of drop zones, collision detection with draggable items,
6
+ * visual feedback during hover states, and proper positioning of dropped items within the zone.
7
+ * It integrates seamlessly with the drag-and-drop context to provide a complete solution.
8
+ *
9
+ * @template TData - The type of data that can be dropped on this droppable
10
+ * @param options - Configuration options for the droppable behavior
11
+ * @returns Object containing view props, active state, and internal references
12
+ *
13
+ * @example
14
+ * Basic drop zone:
15
+ * ```typescript
16
+ * import { useDroppable } from './hooks/useDroppable';
17
+ *
18
+ * function BasicDropZone() {
19
+ * const { viewProps, isActive } = useDroppable({
20
+ * onDrop: (data) => {
21
+ * console.log('Item dropped:', data);
22
+ * // Handle the dropped item
23
+ * }
24
+ * });
25
+ *
26
+ * return (
27
+ * <Animated.View
28
+ * {...viewProps}
29
+ * style={[
30
+ * styles.dropZone,
31
+ * viewProps.style, // Important: include the active style
32
+ * isActive && styles.highlighted
33
+ * ]}
34
+ * >
35
+ * <Text>Drop items here</Text>
36
+ * </Animated.View>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @example
42
+ * Drop zone with custom alignment and capacity:
43
+ * ```typescript
44
+ * function TaskColumn() {
45
+ * const [tasks, setTasks] = useState<Task[]>([]);
46
+ *
47
+ * const { viewProps, isActive } = useDroppable({
48
+ * droppableId: 'in-progress-column',
49
+ * onDrop: (task: Task) => {
50
+ * setTasks(prev => [...prev, task]);
51
+ * updateTaskStatus(task.id, 'in-progress');
52
+ * },
53
+ * dropAlignment: 'top-center',
54
+ * dropOffset: { x: 0, y: 10 },
55
+ * capacity: 10, // Max 10 tasks in this column
56
+ * activeStyle: {
57
+ * backgroundColor: 'rgba(59, 130, 246, 0.1)',
58
+ * borderColor: '#3b82f6',
59
+ * borderWidth: 2,
60
+ * borderStyle: 'dashed'
61
+ * }
62
+ * });
63
+ *
64
+ * return (
65
+ * <Animated.View {...viewProps} style={[styles.column, viewProps.style]}>
66
+ * <Text style={styles.columnTitle}>In Progress ({tasks.length}/10)</Text>
67
+ * {tasks.map(task => (
68
+ * <TaskCard key={task.id} task={task} />
69
+ * ))}
70
+ * {isActive && (
71
+ * <Text style={styles.dropHint}>Release to add task</Text>
72
+ * )}
73
+ * </Animated.View>
74
+ * );
75
+ * }
76
+ * ```
77
+ *
78
+ * @example
79
+ * Conditional drop zone with validation:
80
+ * ```typescript
81
+ * function RestrictedDropZone() {
82
+ * const [canAcceptItems, setCanAcceptItems] = useState(true);
83
+ *
84
+ * const { viewProps, isActive } = useDroppable({
85
+ * onDrop: (data: FileData) => {
86
+ * if (data.type === 'image' && data.size < 5000000) {
87
+ * uploadFile(data);
88
+ * } else {
89
+ * showError('Only images under 5MB allowed');
90
+ * }
91
+ * },
92
+ * dropDisabled: !canAcceptItems,
93
+ * onActiveChange: (active) => {
94
+ * if (active) {
95
+ * setHoverFeedback('Drop your image here');
96
+ * } else {
97
+ * setHoverFeedback('');
98
+ * }
99
+ * },
100
+ * activeStyle: {
101
+ * backgroundColor: canAcceptItems ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)',
102
+ * borderColor: canAcceptItems ? '#22c55e' : '#ef4444'
103
+ * }
104
+ * });
105
+ *
106
+ * return (
107
+ * <Animated.View
108
+ * {...viewProps}
109
+ * style={[
110
+ * styles.uploadZone,
111
+ * viewProps.style,
112
+ * !canAcceptItems && styles.disabled
113
+ * ]}
114
+ * >
115
+ * <Text>
116
+ * {canAcceptItems ? 'Drop images here' : 'Upload disabled'}
117
+ * </Text>
118
+ * {isActive && <Text>Release to upload</Text>}
119
+ * </Animated.View>
120
+ * );
121
+ * }
122
+ * ```
123
+ *
124
+ * @see {@link DropAlignment} for alignment options
125
+ * @see {@link DropOffset} for offset configuration
126
+ * @see {@link UseDroppableOptions} for configuration options
127
+ * @see {@link UseDroppableReturn} for return value details
128
+ */
129
+ export declare const useDroppable: <TData = unknown>(options: UseDroppableOptions<TData>) => UseDroppableReturn;