import { useColor } from '@/hooks/useColor'; import React, { useEffect, useMemo } from 'react'; import { StyleSheet, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { cancelAnimation, interpolate, runOnJS, useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming, } from 'react-native-reanimated'; export interface AudioWaveformProps { data?: number[]; isPlaying?: boolean; progress?: number; onSeek?: (position: number) => void; onSeekStart?: () => void; onSeekEnd?: () => void; style?: ViewStyle; height?: number; barCount?: number; barWidth?: number; barGap?: number; activeColor?: string; inactiveColor?: string; animated?: boolean; showProgress?: boolean; interactive?: boolean; } // FIX: The Bar component now manages its own animation state. const Bar = React.memo( ({ value, height, width, isActive, showProgress, activeColor, inactiveColor, opacity, isPlaying, animated, }: { value: number; height: number; width: number; isActive: boolean; showProgress: boolean; activeColor: string; inactiveColor: string; opacity: number; isPlaying: boolean; animated: boolean; }) => { // Each Bar has its own shared value, created at the top level. This is correct. const animatedValue = useSharedValue(value); const animatedStyle = useAnimatedStyle(() => { return { height: interpolate( animatedValue.value, [0, 1], [4, height * 0.9], 'clamp' ), }; }); // This effect handles animations when the bar's data or playing state changes. useEffect(() => { if (isPlaying && animated && !showProgress) { // "Live" animation effect const randomDuration = 200 + Math.random() * 200; animatedValue.value = withRepeat( withSequence( withTiming(value * (0.9 + Math.random() * 0.2), { duration: randomDuration, }), withTiming(value * (0.9 + Math.random() * 0.2), { duration: randomDuration, }) ), -1, true ); } else { // Animate to the new static value cancelAnimation(animatedValue); animatedValue.value = withTiming(value, { duration: animated ? 250 : 0, }); } return () => { cancelAnimation(animatedValue); }; }, [value, isPlaying, animated, showProgress, animatedValue]); return ( ); } ); export function AudioWaveform({ data, isPlaying = false, progress = 0, onSeek, onSeekStart, onSeekEnd, style, height = 60, barCount = 50, barWidth = 3, barGap = 2, activeColor, inactiveColor, animated = true, showProgress = false, interactive = false, }: AudioWaveformProps) { const primaryColor = useColor('destructive'); const mutedColor = useColor('textMuted'); const finalActiveColor = activeColor || primaryColor; const finalInactiveColor = inactiveColor || mutedColor; // FIX: This now just memoizes the raw data array, not an array of hooks. const waveformData = useMemo( () => data || generateSampleWaveform(barCount), [data, barCount] ); const totalWidth = barCount * barWidth + (barCount - 1) * barGap; const getProgressLinePosition = () => { const progressRatio = Math.max(0, Math.min(100, progress)) / 100; if (progressRatio === 0) return 0; if (progressRatio === 1) return totalWidth - 1; const exactBarPosition = progressRatio * barCount; const barIndex = Math.floor(exactBarPosition); const barProgress = exactBarPosition - barIndex; let position = barIndex * (barWidth + barGap); position += barProgress * barWidth; return Math.min(position, totalWidth - 1); }; const handleSeek = (x: number) => { if (!onSeek) return; const clampedX = Math.max(0, Math.min(totalWidth, x)); const seekPercentage = (clampedX / totalWidth) * 100; onSeek(seekPercentage); }; const panGesture = Gesture.Pan() .enabled(interactive) .onStart((event) => { if (onSeekStart) runOnJS(onSeekStart)(); runOnJS(handleSeek)(event.x); }) .onUpdate((event) => { runOnJS(handleSeek)(event.x); }) .onEnd(() => { if (onSeekEnd) runOnJS(onSeekEnd)(); }); return ( {/* FIX: We now map over the data array and pass the value to each Bar */} {waveformData.map((value, index) => { const progressRatio = progress / 100; const barProgress = (index + 0.5) / barCount; const isActive = showProgress ? barProgress <= progressRatio : false; let opacity = 1; if (showProgress && barProgress > progressRatio) { const distanceFromProgress = barProgress - progressRatio; opacity = Math.max(0.3, 1 - distanceFromProgress * 2); } return ( ); })} {showProgress && ( )} ); } // Helper function remains the same function generateSampleWaveform(barCount: number): number[] { return Array.from({ length: barCount }, (_, i) => { const wave1 = Math.sin((i / barCount) * Math.PI * 4) * 0.3; const wave2 = Math.sin((i / barCount) * Math.PI * 8) * 0.15; const wave3 = Math.sin((i / barCount) * Math.PI * 2) * 0.2; const noise = (Math.random() - 0.5) * 0.2; const base = 0.4; const peak = Math.random() < 0.1 ? Math.random() * 0.3 : 0; return Math.max( 0.1, Math.min(0.95, base + wave1 + wave2 + wave3 + noise + peak) ); }); } const styles = StyleSheet.create({ container: { justifyContent: 'center', alignItems: 'center', position: 'relative', }, waveform: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', position: 'relative', }, barContainer: { justifyContent: 'center', alignItems: 'center', height: '100%', }, bar: { borderRadius: 1.5, minHeight: 4, }, progressLine: { position: 'absolute', width: 2, borderRadius: 1, opacity: 0.9, top: '2.5%', zIndex: 10, }, });