diff --git a/src/components/tools/about.astro b/src/components/tools/about.astro
new file mode 100644
index 0000000..04fb39c
--- /dev/null
+++ b/src/components/tools/about.astro
@@ -0,0 +1,33 @@
+---
+import { Container } from '../container';
+
+interface Props {
+ text: string;
+}
+
+const { text } = Astro.props;
+---
+
+
+
+ About this tool
+ {text}
+
+
+
+
diff --git a/src/components/tools/breathing/breathing.module.css b/src/components/tools/breathing/breathing.module.css
new file mode 100644
index 0000000..05b9baa
--- /dev/null
+++ b/src/components/tools/breathing/breathing.module.css
@@ -0,0 +1,47 @@
+.exercise {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 75px 0;
+ margin-top: 12px;
+ background-color: var(--color-neutral-50);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 8px;
+
+ & .phase {
+ font-family: var(--font-display);
+ font-size: var(--font-lg);
+ font-weight: 600;
+ }
+
+ & .circle {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: -1;
+ height: 55%;
+ aspect-ratio: 1 / 1;
+ background-image: radial-gradient(
+ var(--color-neutral-50),
+ var(--color-neutral-100)
+ );
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
+
+.selectBox {
+ width: 100%;
+ min-width: 0;
+ height: 45px;
+ padding: 0 12px;
+ margin-top: 8px;
+ font-size: var(--font-sm);
+ color: var(--color-foreground);
+ background-color: var(--color-neutral-100);
+ border: 1px solid var(--color-neutral-200);
+ border-radius: 8px;
+}
diff --git a/src/components/tools/breathing/breathing.tsx b/src/components/tools/breathing/breathing.tsx
new file mode 100644
index 0000000..4480bc1
--- /dev/null
+++ b/src/components/tools/breathing/breathing.tsx
@@ -0,0 +1,124 @@
+import { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+
+import { Container } from '@/components/container';
+
+import styles from './breathing.module.css';
+
+type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
+type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
+
+export function Breathing() {
+ const [selectedExercise, setSelectedExercise] =
+ useState('4-7-8 Breathing');
+
+ const getAnimationPhases = (
+ exercise: Exercise,
+ ): Array<'inhale' | 'holdInhale' | 'exhale' | 'holdExhale'> => {
+ switch (exercise) {
+ case 'Box Breathing':
+ return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
+ case 'Resonant Breathing':
+ return ['inhale', 'exhale'];
+ case '4-7-8 Breathing':
+ return ['inhale', 'holdInhale', 'exhale'];
+ default:
+ return ['inhale', 'holdInhale', 'exhale', 'holdExhale'];
+ }
+ };
+
+ const getAnimationDurations = (exercise: Exercise) => {
+ switch (exercise) {
+ case 'Box Breathing':
+ return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
+ case 'Resonant Breathing':
+ return { exhale: 5, inhale: 5 };
+ case '4-7-8 Breathing':
+ return { exhale: 8, holdInhale: 7, inhale: 4 };
+ default:
+ return { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 };
+ }
+ };
+
+ const getLabel = (phase: Phase) => {
+ switch (phase) {
+ case 'inhale':
+ return 'Inhale';
+ case 'exhale':
+ return 'Exhale';
+ default:
+ return 'Hold';
+ }
+ };
+
+ const [phase, setPhase] = useState('inhale');
+ const [durations, setDurations] = useState(
+ getAnimationDurations(selectedExercise),
+ );
+
+ const animationVariants = {
+ exhale: {
+ transform: 'translate(-50%, -50%) scale(1)',
+ transition: { duration: durations.exhale },
+ },
+ holdExhale: {
+ transform: 'translate(-50%, -50%) scale(1)',
+ transition: { duration: durations.holdExhale || 4 },
+ },
+ holdInhale: {
+ transform: 'translate(-50%, -50%) scale(1.5)',
+ transition: { duration: durations.holdInhale || 4 },
+ },
+ inhale: {
+ transform: 'translate(-50%, -50%) scale(1.5)',
+ transition: { duration: durations.inhale },
+ },
+ };
+
+ useEffect(() => {
+ setDurations(getAnimationDurations(selectedExercise));
+ }, [selectedExercise]);
+
+ useEffect(() => {
+ const phases = getAnimationPhases(selectedExercise);
+
+ let phaseIndex = 0;
+
+ setPhase(phases[phaseIndex]);
+
+ const interval = setInterval(
+ () => {
+ phaseIndex = (phaseIndex + 1) % phases.length;
+
+ setPhase(phases[phaseIndex]);
+ },
+ (durations[phases[phaseIndex]] || 4) * 1000,
+ );
+
+ return () => clearInterval(interval);
+ }, [selectedExercise, durations]);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/tools/breathing/index.ts b/src/components/tools/breathing/index.ts
new file mode 100644
index 0000000..7049d47
--- /dev/null
+++ b/src/components/tools/breathing/index.ts
@@ -0,0 +1 @@
+export { Breathing } from './breathing';
diff --git a/src/components/tools/hero.astro b/src/components/tools/hero.astro
new file mode 100644
index 0000000..6d7bd7c
--- /dev/null
+++ b/src/components/tools/hero.astro
@@ -0,0 +1,55 @@
+---
+import { Container } from '../container';
+
+interface Props {
+ desc: string;
+ title: string;
+}
+
+const { desc, title } = Astro.props;
+---
+
+
+
+
+ {title}
+ {desc}
+ Part of Moodist
+
+
+
+
diff --git a/src/pages/tools/breathing-exercises.astro b/src/pages/tools/breathing-exercises.astro
new file mode 100644
index 0000000..965a0f7
--- /dev/null
+++ b/src/pages/tools/breathing-exercises.astro
@@ -0,0 +1,19 @@
+---
+import Layout from '@/layouts/layout.astro';
+
+import Donate from '@/components/donate.astro';
+import Hero from '@/components/tools/hero.astro';
+import Footer from '@/components/footer.astro';
+import About from '@/components/tools/about.astro';
+import { Breathing } from '@/components/tools/breathing';
+---
+
+
+
+
+
+
+
+