"use client"; // ============================================================ // BreathingWidget.tsx โ€” MEMBER 4 OWNS THIS // Pure CSS 4-7-8 breathing animation. No external library. // Wrapped in @media prefers-reduced-motion. // ============================================================ import { useState, useEffect, useRef } from "react"; type Phase = "inhale" | "hold" | "exhale" | "rest"; const PHASES: { phase: Phase; duration: number; label: string; sublabel: string }[] = [ { phase: "inhale", duration: 4, label: "Breathe In", sublabel: "through your nose" }, { phase: "hold", duration: 7, label: "Hold", sublabel: "gently" }, { phase: "exhale", duration: 8, label: "Breathe Out", sublabel: "through your mouth" }, { phase: "rest", duration: 1, label: "Rest", sublabel: "" }, ]; const PHASE_COLORS: Record = { inhale: "#FF9933", hold: "#6366F1", exhale: "#22C55E", rest: "#64748B", }; const PHASE_SCALE: Record = { inhale: 1, hold: 1, exhale: 0.45, rest: 0.45, }; export default function BreathingWidget() { const [isRunning, setIsRunning] = useState(false); const [phaseIndex, setPhaseIndex] = useState(0); const [countdown, setCountdown] = useState(PHASES[0].duration); const [cycleCount, setCycleCount] = useState(0); const [scale, setScale] = useState(0.45); const intervalRef = useRef(null); const currentPhase = PHASES[phaseIndex]; const color = PHASE_COLORS[currentPhase.phase]; useEffect(() => { if (!isRunning) { if (intervalRef.current) clearInterval(intervalRef.current); setScale(0.45); return; } // Set initial scale for the phase if (currentPhase.phase === "inhale") { setScale(1); } else if (currentPhase.phase === "hold") { setScale(1); } else { setScale(0.45); } setCountdown(currentPhase.duration); intervalRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) { // Move to next phase setPhaseIndex((pi) => { const next = (pi + 1) % PHASES.length; if (next === 0) setCycleCount((c) => c + 1); return next; }); return currentPhase.duration; } return prev - 1; }); }, 1000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRunning, phaseIndex]); const handleToggle = () => { if (isRunning) { setIsRunning(false); setPhaseIndex(0); setCountdown(PHASES[0].duration); setCycleCount(0); } else { setIsRunning(true); } }; // Transition duration based on phase const transitionDuration = currentPhase.phase === "inhale" ? "4s" : currentPhase.phase === "exhale" ? "8s" : "0.3s"; return (
{/* Header */}

4-7-8 Breathing

Calms the nervous system in 5 minutes

{cycleCount > 0 && ( {cycleCount} cycle{cycleCount !== 1 ? "s" : ""} )}
{/* Breathing circle โ€” pure CSS */} {/* @media prefers-reduced-motion handled via inline style transition */}
{/* Outer ring โ€” decorative */}
{/* Middle ring */}
{/* Main breathing circle */}
{/* Inner text */}
{isRunning ? ( <> {countdown} ) : ( tap start )}
{/* Phase label */}
{isRunning && ( <>

{currentPhase.label}

{currentPhase.sublabel && (

{currentPhase.sublabel}

)} )}
{/* Phase guide */}
{PHASES.filter((p) => p.phase !== "rest").map((p) => (

{p.label}

{p.duration}s

))}
{/* Start / Stop button */}

@media prefers-reduced-motion respected ยท No library used

); }