import { useLayoutEffect, useRef } from "react"; import { useSharedValue, useDerivedValue, makeMutable, type SharedValue, } from "react-native-reanimated"; import { useRecyclingState } from "@shopify/flash-list"; import { useDragState } from "../../contexts/DragStateContext"; import { DRAG_THRESHOLDS } from "../../constants/drag"; import type { UseDragShiftConfig, UseDragShiftResult } from "../../types"; let DISABLED_SHIFT_Y: SharedValue | null = null; function getDisabledShiftY(): SharedValue { if (!DISABLED_SHIFT_Y) { DISABLED_SHIFT_Y = makeMutable(0); } return DISABLED_SHIFT_Y; } /** * Hook that calculates shift for non-dragged items. * * Returns a purely derived value (no separate animated SharedValue). * The animated style hook handles smooth transitions via withTiming. * * @example * ```tsx * const { targetShiftY } = useDragShift({ itemId: item.id, index }); * // Use targetShiftY.value in animated style (apply withTiming for smooth animation) * ``` */ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult { const { itemId, index, dragEnabled = true } = config; const wasEverEnabled = useRef(dragEnabled); if (dragEnabled) wasEverEnabled.current = true; if (!wasEverEnabled.current) { return { shiftY: getDisabledShiftY() }; } const { isDragging: globalIsDragging, isDropping, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, config: dragConfig, dragStartIndexSnapshot, dragSequence, hoveredIndex, } = useDragState(); const currentIndex = useSharedValue(index); const [prevItemId, setPrevItemId] = useRecyclingState(itemId, [itemId]); const prevIndexRef = useRef(index); useLayoutEffect(() => { const previousIndex = prevIndexRef.current; prevIndexRef.current = index; if (prevItemId !== itemId) { currentIndex.value = index; setPrevItemId(itemId); } else if (previousIndex !== index) { currentIndex.value = index; } }, [itemId, prevItemId, setPrevItemId, currentIndex, index]); const targetShiftY = useDerivedValue(() => { "worklet"; const _sequence = dragSequence.value; const currentDraggedIndex = draggedIndex.value; const isDraggingNow = globalIsDragging.value; const translateYNow = currentTranslateY.value; const currentDraggedItemId = draggedItemId.value; // Always use config.itemHeight - it includes margin/gap which matches FlashList positioning const itemHeight = dragConfig.itemHeight; if (currentDraggedItemId === itemId) return 0; // Keep shift targets during drop until FlashList repositions if (!isDraggingNow && !isDropping.value) { return 0; } const registryToUse = dragStartIndexSnapshot.value; const snapshotIndex = registryToUse[itemId]; const myIndex = snapshotIndex !== undefined ? snapshotIndex : currentIndex.value; const scrollDelta = scrollOffset.value - dragStartScrollOffset.value; const effectiveTranslateY = translateYNow + scrollDelta; const offset = effectiveTranslateY > 0 ? DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : effectiveTranslateY < 0 ? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : 0; const computedHoveredIndex = currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset); const hoveredIndexValue = hoveredIndex.value >= 0 ? hoveredIndex.value : computedHoveredIndex; const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndexValue); const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndexValue); if (myIndex < minAffectedIndex || myIndex > maxAffectedIndex) { return 0; } let dragShift = 0; if (hoveredIndexValue > currentDraggedIndex) { if (myIndex > currentDraggedIndex && myIndex <= hoveredIndexValue) { dragShift = -itemHeight; } } else if (hoveredIndexValue < currentDraggedIndex) { if (myIndex < currentDraggedIndex && myIndex >= hoveredIndexValue) { dragShift = itemHeight; } } return dragShift; }, [itemId]); return { shiftY: targetShiftY }; }