import { useLayoutEffect, useRef } from "react"; import { withTiming, cancelAnimation, Easing, } from "react-native-reanimated"; import { useRecyclingState } from "@shopify/flash-list"; import { useDragState } from "../../contexts/DragStateContext"; import { ANIMATION_TIMING, DRAG_THRESHOLDS, } from "../../constants/drag"; import type { UseDropCompensationConfig } from "../../types"; /** * Hook that handles index change compensation after drag reorder. * * REFACTORED: Simplified to only handle non-dragged item compensation. * The complex commit detection has been removed - we now trust FlashList * to handle item repositioning and simply snap non-dragged items to their * correct positions during drop. * * When an item is dropped and the data updates, React re-renders with new indices. * This hook compensates non-dragged items by adjusting their translateY. * * @example * ```tsx * useDropCompensation({ * itemId: item.id, * index, * translateY, * }); * ``` */ export function useDropCompensation(config: UseDropCompensationConfig): void { const { itemId, index, translateY, dragEnabled = true } = config; // Track if drag was ever enabled (for stable hook call order) const wasEverEnabled = useRef(dragEnabled); if (dragEnabled) wasEverEnabled.current = true; // PERFORMANCE OPTIMIZATION: Early return when drag was never enabled // This avoids creating SharedValues and effects for non-draggable items. if (!wasEverEnabled.current) { return; } // Global drag state const { isDragging: globalIsDragging, draggedItemId, measuredItemHeight, isDropping, config: dragConfig, } = useDragState(); // Use FlashList's useRecyclingState for automatic reset on view recycling const [prevIndex, setPrevIndex] = useRecyclingState(index, [itemId]); // Handle index changes for NON-DRAGGED items after data updates useLayoutEffect(() => { if (index !== prevIndex) { const isTheDraggedItem = draggedItemId.value === itemId; // Skip if this is the dragged item if (isTheDraggedItem) { setPrevIndex(index); return; } // During active drop, snap to 0 to avoid conflicts with FlashList repositioning const dropInFlight = isDropping.value || globalIsDragging.value; if (dropInFlight) { if ( Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD + DRAG_THRESHOLDS.FLOAT_EPSILON ) { cancelAnimation(translateY); translateY.value = 0; } setPrevIndex(index); return; } // Outside active drag/drop, compensate for index change // Always use config.itemHeight - matches FlashList positioning (includes margins) const itemHeight = dragConfig.itemHeight; const indexDelta = index - prevIndex; const heightDelta = indexDelta * itemHeight; const compensatedY = translateY.value - heightDelta; if ( Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD + DRAG_THRESHOLDS.FLOAT_EPSILON ) { translateY.value = withTiming(compensatedY, { duration: ANIMATION_TIMING.COMPENSATION_DURATION, easing: Easing.out(Easing.ease), }); } setPrevIndex(index); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [index, prevIndex, translateY, setPrevIndex, itemId]); }