/** * LottiePlayer Component * * Universal Lottie animation player component */ 'use client'; import React from 'react'; import Lottie from 'react-lottie-player'; import { LottiePlayerProps } from './types'; import { useLottie } from './useLottie'; import { usePrefersReducedMotion } from './usePrefersReducedMotion'; // Size presets mapping const SIZE_PRESETS = { xs: { width: 64, height: 64 }, sm: { width: 128, height: 128 }, md: { width: 256, height: 256 }, lg: { width: 384, height: 384 }, xl: { width: 512, height: 512 }, full: { width: '100%', height: '100%' }, } as const; const SPEED_STEPS = [0.5, 1, 1.5, 2] as const; /** * LottiePlayer component for displaying Lottie animations * * Features: * - Loads animations from URLs or objects * - Size presets or custom dimensions * - Interactive playback controls (play/pause, loop, speed) * - Segment playback and hover-to-play * - Loading and error states * - Respects `prefers-reduced-motion` * - Event callbacks * * Usage: * ```tsx * // From URL with size preset * * * // Interactive controls * * * // Play a frame range only * * * // Play on hover * * ``` */ export function LottiePlayer({ src, size = 'md', width, height, autoplay = true, loop = true, speed = 1, direction = 1, controls = false, segment, hoverToPlay = false, background, className, ariaLabel, respectReducedMotion = true, showLoading = true, onComplete, onLoad, onError, }: LottiePlayerProps) { // Load animation data using our custom hook const { animationData, isLoading, error, retry } = useLottie({ src }); const prefersReducedMotion = usePrefersReducedMotion(); const reduceMotion = respectReducedMotion && prefersReducedMotion; // Interactive playback state (only used when `controls` is enabled). const [isPlaying, setIsPlaying] = React.useState(autoplay); const [loopEnabled, setLoopEnabled] = React.useState(Boolean(loop)); const [currentSpeed, setCurrentSpeed] = React.useState(speed); const [hovered, setHovered] = React.useState(false); // Keep internal state in sync if controlling props change. React.useEffect(() => setIsPlaying(autoplay), [autoplay]); React.useEffect(() => setLoopEnabled(Boolean(loop)), [loop]); React.useEffect(() => setCurrentSpeed(speed), [speed]); // Notify parent about load state once per loaded animation. const notifiedLoadRef = React.useRef(null); React.useEffect(() => { if (animationData && notifiedLoadRef.current !== animationData) { notifiedLoadRef.current = animationData; onLoad?.(); } }, [animationData, onLoad]); // Notify parent about errors once per error instance. const notifiedErrorRef = React.useRef(null); React.useEffect(() => { if (error && notifiedErrorRef.current !== error) { notifiedErrorRef.current = error; onError?.(error); } if (!error) { notifiedErrorRef.current = null; } }, [error, onError]); // Determine dimensions const dimensions = React.useMemo(() => { // Custom dimensions override size preset if (width !== undefined || height !== undefined) { return { width: width ?? SIZE_PRESETS[size].width, height: height ?? SIZE_PRESETS[size].height, }; } return SIZE_PRESETS[size]; }, [size, width, height]); // Handle complete event const handleComplete = React.useCallback(() => { // When not looping, reflect the finished state in the controls. if (!loopEnabled) { setIsPlaying(false); } onComplete?.(); }, [loopEnabled, onComplete]); const togglePlay = React.useCallback(() => { setIsPlaying((prev) => !prev); }, []); const cycleSpeed = React.useCallback(() => { setCurrentSpeed((prev) => { const idx = SPEED_STEPS.indexOf(prev as (typeof SPEED_STEPS)[number]); return SPEED_STEPS[(idx + 1) % SPEED_STEPS.length]; }); }, []); // Resolve the effective play / loop / speed values. // Reduced motion forces a paused, non-looping static frame. const effectivePlaying = reduceMotion ? false : hoverToPlay ? hovered : controls ? isPlaying : autoplay; const effectiveLoop = reduceMotion ? false : controls ? loopEnabled : loop; const effectiveSpeed = controls ? currentSpeed : speed; // Segment playback: react-lottie-player expects [start, end]. const segments = React.useMemo( () => (segment ? ([segment[0], segment[1]] as [number, number]) : undefined), [segment] ); const containerStyle = React.useMemo( () => ({ width: dimensions.width, height: dimensions.height, background: background || 'transparent', }), [dimensions.width, dimensions.height, background] ); // Loading state if (isLoading && showLoading) { return ( Loading animation... ); } // Error state if (error) { return ( {error.message} Retry ); } // No animation data if (!animationData) { return null; } // Render the Lottie player return ( setHovered(true) : undefined} onMouseLeave={hoverToPlay ? () => setHovered(false) : undefined} > {controls && ( {effectivePlaying ? ( ) : ( )} setLoopEnabled((prev) => !prev)} aria-label={loopEnabled ? 'Disable loop' : 'Enable loop'} aria-pressed={loopEnabled} className={`inline-flex h-7 w-7 items-center justify-center rounded transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${ loopEnabled ? 'text-foreground' : 'text-muted-foreground' }`} > {currentSpeed}x {reduceMotion && ( Reduced motion )} )} ); }