diff --git a/components/VoiceFAB.tsx b/components/VoiceFAB.tsx new file mode 100644 index 0000000..a4b7667 --- /dev/null +++ b/components/VoiceFAB.tsx @@ -0,0 +1,144 @@ +/** + * Voice Floating Action Button Component + * + * A floating action button for initiating voice calls with Julia AI. + * Shows on screens where voice chat is available. + * Hidden when a call is already active. + */ + +import React, { useRef, useEffect } from 'react'; +import { + StyleSheet, + TouchableOpacity, + Animated, + ViewStyle, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { AppColors, BorderRadius } from '@/constants/theme'; +import { useVoiceCall } from '@/contexts/VoiceCallContext'; + +interface VoiceFABProps { + onPress: () => void; + style?: ViewStyle; + disabled?: boolean; +} + +const FAB_SIZE = 56; + +export function VoiceFAB({ onPress, style, disabled = false }: VoiceFABProps) { + const { isCallActive } = useVoiceCall(); + const insets = useSafeAreaInsets(); + + // Animation values + const scale = useRef(new Animated.Value(1)).current; + const opacity = useRef(new Animated.Value(1)).current; + + // Hide FAB when call is active + useEffect(() => { + if (isCallActive) { + Animated.parallel([ + Animated.timing(scale, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.spring(scale, { + toValue: 1, + friction: 5, + tension: 40, + useNativeDriver: true, + }), + Animated.timing(opacity, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + } + }, [isCallActive, scale, opacity]); + + // Press animation + const handlePressIn = () => { + Animated.spring(scale, { + toValue: 0.9, + friction: 5, + useNativeDriver: true, + }).start(); + }; + + const handlePressOut = () => { + Animated.spring(scale, { + toValue: 1, + friction: 5, + useNativeDriver: true, + }).start(); + }; + + // Don't render if call is active + if (isCallActive) { + return null; + } + + return ( + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + right: 16, + zIndex: 100, + }, + fab: { + width: FAB_SIZE, + height: FAB_SIZE, + borderRadius: BorderRadius.full, + backgroundColor: AppColors.success, + justifyContent: 'center', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, + }, + fabDisabled: { + backgroundColor: AppColors.surface, + shadowOpacity: 0.1, + }, +});