import { Text } from '@/components/ui/text'; import { useColor } from '@/hooks/useColor'; import { BORDER_RADIUS, CORNERS, FONT_SIZE } from '@/theme/globals'; import { Loader2 } from 'lucide-react-native'; import React, { useEffect, useMemo } from 'react'; import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native'; import Animated, { Easing, SharedValue, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming, } from 'react-native-reanimated'; // Types type SpinnerSize = 'default' | 'sm' | 'lg' | 'icon'; export type SpinnerVariant = 'default' | 'circle' | 'dots' | 'pulse' | 'bars'; interface SpinnerProps { size?: SpinnerSize; variant?: SpinnerVariant; label?: string; showLabel?: boolean; style?: ViewStyle; color?: string; thickness?: number; // Note: thickness is not used in the original component logic speed?: 'slow' | 'normal' | 'fast'; } interface LoadingOverlayProps extends SpinnerProps { visible: boolean; backdrop?: boolean; backdropColor?: string; backdropOpacity?: number; onRequestClose?: () => void; } interface SpinnerConfig { size: number; iconSize: number; fontSize: number; gap: number; thickness: number; } // Configuration const sizeConfig: Record = { sm: { size: 16, iconSize: 16, fontSize: 12, gap: 6, thickness: 2 }, default: { size: 24, iconSize: 24, fontSize: FONT_SIZE, gap: 8, thickness: 2, }, lg: { size: 32, iconSize: 32, fontSize: 16, gap: 10, thickness: 3 }, icon: { size: 24, iconSize: 24, fontSize: FONT_SIZE, gap: 8, thickness: 2 }, }; const speedConfig = { slow: 1500, normal: 1000, fast: 500, }; // --- Helper Animated Components for Dots and Bars --- interface AnimatedShapeProps { anim: SharedValue; color: string; size: number; style: ViewStyle; } const AnimatedDot = React.memo( ({ anim, color, size, style }: AnimatedShapeProps) => { const animatedStyle = useAnimatedStyle(() => ({ opacity: anim.value, })); return ( ); } ); const AnimatedBar = React.memo( ({ anim, color, size, style }: AnimatedShapeProps) => { const animatedStyle = useAnimatedStyle(() => ({ opacity: anim.value, })); return ( ); } ); // Main Spinner Component export function Spinner({ size = 'default', variant = 'default', label, showLabel = false, style, color, speed = 'normal', }: SpinnerProps) { // Reanimated shared values const rotate = useSharedValue(0); const pulse = useSharedValue(1); // --- FIX: Call hooks at the top level --- // 1. Call useSharedValue at the top level for each dot/bar const dotAnim1 = useSharedValue(0.3); const dotAnim2 = useSharedValue(0.3); const dotAnim3 = useSharedValue(0.3); const barAnim1 = useSharedValue(0.3); const barAnim2 = useSharedValue(0.3); const barAnim3 = useSharedValue(0.3); const barAnim4 = useSharedValue(0.3); // 2. Use useMemo to create a stable array reference from the values const dotsAnims = useMemo( () => [dotAnim1, dotAnim2, dotAnim3], [dotAnim1, dotAnim2, dotAnim3] ); const barsAnims = useMemo( () => [barAnim1, barAnim2, barAnim3, barAnim4], [barAnim1, barAnim2, barAnim3, barAnim4] ); // --- END FIX --- // Theme colors const primaryColor = useColor('text'); const textColor = useColor('text'); const config = sizeConfig[size]; const spinnerColor = color || primaryColor; const animationDuration = speedConfig[speed]; // Rotation animation useEffect(() => { if (variant === 'circle') { rotate.value = withRepeat( withTiming(360, { duration: animationDuration, easing: Easing.linear }), -1 ); } else { rotate.value = 0; // Reset } }, [rotate, variant, animationDuration]); // Pulse animation useEffect(() => { if (variant === 'pulse') { pulse.value = withRepeat( withSequence( withTiming(1.3, { duration: animationDuration / 2 }), withTiming(1, { duration: animationDuration / 2 }) ), -1, true ); } else { pulse.value = 1; // Reset } }, [pulse, variant, animationDuration]); // Dots animation useEffect(() => { if (variant === 'dots') { dotsAnims.forEach((anim, index) => { anim.value = withRepeat( withSequence( withDelay( index * (animationDuration / 6), withTiming(1, { duration: animationDuration / 3 }) ), withTiming(0.3, { duration: animationDuration / 3 }) ), -1 ); }); } else { dotsAnims.forEach((anim) => (anim.value = 0.3)); // Reset } }, [dotsAnims, variant, animationDuration]); // Bars animation useEffect(() => { if (variant === 'bars') { barsAnims.forEach((anim, index) => { anim.value = withRepeat( withSequence( withDelay( index * (animationDuration / 8), withTiming(1, { duration: animationDuration / 4 }) ), withTiming(0.3, { duration: animationDuration / 4 }) ), -1 ); }); } else { barsAnims.forEach((anim) => (anim.value = 0.3)); // Reset } }, [barsAnims, variant, animationDuration]); // Animated styles const animatedCircleStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${rotate.value}deg` }], })); const animatedPulseStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulse.value }], })); const renderSpinner = () => { switch (variant) { case 'default': return ( ); case 'circle': return ( ); case 'pulse': return ( ); case 'dots': return ( {dotsAnims.map((anim, index) => ( ))} ); case 'bars': return ( {barsAnims.map((anim, index) => ( ))} ); default: return null; } }; const containerStyle: ViewStyle = { alignItems: 'center', justifyContent: 'center', gap: config.gap, }; return ( {renderSpinner()} {(showLabel || label) && ( {label || 'Loading...'} )} ); } // Loading Overlay Component export function LoadingOverlay({ visible, backdrop = true, backdropColor, backdropOpacity = 0.5, ...spinnerProps }: LoadingOverlayProps) { const opacity = useSharedValue(0); const backgroundColor = useColor('background'); const cardColor = useColor('card'); useEffect(() => { opacity.value = withTiming(visible ? 1 : 0, { duration: 200, }); }, [visible, opacity]); const animatedOverlayStyle = useAnimatedStyle(() => ({ opacity: opacity.value, // Conditionally render to avoid interaction issues display: opacity.value === 0 ? 'none' : 'flex', })); const defaultBackdropColor = backdropColor || `${backgroundColor}${Math.round(backdropOpacity * 255) .toString(16) .padStart(2, '0')}`; return ( ); } // Inline Loader Component (for buttons, etc.) export function InlineLoader({ size = 'sm', variant = 'default', color, }: Omit) { return ( ); } // Button Spinner Component - optimized for button usage export function ButtonSpinner({ size = 'sm', variant = 'default', color, }: Omit) { const primaryForegroundColor = useColor('primaryForeground'); return ( ); } const styles = StyleSheet.create({ spinner: { alignSelf: 'center', }, customSpinner: { alignItems: 'center', justifyContent: 'center', }, pulseSpinner: { borderRadius: 999, }, dotsContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, dot: { borderRadius: 999, }, barsContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, bar: { borderRadius: CORNERS, }, label: { textAlign: 'center', fontWeight: '500', }, overlay: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', zIndex: 9999, }, overlayContent: { padding: 60, borderRadius: BORDER_RADIUS, }, inlineLoader: { minHeight: 0, minWidth: 0, }, buttonSpinner: { minHeight: 0, minWidth: 0, }, });