All files / apps/host/src/components/AnimatedPendingRequestList/animations useImageAnimation.ts

88.88% Statements 24/27
91.17% Branches 31/34
100% Functions 7/7
86.95% Lines 20/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90                                        3x               10x       10x   10x 4x     10x   4x 3x 3x       3x 1x                         10x 10x   10x 9x 2x                   10x 10x   10x 6x                    
import {
  SharedValue,
  useAnimatedReaction,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
 
import { ANIMATION_DURATION_SHORT, EASING_OUT_CUBIC } from '@/constants/animation';
 
import { MASCOT_DRAG_FADE_FULL, MASCOT_DRAG_FADE_START } from '../constants';
 
interface UseImageAnimationProps {
  imageAnimationProgress?: SharedValue<number>;
  translateX: SharedValue<number>;
  translateY: SharedValue<number>;
  currentIndex: number;
  index: number;
  isCurrentItemNewRequest?: boolean;
}
 
export const useImageAnimation = ({
  imageAnimationProgress,
  translateX,
  translateY,
  currentIndex,
  index,
  isCurrentItemNewRequest,
}: UseImageAnimationProps) => {
  const wasDragging = useSharedValue(false);
 
  // Real-time drag: fade mascot out proportional to drag distance
  // On snap-back (return to rest after drag): spring mascot back in
  useAnimatedReaction(
    () => {
      if (!imageAnimationProgress || currentIndex !== index || isCurrentItemNewRequest) return null;
      return Math.max(Math.abs(translateX.value), Math.abs(translateY.value));
    },
    dragDist => {
      if (dragDist === null || !imageAnimationProgress) return;
 
      if (dragDist > MASCOT_DRAG_FADE_START) {
        wasDragging.value = true;
        const t = Math.min(
          1,
          (dragDist - MASCOT_DRAG_FADE_START) / (MASCOT_DRAG_FADE_FULL - MASCOT_DRAG_FADE_START),
        );
        imageAnimationProgress.value = 1 - t;
      } else Iif (dragDist <= 2 && wasDragging.value) {
        // Card returned to rest after a drag (snap-back complete)
        wasDragging.value = false;
        imageAnimationProgress.value = withTiming(1, {
          duration: ANIMATION_DURATION_SHORT,
          easing: EASING_OUT_CUBIC,
        });
      }
    },
    [currentIndex, index, isCurrentItemNewRequest],
  );
 
  // Loop card on top: hide mascot
  useAnimatedReaction(
    () => currentIndex === index && !!isCurrentItemNewRequest,
    (isLoopTop, wasLoopTop) => {
      if (!imageAnimationProgress) return;
      if (isLoopTop && !wasLoopTop) {
        imageAnimationProgress.value = withTiming(0, {
          duration: ANIMATION_DURATION_SHORT,
          easing: EASING_OUT_CUBIC,
        });
      }
    },
    [currentIndex, index, isCurrentItemNewRequest],
  );
 
  // Re-show mascot when returning to a regular card after a loop card
  useAnimatedReaction(
    () => currentIndex === index && !isCurrentItemNewRequest,
    (isRegularTop, wasRegularTop) => {
      if (!imageAnimationProgress || wasDragging.value) return;
      Iif (isRegularTop && !wasRegularTop && imageAnimationProgress.value < 1) {
        imageAnimationProgress.value = withTiming(1, {
          duration: ANIMATION_DURATION_SHORT,
          easing: EASING_OUT_CUBIC,
        });
      }
    },
    [currentIndex, index, isCurrentItemNewRequest],
  );
};