import { memo, type ReactElement, useCallback } from "react"; import { StyleSheet, type ViewStyle } from "react-native"; import Animated, { useAnimatedStyle, useSharedValue, useDerivedValue, withTiming } from "react-native-reanimated"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { calculateLayout, lastOrder, type Offset, remove, reorder, between, useVector } from "./Layout"; import type { DuoAnimatedStyleWorklet, OnDropFunction } from "./types"; import type { DuoWordAnimatedStyle } from "./index"; import { scheduleOnRN } from "react-native-worklets"; export interface SortableWordProps { animatedStyleWorklet?: DuoAnimatedStyleWorklet; onDrop?: OnDropFunction; offsets: Offset[]; children: ReactElement<{ id: number }>; index: number; containerWidth: number; gesturesDisabled: boolean; rtl: boolean; linesHeight: number; wordHeight: number; wordGap: number; wordBankOffsetY: number; lineGap: number; } const SortableWord = ({ animatedStyleWorklet, offsets, index, children, containerWidth, gesturesDisabled, rtl, linesHeight, wordHeight, wordGap, wordBankOffsetY, lineGap, onDrop, }: SortableWordProps) => { const offset = offsets[index]; const isGestureActive = useSharedValue(false); const isAnimating = useSharedValue(false); const translation = useVector(); const isInBank = useDerivedValue(() => offset.order.value === -1); const ctxX = useSharedValue(0); const ctxY = useSharedValue(0); const panOrderHasChanged = useSharedValue(false); const emitOnDrop = useCallback( () => onDrop?.({ index, destination: offset.order.value === -1 ? "bank" : "answered", position: offset.order.value, }), [index, offset, onDrop], ); const panGesture = Gesture.Pan() .onStart(() => { if (isAnimating.value) { return; } panOrderHasChanged.value = false; if (isInBank.value) { translation.x.value = offset.originalX.value; translation.y.value = offset.originalY.value + wordBankOffsetY; } else { translation.x.value = offset.x.value; translation.y.value = offset.y.value; } ctxX.value = translation.x.value; ctxY.value = translation.y.value; }) .onChange(({ translationX, translationY }) => { isGestureActive.value = true; translation.x.value = ctxX.value + translationX; translation.y.value = ctxY.value + translationY; if (isInBank.value && translation.y.value < linesHeight) { offset.order.value = lastOrder(offsets); calculateLayout(offsets, containerWidth, wordHeight, wordGap, lineGap, rtl); panOrderHasChanged.value = true; } else if (!isInBank.value && translation.y.value > linesHeight - wordHeight / 2) { offset.order.value = -1; remove(offsets, index); calculateLayout(offsets, containerWidth, wordHeight, wordGap, lineGap, rtl); panOrderHasChanged.value = true; } for (let i = 0; i < offsets.length; i++) { const o = offsets[i]; if (i === index && o.order.value !== -1) { continue; } const isItemInBank = o.order.value === -1; const x = isItemInBank ? o.originalX.value : o.x.value; const y = isItemInBank ? o.originalY.value + wordBankOffsetY : o.y.value; if ( between(translation.x.value, x, x + o.width.value, false) && between(translation.y.value, y, y + wordHeight) && // NOTE: check y value when interacting with bottom offset.order.value !== o.order.value ) { reorder(offsets, offset.order.value, o.order.value); calculateLayout(offsets, containerWidth, wordHeight, wordGap, lineGap, rtl); panOrderHasChanged.value = true; break; } } }) .onEnd(() => { isAnimating.value = true; translation.x.value = offset.x.value; translation.y.value = offset.y.value; isGestureActive.value = false; if (panOrderHasChanged.value) { scheduleOnRN(emitOnDrop); } panOrderHasChanged.value = false; }); const translateX = useDerivedValue(() => { if (isGestureActive.value) { return translation.x.value; } return withTiming( isInBank.value ? offset.originalX.value : offset.x.value, { duration: 250 }, () => (isAnimating.value = false), ); }); const translateY = useDerivedValue(() => { if (isGestureActive.value) { return translation.y.value; } return withTiming( isInBank.value ? offset.originalY.value + wordBankOffsetY : offset.y.value, { duration: 250 }, () => (isAnimating.value = false), ); }); const style = useAnimatedStyle(() => { const style: DuoWordAnimatedStyle & ViewStyle = { position: "absolute", top: 0, left: -1, zIndex: isGestureActive.value || isAnimating.value ? 100 : Math.max(1, offset.order.value), width: offset.width.value + 2, height: wordHeight, transform: [{ translateX: translateX.value }, { translateY: translateY.value }], }; return (animatedStyleWorklet?.(style, isGestureActive.value) || style) as ViewStyle; }); const tapGesture = Gesture.Tap().onStart(() => { if (isInBank.value) { offset.order.value = lastOrder(offsets); } else { offset.order.value = -1; remove(offsets, index); } isAnimating.value = true; calculateLayout(offsets, containerWidth, wordHeight, wordGap, lineGap, rtl); translation.x.value = offset.x.value; translation.y.value = offset.y.value; scheduleOnRN(emitOnDrop); }); return ( {gesturesDisabled ? ( {children} ) : ( {children} )} ); }; export default memo(SortableWord);