import React, { useRef, useEffect, useState, useCallback } from 'react'; import { View, Animated, Text, StyleSheet } from 'react-native'; import { useTheme } from '../../theme/useTheme'; import type { ProgressProps, ProgressColor, ProgressSize } from './Progress.types'; const sizeMap: Record = { sm: { linear: 4, circular: 32 }, md: { linear: 8, circular: 48 }, lg: { linear: 12, circular: 64 }, }; const colorKeyMap: Record = { primary: 'primary', secondary: 'secondary', success: 'success', warning: 'warning', error: 'error', info: 'info', }; export const Progress: React.FC = ({ value, variant = 'linear', color = 'primary', size = 'md', showValue = false, animated = true, indeterminate = false, style, }) => { const theme = useTheme(); const [trackWidth, setTrackWidth] = useState(200); const animatedWidth = useRef(new Animated.Value(0)).current; const indeterminateTranslate = useRef(new Animated.Value(0)).current; const circularRotation = useRef(new Animated.Value(0)).current; const colorValue = theme.colors[colorKeyMap[color]]; const onTrackLayout = useCallback((e: { nativeEvent: { layout: { width: number } } }) => { setTrackWidth(e.nativeEvent.layout.width); }, []); const dimensions = sizeMap[size]; const clampedValue = Math.min(100, Math.max(0, value)); useEffect(() => { if (indeterminate) { if (variant === 'linear') { const animation = Animated.loop( Animated.sequence([ Animated.timing(indeterminateTranslate, { toValue: 1, duration: 800, useNativeDriver: true, }), Animated.timing(indeterminateTranslate, { toValue: 0, duration: 800, useNativeDriver: true, }), ]) ); animation.start(); return () => animation.stop(); } const animation = Animated.loop( Animated.timing(circularRotation, { toValue: 1, duration: 1000, useNativeDriver: true, }) ); animation.start(); return () => animation.stop(); } if (animated) { Animated.timing(animatedWidth, { toValue: clampedValue, duration: 300, useNativeDriver: false, }).start(); } else { animatedWidth.setValue(clampedValue); } return undefined; }, [clampedValue, indeterminate, variant, animated, animatedWidth, indeterminateTranslate, circularRotation]); if (variant === 'linear') { const fillWidth = Math.max(trackWidth * 0.3, 20); const maxTranslate = Math.max(0, trackWidth - fillWidth); const indeterminateTranslateX = indeterminateTranslate.interpolate({ inputRange: [0, 1], outputRange: [0, maxTranslate], }); return ( {indeterminate ? ( ) : ( )} {showValue && !indeterminate && ( {Math.round(clampedValue)}% )} ); } // Circular variant const rotation = indeterminate ? circularRotation.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'], }) : `${(clampedValue / 100) * 360}deg`; return ( {showValue && !indeterminate && ( {Math.round(clampedValue)}% )} ); }; const styles = StyleSheet.create({ linearWrapper: { flexDirection: 'row', alignItems: 'center', }, track: { flex: 1, overflow: 'hidden', }, fill: { position: 'absolute', left: 0, top: 0, }, valueText: {}, circularWrapper: { alignItems: 'center', justifyContent: 'center', }, circularTrack: { position: 'absolute', }, circularProgress: { position: 'absolute', }, circularValueWrapper: { alignItems: 'center', justifyContent: 'center', }, circularValueText: { textAlign: 'center', }, });