import { useCallback } from 'react'; import { measure, type AnimatedRef, } from 'react-native-reanimated'; import type Animated from 'react-native-reanimated'; import { useDragState } from '../../contexts/DragStateContext'; /** * Configuration for useDragOverlay hook. */ export interface UseDragOverlayConfig { /** Ref to measure item container */ containerRef: AnimatedRef; } /** * Input for overlay setup (from gesture event). */ export interface OverlaySetupInput { /** Absolute Y position of the finger */ absoluteY: number; } /** * Return value from useDragOverlay hook. */ export interface UseDragOverlayResult { /** * Measure item and set up overlay positioning. * Call this in onStart of the pan gesture. * * @worklet - This function runs on the UI thread * @returns The measured item height, or 0 if measurement failed */ measureAndSetupOverlay: (input: OverlaySetupInput) => number; } /** * Hook that handles overlay measurement and positioning during drag start. * * This hook is responsible for: * - Measuring the dragged item's position using measure() * - Setting up overlay coordinates relative to the drag container * - Calculating the touch offset within the item for proper finger tracking * - Setting overlay ready/active flags * * NOTE: This measure() usage is safe per CLAUDE.md guidelines because: * 1. It's only used for INITIAL overlay positioning at drag START * 2. It's NOT used for ongoing calculations or drop positioning * 3. Drop calculations use index-based math (index * itemHeight) which is reliable * * @param config - Configuration with containerRef * @returns Function to measure and setup overlay */ export function useDragOverlay( config: UseDragOverlayConfig, ): UseDragOverlayResult { const { containerRef } = config; const { overlayBaseX, overlayBaseY, overlayWidth, overlayHeight, overlayActive, overlayReady, overlayTouchOffset, overlayAbsoluteY, overlayContainerX, overlayContainerY, overlayContainerReady, dragContainerRef, config: dragConfig, } = useDragState(); const measureAndSetupOverlay = useCallback( (input: OverlaySetupInput): number => { 'worklet'; const { absoluteY } = input; // Measure actual item height for accurate drag calculations. const measured = measure(containerRef); if (measured && overlayContainerReady.value) { // Use Reanimated measure() for both item and container to ensure // identical coordinate systems (fixes Android status bar offset). const containerMeasured = measure(dragContainerRef); if (containerMeasured) { overlayBaseY.value = measured.pageY - containerMeasured.pageY; overlayBaseX.value = measured.pageX - containerMeasured.pageX; // FIX: Update overlayContainerY/X to use same coordinate system as measure() // This prevents a jump caused by measureInWindow() having different offsets // (e.g., Android status bar offset is handled differently between the two APIs) overlayContainerY.value = containerMeasured.pageY; overlayContainerX.value = containerMeasured.pageX; // Capture touch offset within item for absolute positioning // This is how far down in the item the finger started overlayTouchOffset.value = absoluteY - measured.pageY; // IMPORTANT: Also update overlayAbsoluteY to match the same moment // This prevents a jump caused by onBegin capturing a different position // if the finger moved during the long press wait (~200ms) overlayAbsoluteY.value = absoluteY; } else { // Fallback to JS-thread values if container measure fails overlayBaseY.value = measured.pageY - overlayContainerY.value; overlayBaseX.value = measured.pageX - overlayContainerX.value; overlayTouchOffset.value = absoluteY - measured.pageY; // IMPORTANT: Also update overlayAbsoluteY to match the same moment overlayAbsoluteY.value = absoluteY; } overlayWidth.value = measured.width; overlayHeight.value = measured.height; overlayActive.value = true; overlayReady.value = true; // DEV: Warn if itemHeight config is smaller than measured content // This always indicates misconfiguration since config should include margins // Use 1px tolerance to avoid false positives from floating point precision if (__DEV__ && measured.height > dragConfig.itemHeight + 1) { console.warn( `[AnimatedFlashList] itemHeight (${dragConfig.itemHeight}px) is smaller than ` + `measured item content (${measured.height.toFixed(0)}px). ` + `Configure itemHeight to include margins/gaps for accurate drag positioning.` ); } return measured.height; } else { overlayActive.value = false; overlayReady.value = false; return 0; } }, [ containerRef, overlayBaseX, overlayBaseY, overlayWidth, overlayHeight, overlayActive, overlayReady, overlayTouchOffset, overlayAbsoluteY, overlayContainerX, overlayContainerY, overlayContainerReady, dragContainerRef, dragConfig, ], ); return { measureAndSetupOverlay }; }