import { DRAG_THRESHOLDS } from '../constants/drag'; /** * Input parameters for hover index calculation. */ export interface HoverCalculationInput { /** Effective Y translation including scroll compensation */ effectiveTranslateY: number; /** Height of each item in pixels */ itemHeight: number; /** Index of the dragged item at drag start */ draggedIndex: number; /** Total number of items in the list */ totalItems: number; /** Current hovered index (for hysteresis calculation) */ currentHoveredIndex: number; } /** * Result of hover index calculation. */ export interface HoverCalculationResult { /** The proposed new hover index */ proposedIndex: number; /** Whether the hover index should switch (crosses hysteresis threshold) */ shouldSwitch: boolean; /** The position delta from dragged index to proposed index */ positionDelta: number; } /** * Calculates the hover index during drag based on translation and scroll offset. * * Uses a bias offset to make it slightly easier to cross into next/prev positions, * and hysteresis to prevent flickering near boundaries. * * This is a worklet function that runs on the UI thread. * * @param input - The calculation input parameters * @returns The calculation result with proposed index and switch decision */ export function calculateHoverIndex(input: HoverCalculationInput): HoverCalculationResult { 'worklet'; const { effectiveTranslateY, itemHeight, draggedIndex, totalItems, currentHoveredIndex, } = input; // Calculate offset bias based on drag direction const offset = effectiveTranslateY > DRAG_THRESHOLDS.FLOAT_EPSILON ? DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : effectiveTranslateY < -DRAG_THRESHOLDS.FLOAT_EPSILON ? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : 0; // Calculate biased delta (how many items we've moved) const biasedDelta = effectiveTranslateY / itemHeight + offset; // Current hover delta relative to dragged index const currentHoverDelta = currentHoveredIndex - draggedIndex; // Target delta (rounded to nearest integer) const targetDelta = Math.round(biasedDelta); // Calculate difference from current position for hysteresis check const deltaDiff = biasedDelta - currentHoverDelta; // Only switch if we've crossed the hysteresis threshold const shouldSwitch = Math.abs(deltaDiff) >= DRAG_THRESHOLDS.HOVER_SWITCH_THRESHOLD; // Use target delta if switching, otherwise keep current const nextHoverDelta = shouldSwitch ? targetDelta : currentHoverDelta; // Calculate proposed index, clamped to valid range const proposedIndex = Math.max( 0, Math.min(totalItems - 1, draggedIndex + nextHoverDelta), ); return { proposedIndex, shouldSwitch, positionDelta: nextHoverDelta, }; } /** * Input parameters for drop index calculation (used in onEnd). */ export interface DropCalculationInput { /** Final Y translation including scroll compensation */ finalY: number; /** Height of each item in pixels */ itemHeight: number; /** Index of the dragged item at drag start */ draggedIndex: number; /** Total number of items in the list */ totalItems: number; /** The hovered index at the moment of release */ hoveredIndexAtEnd: number; } /** * Result of drop index calculation. */ export interface DropCalculationResult { /** The snapped drop index (clamped to hovered range) */ snapIndex: number; /** Position delta from dragged index to snap index */ positionDelta: number; /** The snapped Y position in pixels */ snappedFinalY: number; /** Raw drop index before clamping (for debugging) */ rawDropIndex: number; } /** * Calculates the drop index when the user releases the drag. * * This uses the same offset bias as hover calculation, but also clamps * the result to the range that was actually hovered during the drag. * This prevents overshoots where the item jumps to an index that was * never visually indicated during the drag. * * This is a worklet function that runs on the UI thread. * * @param input - The calculation input parameters * @returns The calculation result with snap index and position delta */ export function calculateDropIndex(input: DropCalculationInput): DropCalculationResult { 'worklet'; const { finalY, itemHeight, draggedIndex, totalItems, hoveredIndexAtEnd, } = input; // Calculate offset bias based on drag direction const offset = finalY > DRAG_THRESHOLDS.FLOAT_EPSILON ? DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : finalY < -DRAG_THRESHOLDS.FLOAT_EPSILON ? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR : 0; // Calculate raw drop index const rawDropIndex = draggedIndex + finalY / itemHeight; // Round and clamp to valid range const fallbackDelta = Math.round(finalY / itemHeight + offset); const snappedIndexUnclamped = Math.max( 0, Math.min(totalItems - 1, Math.round(rawDropIndex)), ); // Clamp to the range actually hovered during drag. // This prevents overshoots where rawDropIndex jumps past the hovered band. const clampMin = Math.min(draggedIndex, hoveredIndexAtEnd); const clampMax = Math.max(draggedIndex, hoveredIndexAtEnd); const snapIndex = Math.max(clampMin, Math.min(clampMax, snappedIndexUnclamped)); // Calculate final position delta const positionDelta = snapIndex - draggedIndex; const snappedFinalY = positionDelta * itemHeight; return { snapIndex, positionDelta, snappedFinalY, rawDropIndex, }; }