'use client'; import { ReactNode, useRef } from 'react'; import { Variant, motion } from 'framer-motion'; import { useAppState } from '@/app/AppState'; import usePrefersReducedMotion from '@/utility/usePrefersReducedMotion'; const IGNORE_CAN_START = true; export type AnimationType = 'none' | 'scale' | 'left' | 'right' | 'bottom'; export interface AnimationConfig { type?: AnimationType duration?: number staggerDelay?: number scaleOffset?: number distanceOffset?: number } interface Props extends AnimationConfig { className?: string classNameItem?: string items: ReactNode[] itemKeys?: string[] canStart?: boolean animateFromAppState?: boolean animateOnFirstLoadOnly?: boolean staggerOnFirstLoadOnly?: boolean onAnimationComplete?: () => void } function AnimateItems({ className, classNameItem, items, itemKeys, canStart = true, type = 'scale', duration = 0.6, staggerDelay = 0.1, scaleOffset = 0.9, distanceOffset = 20, animateFromAppState, animateOnFirstLoadOnly, staggerOnFirstLoadOnly, onAnimationComplete, }: Props) { const { hasLoaded, nextPhotoAnimation, getNextPhotoAnimationId, clearNextPhotoAnimation, } = useAppState(); const nextPhotoAnimationId = useRef(undefined); const prefersReducedMotion = usePrefersReducedMotion(); const hasLoadedInitial = useRef(hasLoaded); const nextPhotoAnimationInitial = useRef(nextPhotoAnimation); const shouldAnimate = type !== 'none' && !prefersReducedMotion && !(animateOnFirstLoadOnly && hasLoadedInitial.current); const shouldStagger = !(staggerOnFirstLoadOnly && hasLoadedInitial.current); const typeResolved = animateFromAppState ? (nextPhotoAnimationInitial.current?.type ?? type) : type; const durationResolved = animateFromAppState ? (nextPhotoAnimationInitial.current?.duration ?? duration) : duration; const getInitialVariant = (): Variant => { switch (typeResolved) { case 'left': return { opacity: 0, transform: `translateX(${distanceOffset}px)`, }; case 'right': return { opacity: 0, transform: `translateX(${-distanceOffset}px)`, }; case 'bottom': return { opacity: 0, transform: `translateY(${distanceOffset}px)`, }; default: return { opacity: 0, transform: `translateY(${distanceOffset}px) scale(${scaleOffset})`, }; } }; return ( { nextPhotoAnimationId.current = getNextPhotoAnimationId?.(); }} onAnimationComplete={() => { if (animateFromAppState) { clearNextPhotoAnimation?.(nextPhotoAnimationId.current); } onAnimationComplete?.(); }} > {items.map((item, index) => {item} )} ); }; export default AnimateItems;