import { View } from '@/components/ui/view'; import { useColor } from '@/hooks/useColor'; import { HEIGHT } from '@/theme/globals'; import React, { useEffect } from 'react'; import { ViewStyle } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; interface ProgressProps { value: number; // 0-100 style?: ViewStyle; height?: number; onValueChange?: (value: number) => void; onSeekStart?: () => void; onSeekEnd?: () => void; interactive?: boolean; } export function Progress({ value, style, height = HEIGHT, onValueChange, onSeekStart, onSeekEnd, interactive = false, }: ProgressProps) { const primaryColor = useColor('primary'); const mutedColor = useColor('muted'); const clampedValue = Math.max(0, Math.min(100, value)); const progressWidth = useSharedValue(clampedValue); const containerWidth = useSharedValue(200); // Default width, will be updated const isDragging = useSharedValue(false); // Update animation when value prop changes (only if not dragging) useEffect(() => { if (!isDragging.value) { progressWidth.value = withTiming(clampedValue, { duration: 300 }); } }, [clampedValue]); const updateValue = (newValue: number) => { const clamped = Math.max(0, Math.min(100, newValue)); onValueChange?.(clamped); }; const handleSeekStart = () => { isDragging.value = true; onSeekStart?.(); }; const handleSeekEnd = () => { isDragging.value = false; onSeekEnd?.(); }; // Create pan gesture using the new Gesture API const panGesture = Gesture.Pan() .onStart(() => { if (!interactive) return; runOnJS(handleSeekStart)(); }) .onUpdate((event) => { if (!interactive) return; // Calculate new progress based on gesture position const newProgress = (event.x / containerWidth.value) * 100; const clampedProgress = Math.max(0, Math.min(100, newProgress)); progressWidth.value = clampedProgress; runOnJS(updateValue)(clampedProgress); }) .onEnd(() => { if (!interactive) return; runOnJS(handleSeekEnd)(); }); // Create tap gesture for direct seeking const tapGesture = Gesture.Tap().onStart((event) => { if (!interactive) return; runOnJS(handleSeekStart)(); // Calculate progress based on tap position const newProgress = (event.x / containerWidth.value) * 100; const clampedProgress = Math.max(0, Math.min(100, newProgress)); progressWidth.value = withTiming(clampedProgress, { duration: 200 }); runOnJS(updateValue)(clampedProgress); setTimeout(() => { runOnJS(handleSeekEnd)(); }, 200); }); // Combine gestures const combinedGesture = Gesture.Race(panGesture, tapGesture); const animatedProgressStyle = useAnimatedStyle(() => { return { width: `${progressWidth.value}%`, }; }); const containerStyle: ViewStyle[] = [ { height: height, width: '100%' as const, backgroundColor: mutedColor, borderRadius: height / 2, overflow: 'hidden' as const, }, ...(style ? [style] : []), ]; const onLayout = (event: any) => { containerWidth.value = event.nativeEvent.layout.width; }; if (interactive) { return ( ); } return ( ); }