// 4-7-8 Atemübung: Card (in-chat) + Drawer (bottom sheet). import { useEffect, useRef, useState } from 'react'; import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native'; import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants'; import { useColors } from '../../lib/theme'; type Props = { onDone: () => void; onSpeak?: (text: string) => Promise | void }; export function BreathingCard({ onDone, onSpeak }: Props) { const colors = useColors(); const [breathState, setBreathState] = useState('idle'); const [countdown, setCountdown] = useState(3); const [round, setRound] = useState(1); const [phaseIndex, setPhaseIndex] = useState(0); const [count, setCount] = useState(BREATH_PHASES[0]!.duration); const pulse = useRef(new Animated.Value(1)).current; const timerRef = useRef | null>(null); const animRef = useRef(null); const currentPhase = BREATH_PHASES[phaseIndex]!; function runPulse(target: number, seconds: number) { animRef.current?.stop(); animRef.current = Animated.timing(pulse, { toValue: target, duration: seconds * 1000, useNativeDriver: true }); animRef.current.start(); } // Countdown: visually 3 → 2 → 1 → 0 ("Los!"), then transition to active useEffect(() => { if (breathState !== 'countdown') return; if (countdown > 0) { const t = setTimeout(() => setCountdown((c) => c - 1), 1000); return () => clearTimeout(t); } else { // Kein "Los!" sprechen — würde laufende Lyra-Antwort abbrechen const t = setTimeout(() => setBreathState('active'), 500); return () => clearTimeout(t); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [breathState, countdown]); // Breathing phases with TTS guidance useEffect(() => { if (breathState !== 'active') return; let remaining = currentPhase.duration; setCount(remaining); runPulse(currentPhase.phase === 'exhale' ? 1 : 1.22, currentPhase.duration); // Speak only if speakLine defined — short words avoid overlap (inhale=4s, exhale=8s) let speakTimer: ReturnType | null = null; if (currentPhase.speakLine) { speakTimer = setTimeout(() => onSpeak?.(currentPhase.speakLine!), 350); } timerRef.current = setInterval(() => { remaining -= 1; setCount(remaining); if (remaining <= 0) { clearInterval(timerRef.current!); const next = phaseIndex + 1; if (next >= BREATH_PHASES.length) { if (round >= TOTAL_ROUNDS) { // Lob ZUERST komplett ausspielen, DANN onDone (das triggert Lyras nächste Frage) (async () => { try { await onSpeak?.('Sehr gut! Du hast alle drei Runden geschafft. Wunderbar gemacht!'); } catch {} onDone(); })(); return; } // Nächste Runde still starten setRound((r) => r + 1); setPhaseIndex(0); } else { setPhaseIndex(next); } } }, 1000); return () => { if (timerRef.current) clearInterval(timerRef.current); if (speakTimer) clearTimeout(speakTimer); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [breathState, phaseIndex, round]); return ( {breathState === 'idle' ? ( 4-7-8 Atemübung 3 Runden · beruhigt dein Nervensystem { setCountdown(3); setBreathState('countdown'); }}> Starten ) : breathState === 'countdown' ? ( Gleich geht's los... {countdown > 0 ? countdown : '✓'} ) : ( Runde {round} / {TOTAL_ROUNDS} {count} {currentPhase.label} )} ); } // ── BreathingDrawer (bottom sheet, covers input, slides up) ─────────────────── export function BreathingDrawer({ onDone, onSpeak }: Props) { const colors = useColors(); const slideAnim = useRef(new Animated.Value(500)).current; useEffect(() => { Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start(); }, []); return ( <> ); } const st = StyleSheet.create({ breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 }, breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 }, breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 }, breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 }, breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 }, breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 }, breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }, breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' }, breathStartBtn: { borderRadius: 12, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 }, breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 }, breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' }, breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 }, });