Development
Hook
Hooks
What react-native-reanimated-dnd runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add entropyconquers/react-native-reanimated-dnd > /plugin install reanimated-dnd@reanimated-dnd-skills
Ships with react-native-reanimated-dnd. Installing the plugin gets these hooks.
Where it lives
- hooks/index.tsGitHub
Read the script
// Export hooks export { useSortable } from "./useSortable"; export { useSortableList } from "./useSortableList"; export { useHorizontalSortable } from "./useHorizontalSortable"; export { useHorizontalSortableList } from "./useHorizontalSortableList"; export { useDraggable } from "./useDraggable"; export { useDroppable } from "./useDroppable"; - hooks/safeMeasure.tsGitHub
Read the script
import { Component } from "react"; import { AnimatedRef, measure } from "react-native-reanimated"; /** * Reanimated can throw before returning `null` when a node is not attached yet. * Guarding this keeps early Fabric measurements from taking down the app. */ export const safeMeasure = <T extends Component>(ref: AnimatedRef<T>) => { "worklet"; try { const measurement = measure(ref); if (!measurement) { return null; } if (measurement.width <= 0 || measurement.height <= 0) { return null; } return measurement; } catch { return null; } }; - hooks/useDraggable.tsGitHub
Read the script
// hooks/useDraggable.ts import React, { useCallback, useContext, useEffect, useRef, useState, } from "react"; import { LayoutChangeEvent } from "react-native"; import Animated, { AnimatedStyle, useAnimatedReaction, useAnimatedRef, useSharedValue, useAnimatedStyle, withSpring, } from "react-native-reanimated"; import { Gesture, GestureType, PanGestureHandlerEventPayload, } from "react-native-gesture-handler"; import { scheduleOnRN, scheduleOnUI } from "react-native-worklets"; import { DropAlignment, DropOffset, DropSlot, SlotsContext, SlotsContextValue, } from "../types/context"; import { AnimationFunction, CollisionAlgorithm, DraggableState, UseDraggableOptions, UseDraggableReturn, } from "../types/draggable"; import { safeMeasure } from "./safeMeasure"; /** * A powerful hook for creating draggable components with advanced features like * collision detection, bounded dragging, axis constraints, and custom animations. * * This hook provides the core functionality for drag-and-drop interactions, * handling gesture recognition, position tracking, collision detection with drop zones, * and smooth animations. * * @template TData - The type of data associated with the draggable item * @param options - Configuration options for the draggable behavior * @returns Object containing props, gesture handlers, and state for the draggable component * * @example * Basic draggable component: * ```typescript * import { useDraggable } from './hooks/useDraggable'; * * function MyDraggable() { * const { animatedViewProps, gesture, state } = useDraggable({ * data: { id: '1', name: 'Draggable Item' }, * onDragStart: (data) => console.log('Started dragging:', data.name), * onDragEnd: (data) => console.log('Finished dragging:', data.name), * }); * * return ( * <GestureDetector gesture={gesture}> * <Animated.View {...animatedViewProps}> * <Text>Drag me!</Text> * </Animated.View> * </GestureDetector> * ); * } * ``` * * @example * Draggable with custom animation and bounds: * ```typescript * function BoundedDraggable() { * const boundsRef = useRef<View>(null); * * const { animatedViewProps, gesture } = useDraggable({ * data: { id: '2', type: 'bounded' }, * dragBoundsRef: boundsRef, * dragAxis: 'x', // Only horizontal movement * animationFunction: (toValue) => { * 'worklet'; * return withTiming(toValue, { duration: 300 }); * }, * collisionAlgorithm: 'center', * }); * * return ( * <View ref={boundsRef} style={styles.container}> * <GestureDetector gesture={gesture}> * <Animated.View {...animatedViewProps}> * <Text>Bounded horizontal draggable</Text> * </Animated.View> * </GestureDetector> * </View> * ); * } * ``` * * @example * Draggable with state tracking: * ```typescript * function StatefulDraggable() { * const [dragState, setDragState] = useState(DraggableState.IDLE); * * const { animatedViewProps, gesture } = useDraggable({ * data: { id: '3', status: 'active' }, * onStateChange: setDragState, * onDragging: ({ x, y, tx, ty }) => { * console.log(`Position: (${x + tx}, ${y + ty})`); * }, * }); * * return ( * <GestureDetector gesture={gesture}> * <Animated.View * {...animatedViewProps} * style={[ * animatedViewProps.style, * { opacity: dragState === DraggableState.DRAGGING ? 0.7 : 1 } * ]} * > * <Text>State: {dragState}</Text> * </Animated.View> * </GestureDetector> * ); * } * ``` * * @see {@link DraggableState} for state management * @see {@link CollisionAlgorithm} for collision detection options * @see {@link AnimationFunction} for custom animations * @see {@link UseDraggableOptions} for configuration options * @see {@link UseDraggableReturn} for return value details */ export const useDraggable = <TData = unknown>( options: UseDraggableOptions<TData> ): UseDraggableReturn => { const { data, draggableId, dragDisabled = false, preDragDelay = 0, onDragStart, onDragEnd, onDragging, onStateChange, animationFunction, dragBoundsRef, dragAxis = "both", collisionAlgorithm = "intersect", } = options; // Create animated ref first const animatedViewRef = useAnimatedRef<Animated.View>(); // Add state management const [state, setState] = useState<DraggableState>(DraggableState.IDLE); const [hasHandle, setHasHandle] = useState(false); const registerHandle = useCallback((registered: boolean) => { setHasHandle(registered); }, []); useEffect(() => { onStateChange?.(state); }, [state, onStateChange]); const tx = useSharedValue(0); const ty = useSharedValue(0); const offsetX = useSharedValue(0); const offsetY = useSharedValue(0); const dragDisabledShared = useSharedValue(dragDisabled); const dragAxisShared = useSharedValue(dragAxis); const preDragDelayShared = useSharedValue(preDragDelay); const nodeReady = useSharedValue(false); const originX = useSharedValue(0); const originY = useSharedValue(0); const itemW = useSharedValue(0); const itemH = useSharedValue(0); const isOriginSet = useRef(false); const internalDraggableId = useRef( draggableId || `draggable-${Math.random().toString(36).substr(2, 9)}` ).current; const boundsX = useSharedValue(0); const boundsY = useSharedValue(0); const boundsWidth = useSharedValue(0); const boundsHeight = useSharedValue(0); const boundsAreSet = useSharedValue(false); const { getSlots, setActiveHoverSlot, activeHoverSlotId, registerPositionUpdateListener, unregisterPositionUpdateListener, registerDroppedItem, unregisterDroppedItem, hasAvailableCapacity, onDragging: contextOnDragging, onDragStart: contextOnDragStart, onDragEnd: context - hooks/useDroppable.tsGitHub
Read the script
import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; import { LayoutChangeEvent, StyleSheet } from "react-native"; import Animated, { useAnimatedRef, useSharedValue, } from "react-native-reanimated"; import { scheduleOnRN, scheduleOnUI } from "react-native-worklets"; import { DropAlignment, DropOffset, SlotsContext, SlotsContextValue, } from "../types/context"; import { UseDroppableOptions, UseDroppableReturn } from "../types/droppable"; import { safeMeasure } from "./safeMeasure"; let _nextDroppableId = 1; const _getUniqueDroppableId = (): number => { return _nextDroppableId++; }; /** * A hook for creating drop zones that can receive draggable items. * * This hook handles the registration of drop zones, collision detection with draggable items, * visual feedback during hover states, and proper positioning of dropped items within the zone. * It integrates seamlessly with the drag-and-drop context to provide a complete solution. * * @template TData - The type of data that can be dropped on this droppable * @param options - Configuration options for the droppable behavior * @returns Object containing view props, active state, and internal references * * @example * Basic drop zone: * ```typescript * import { useDroppable } from './hooks/useDroppable'; * * function BasicDropZone() { * const { viewProps, isActive } = useDroppable({ * onDrop: (data) => { * console.log('Item dropped:', data); * // Handle the dropped item * } * }); * * return ( * <Animated.View * {...viewProps} * style={[ * styles.dropZone, * viewProps.style, // Important: include the active style * isActive && styles.highlighted * ]} * > * <Text>Drop items here</Text> * </Animated.View> * ); * } * ``` * * @example * Drop zone with custom alignment and capacity: * ```typescript * function TaskColumn() { * const [tasks, setTasks] = useState<Task[]>([]); * * const { viewProps, isActive } = useDroppable({ * droppableId: 'in-progress-column', * onDrop: (task: Task) => { * setTasks(prev => [...prev, task]); * updateTaskStatus(task.id, 'in-progress'); * }, * dropAlignment: 'top-center', * dropOffset: { x: 0, y: 10 }, * capacity: 10, // Max 10 tasks in this column * activeStyle: { * backgroundColor: 'rgba(59, 130, 246, 0.1)', * borderColor: '#3b82f6', * borderWidth: 2, * borderStyle: 'dashed' * } * }); * * return ( * <Animated.View {...viewProps} style={[styles.column, viewProps.style]}> * <Text style={styles.columnTitle}>In Progress ({tasks.length}/10)</Text> * {tasks.map(task => ( * <TaskCard key={task.id} task={task} /> * ))} * {isActive && ( * <Text style={styles.dropHint}>Release to add task</Text> * )} * </Animated.View> * ); * } * ``` * * @example * Conditional drop zone with validation: * ```typescript * function RestrictedDropZone() { * const [canAcceptItems, setCanAcceptItems] = useState(true); * * const { viewProps, isActive } = useDroppable({ * onDrop: (data: FileData) => { * if (data.type === 'image' && data.size < 5000000) { * uploadFile(data); * } else { * showError('Only images under 5MB allowed'); * } * }, * dropDisabled: !canAcceptItems, * onActiveChange: (active) => { * if (active) { * setHoverFeedback('Drop your image here'); * } else { * setHoverFeedback(''); * } * }, * activeStyle: { * backgroundColor: canAcceptItems ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)', * borderColor: canAcceptItems ? '#22c55e' : '#ef4444' * } * }); * * return ( * <Animated.View * {...viewProps} * style={[ * styles.uploadZone, * viewProps.style, * !canAcceptItems && styles.disabled * ]} * > * <Text> * {canAcceptItems ? 'Drop images here' : 'Upload disabled'} * </Text> * {isActive && <Text>Release to upload</Text>} * </Animated.View> * ); * } * ``` * * @see {@link DropAlignment} for alignment options * @see {@link DropOffset} for offset configuration * @see {@link UseDroppableOptions} for configuration options * @see {@link UseDroppableReturn} for return value details */ export const useDroppable = <TData = unknown>( options: UseDroppableOptions<TData> ): UseDroppableReturn => { const { onDrop, dropDisabled, onActiveChange, dropAlignment, dropOffset, activeStyle, droppableId, capacity, } = options; // Create animated ref first const nodeReady = useSharedValue(false); const animatedViewRef = useAnimatedRef<Animated.View>(); const id = useRef(_getUniqueDroppableId()).current; const stringId = useRef(droppableId || `droppable-${id}`).current; const instanceId = useRef( `droppable-${id}-${Math.random().toString(36).substr(2, 9)}` ).current; const { register, unregister, isRegistered, activeHoverSlotId: contextActiveHoverSlotId, registerPositionUpdateListener, unregisterPositionUpdateListener, } = useContext(SlotsContext) as SlotsContextValue<TData>; const isActive = contextActiveHoverSlotId === id; // Process active style to separate transforms from other styles const { processedActiveStyle, activeTransforms } = useMemo(() => { if (!isActive || !activeStyle) { return { processedActiveStyle: null, activeTransforms: [] }; } const flattenedStyle = StyleSheet.flatten(activeStyle); let processedStyle = { ...flattenedStyle }; let transforms: any[] = []; // Extract and process transforms if present if (flattenedStyle.transform) { if (Array.isArray(flattenedStyle.transform)) { transforms = [...flattenedStyle.transform]; } - hooks/useGridSortable.tsGitHub
Read the script
import { useCallback, useState, useRef } from "react"; import { StyleProp, ViewStyle } from "react-native"; import { SharedValue, useAnimatedReaction, useAnimatedStyle, useSharedValue, withSpring, withTiming, } from "react-native-reanimated"; import { Gesture, GestureType } from "react-native-gesture-handler"; import { scheduleOnRN } from "react-native-worklets"; import { GridScrollDirection, GridPositions, GridDimensions, GridOrientation, GridStrategy, UseGridSortableOptions, UseGridSortableReturn, } from "../types/grid"; import { setGridPosition, setGridAutoScroll, calculateGridContentDimensions, } from "../utils/gridCalculations"; /** * A hook for creating sortable grid items with drag-and-drop reordering capabilities. * * This hook provides the core functionality for individual items within a sortable grid, * handling drag gestures, position animations, auto-scrolling, and reordering logic. * It works in conjunction with useGridSortableList to provide a complete sortable grid solution. * * @template T - The type of data associated with the sortable grid item * @param options - Configuration options for the sortable grid item behavior * @returns Object containing animated styles, gesture handlers, and state for the grid item */ export function useGridSortable<T>( options: UseGridSortableOptions<T> ): UseGridSortableReturn { const { id, positions, scrollY, scrollX, autoScrollDirection, itemsCount, dimensions, orientation, strategy = GridStrategy.Insert, containerWidth = 500, containerHeight = 500, activationDelay, onMove, onDragStart, onDrop, onDragging, isBeingRemoved = false, } = options; const [isMoving, setIsMoving] = useState(false); const [hasHandle, setHasHandle] = useState(false); const registerHandle = useCallback((registered: boolean) => { setHasHandle(registered); }, []); const movingSV = useSharedValue(false); const currentOverItemId = useSharedValue<string | null>(null); const onDraggingLastCallTimestamp = useSharedValue(0); const THROTTLE_INTERVAL = 50; const initialPositionRef = useRef<{ x: number; y: number } | null>(null); if (initialPositionRef.current === null) { const posArr = positions.get(); const pos = posArr?.[id]; initialPositionRef.current = pos ? { x: pos.x, y: pos.y } : { x: 0, y: 0 }; } const initialPosition = initialPositionRef.current; const positionX = useSharedValue(initialPosition.x); const positionY = useSharedValue(initialPosition.y); const topValue = useSharedValue(initialPosition.y); const leftValue = useSharedValue(initialPosition.x); const targetScrollY = useSharedValue(0); const targetScrollX = useSharedValue(0); // Context shared values (replaces gesture handler context object) const initialItemContentX = useSharedValue(0); const initialItemContentY = useSharedValue(0); const initialFingerAbsoluteX = useSharedValue(0); const initialFingerAbsoluteY = useSharedValue(0); const initialScrollY = useSharedValue(0); const initialScrollX = useSharedValue(0); const calculatedContainerHeight = useRef(containerHeight).current; const calculatedContainerWidth = useRef(containerWidth).current; // React to position changes during drag (hit detection, reordering) useAnimatedReaction( () => ({ x: positionX.value, y: positionY.value }), (current, previous) => { if (!movingSV.value) { return; } if ( previous !== null && current.x === previous.x && current.y === previous.y ) { return; } // Calculate target cell for hit detection const { itemWidth, itemHeight, columnGap = 0, rowGap = 0, columns = 3 } = dimensions; const clampedColumn = Math.min( Math.max(0, Math.round(current.x / (itemWidth + columnGap))), (orientation === GridOrientation.Vertical ? columns : Infinity) - 1 ); const clampedRow = Math.floor(current.y / (itemHeight + rowGap)); let targetIndex: number; if (orientation === GridOrientation.Vertical) { targetIndex = clampedRow * columns + clampedColumn; } else { const rows = dimensions.rows ?? 3; targetIndex = clampedColumn * rows + clampedRow; } targetIndex = Math.max(0, Math.min(targetIndex, itemsCount - 1)); // Determine overItemId let newOverItemId: string | null = null; const positionsValue = positions.value; for (const itemId in positionsValue) { if (positionsValue[itemId].index === targetIndex && itemId !== id) { newOverItemId = itemId; break; } } if (currentOverItemId.value !== newOverItemId) { currentOverItemId.value = newOverItemId; } if (onDragging) { const now = Date.now(); if (now - onDraggingLastCallTimestamp.value > THROTTLE_INTERVAL) { scheduleOnRN( onDragging, id, newOverItemId, Math.round(current.x), Math.round(current.y) ); onDraggingLastCallTimestamp.value = now; } } // Update visual position and logical positions topValue.value = current.y; leftValue.value = current.x; setGridPosition( current.x, current.y, scrollX.value, scrollY.value, itemsCount, positions, id, dimensions, orientation, strategy ); setGridAutoScroll( current.x, current.y, scrollX.value, scrollY.value, calculatedContainerWidth, calculatedContainerHeight, dimensions.itemHeight, autoScrollDirection ); }, [ movingSV, dimensions, itemsCount, positions, id, orientation, strategy, onDragging, scrollX, scrollY, autoScrollDirection, - hooks/useGridSortableList.tsGitHub
Read the script
import { useRef, useCallback, useEffect } from "react"; import { scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useSharedValue, } from "react-native-reanimated"; import { GridScrollDirection, GridOrientation, GridStrategy, UseGridSortableListOptions, UseGridSortableListReturn, } from "../types/grid"; import { SortableData } from "../types/sortable"; import { DropProviderRef } from "../types/context"; import { listToGridObject, calculateGridContentDimensions, } from "../utils/gridCalculations"; /** * A hook for managing sortable grids with drag-and-drop reordering capabilities. * * This hook provides the foundational state management and utilities needed to create * sortable grids. It handles position tracking, scroll synchronization, auto-scrolling, * and provides helper functions for individual sortable grid items. * * @template TData - The type of data items in the sortable grid (must extend `{ id: string }`) * @param options - Configuration options for the sortable grid * @returns Object containing shared values, refs, handlers, and utilities for the sortable grid */ export function useGridSortableList<TData extends SortableData>( options: UseGridSortableListOptions<TData> ): UseGridSortableListReturn<TData> { const { data, dimensions, orientation = GridOrientation.Vertical, strategy = GridStrategy.Insert, itemKeyExtractor = (item) => item.id, } = options; // Runtime validation in development mode if (__DEV__) { data.forEach((item, index) => { const id = itemKeyExtractor(item, index); if (typeof id !== "string" || !id) { console.error( `[react-native-reanimated-dnd] Grid item at index ${index} has invalid id: ${id}. ` + `Each item must have a unique string id property.` ); } }); } // Set up shared values const positions = useSharedValue( listToGridObject(data, dimensions, orientation) ); const scrollY = useSharedValue(0); const scrollX = useSharedValue(0); const autoScrollDirection = useSharedValue(GridScrollDirection.None); const scrollViewRef = useAnimatedRef(); const dropProviderRef = useRef<DropProviderRef>(null); // Update positions when data or dimensions change useEffect(() => { positions.value = listToGridObject(data, dimensions, orientation); }, [ data.length, data.map((d) => itemKeyExtractor(d, 0)).join(","), dimensions.columns, dimensions.rows, dimensions.itemWidth, dimensions.itemHeight, dimensions.rowGap, dimensions.columnGap, orientation, ]); // Scrolling synchronization useAnimatedReaction( () => scrollY.value, (scrolling) => { scrollTo(scrollViewRef, scrollX.value, scrolling, false); } ); useAnimatedReaction( () => scrollX.value, (scrolling) => { scrollTo(scrollViewRef, scrolling, scrollY.value, false); } ); // Handle scroll events const handleScroll = useAnimatedScrollHandler((event) => { scrollY.value = event.contentOffset.y; scrollX.value = event.contentOffset.x; }); const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null); const handleScrollEnd = useCallback(() => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } scrollTimeoutRef.current = setTimeout(() => { dropProviderRef.current?.requestPositionUpdate(); }, 50); }, []); // Calculate content dimensions const { width: contentWidth, height: contentHeight } = calculateGridContentDimensions(data.length, dimensions, orientation); // Helper to get props for each grid item const getItemProps = useCallback( (item: TData, index: number) => { const id = itemKeyExtractor(item, index); return { id, positions, scrollY, scrollX, autoScrollDirection, itemsCount: data.length, dimensions, orientation, strategy, }; }, [ data.length, dimensions, orientation, strategy, itemKeyExtractor, positions, scrollY, scrollX, autoScrollDirection, ] ); return { positions, scrollY, scrollX, autoScrollDirection, scrollViewRef, dropProviderRef, handleScroll, handleScrollEnd, contentWidth, contentHeight, getItemProps, }; } - hooks/useHorizontalSortable.tsGitHub
- hooks/useHorizontalSortableList.tsGitHub
- hooks/useSortable.tsGitHub
- hooks/useSortableList.tsGitHub
All 10 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withreact-native-reanimated-dnd
A drag-and-drop library that finally works on React Native ✨ Powerful, performant, and built for the modern React Native developer
Get the whole plugin
Stats
1,054
Stars
43
Forks
Active
Maintenance
TypeScript
Language
MIT
License
11d ago
Last commit
1y ago
Created
Repo: entropyconquers/react-native-reanimated-dnd

