| import React, { useEffect, useState } from 'react'; | |
| import { StyleSheet, View, Dimensions } from 'react-native'; | |
| import Animated, { | |
| useSharedValue, | |
| useAnimatedStyle, | |
| withTiming, | |
| withSequence, | |
| withDelay, | |
| withSpring, | |
| interpolate, | |
| Easing, | |
| runOnJS, | |
| } from 'react-native-reanimated'; | |
| import { COLORS } from '../constants/colors'; | |
| const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); | |
| interface CountdownOverlayProps { | |
| onComplete: () => void; | |
| } | |
| export const CountdownOverlay: React.FC<CountdownOverlayProps> = ({ onComplete }) => { | |
| const [currentNumber, setCurrentNumber] = useState(3); | |
| const [showGo, setShowGo] = useState(false); | |
| const [visible, setVisible] = useState(true); | |
| const numberScale = useSharedValue(0); | |
| const numberOpacity = useSharedValue(0); | |
| const ringScale = useSharedValue(0); | |
| const bgOpacity = useSharedValue(1); | |
| const animateNumber = (num: number) => { | |
| numberScale.value = 0.3; | |
| numberOpacity.value = 0; | |
| ringScale.value = 0.5; | |
| numberScale.value = withSequence( | |
| withSpring(1, { damping: 8, stiffness: 150 }), | |
| withDelay(500, withTiming(1.5, { duration: 200 })) | |
| ); | |
| numberOpacity.value = withSequence( | |
| withTiming(1, { duration: 150 }), | |
| withDelay(500, withTiming(0, { duration: 200 })) | |
| ); | |
| ringScale.value = withSequence( | |
| withSpring(1, { damping: 8, stiffness: 120 }), | |
| withDelay(400, withTiming(1.8, { duration: 300 })) | |
| ); | |
| }; | |
| useEffect(() => { | |
| animateNumber(3); | |
| const timers = [ | |
| setTimeout(() => { setCurrentNumber(2); animateNumber(2); }, 900), | |
| setTimeout(() => { setCurrentNumber(1); animateNumber(1); }, 1800), | |
| setTimeout(() => { | |
| setShowGo(true); | |
| numberScale.value = 0.3; | |
| numberOpacity.value = 0; | |
| numberScale.value = withSpring(1.2, { damping: 6, stiffness: 100 }); | |
| numberOpacity.value = withTiming(1, { duration: 150 }); | |
| }, 2700), | |
| setTimeout(() => { | |
| bgOpacity.value = withTiming(0, { duration: 300 }); | |
| setTimeout(() => { | |
| setVisible(false); | |
| onComplete(); | |
| }, 300); | |
| }, 3300), | |
| ]; | |
| return () => timers.forEach(clearTimeout); | |
| }, []); | |
| const numberStyle = useAnimatedStyle(() => ({ | |
| transform: [{ scale: numberScale.value }], | |
| opacity: numberOpacity.value, | |
| })); | |
| const ringStyle = useAnimatedStyle(() => ({ | |
| transform: [{ scale: ringScale.value }], | |
| opacity: interpolate(ringScale.value, [0.5, 1, 1.8], [0.3, 0.5, 0]), | |
| })); | |
| const containerStyle = useAnimatedStyle(() => ({ | |
| opacity: bgOpacity.value, | |
| })); | |
| if (!visible) return null; | |
| return ( | |
| <Animated.View style={[styles.overlay, containerStyle]}> | |
| {/* Pulse Ring */} | |
| <Animated.View style={[styles.ring, ringStyle]} /> | |
| {/* Number / GO */} | |
| <Animated.Text | |
| style={[ | |
| styles.number, | |
| showGo && styles.goText, | |
| numberStyle, | |
| ]} | |
| > | |
| {showGo ? 'GO!' : currentNumber} | |
| </Animated.Text> | |
| </Animated.View> | |
| ); | |
| }; | |
| const styles = StyleSheet.create({ | |
| overlay: { | |
| ...StyleSheet.absoluteFillObject, | |
| backgroundColor: 'rgba(10, 10, 11, 0.95)', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| zIndex: 999, | |
| }, | |
| ring: { | |
| position: 'absolute', | |
| width: 150, | |
| height: 150, | |
| borderRadius: 75, | |
| borderWidth: 3, | |
| borderColor: COLORS.primary, | |
| }, | |
| number: { | |
| fontSize: 80, | |
| fontWeight: '900', | |
| color: COLORS.text, | |
| textShadowColor: COLORS.primary + '60', | |
| textShadowOffset: { width: 0, height: 0 }, | |
| textShadowRadius: 30, | |
| fontVariant: ['tabular-nums'], | |
| }, | |
| goText: { | |
| color: COLORS.primary, | |
| fontSize: 64, | |
| letterSpacing: 8, | |
| }, | |
| }); | |