diff --git a/src/components/menu/items/breathing-exercise.tsx b/src/components/menu/items/breathing-exercise.tsx
new file mode 100644
index 0000000..6d5dce4
--- /dev/null
+++ b/src/components/menu/items/breathing-exercise.tsx
@@ -0,0 +1,18 @@
+import { MdOutlineTimer } from 'react-icons/md/index';
+
+import { Item } from '../item';
+
+interface BreathingExerciseProps {
+ open: () => void;
+}
+
+export function BreathingExercise({ open }: BreathingExerciseProps) {
+ return (
+ }
+ label="Breathing Exercise"
+ shortcut="Shift + B"
+ onClick={open}
+ />
+ );
+}
diff --git a/src/components/menu/items/index.ts b/src/components/menu/items/index.ts
index e5c2904..08aa347 100644
--- a/src/components/menu/items/index.ts
+++ b/src/components/menu/items/index.ts
@@ -8,3 +8,4 @@ export { Presets as PresetsItem } from './presets';
export { Shortcuts as ShortcutsItem } from './shortcuts';
export { SleepTimer as SleepTimerItem } from './sleep-timer';
export { CountdownTimer as CountdownTimerItem } from './countdown-timer';
+export { BreathingExercise as BreathingExerciseItem } from './breathing-exercise';
diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx
index 52f8f6e..2692861 100644
--- a/src/components/menu/menu.tsx
+++ b/src/components/menu/menu.tsx
@@ -15,13 +15,19 @@ import {
PresetsItem,
ShortcutsItem,
SleepTimerItem,
+ BreathingExerciseItem,
} from './items';
import { Divider } from './divider';
import { ShareLinkModal } from '@/components/modals/share-link';
import { PresetsModal } from '@/components/modals/presets';
import { ShortcutsModal } from '@/components/modals/shortcuts';
import { SleepTimerModal } from '@/components/modals/sleep-timer';
-import { Notepad, Pomodoro, CountdownTimer } from '@/components/toolbox';
+import {
+ Notepad,
+ Pomodoro,
+ CountdownTimer,
+ BreathingExercise,
+} from '@/components/toolbox';
import { fade, mix, slideY } from '@/lib/motion';
import { useSoundStore } from '@/stores/sound';
@@ -36,6 +42,7 @@ export function Menu() {
const initial = useMemo(
() => ({
+ breathingExercise: false,
countdownTimer: false,
notepad: false,
pomodoro: false,
@@ -113,6 +120,9 @@ export function Menu() {
open('pomodoro')} />
open('notepad')} />
+ open('breathingExercise')}
+ />
open('countdownTimer')} />
@@ -144,6 +154,10 @@ export function Menu() {
show={modals.pomodoro}
onClose={() => close('pomodoro')}
/>
+ close('breathingExercise')}
+ />
close('countdownTimer')}
diff --git a/src/components/toolbox/breathing/breathing.module.css b/src/components/toolbox/breathing/breathing.module.css
new file mode 100644
index 0000000..fdbd99d
--- /dev/null
+++ b/src/components/toolbox/breathing/breathing.module.css
@@ -0,0 +1 @@
+/* WIP */
diff --git a/src/components/toolbox/breathing/breathing.tsx b/src/components/toolbox/breathing/breathing.tsx
new file mode 100644
index 0000000..c807a22
--- /dev/null
+++ b/src/components/toolbox/breathing/breathing.tsx
@@ -0,0 +1,18 @@
+import { Modal } from '@/components/modal';
+import { Exercise } from './exercise';
+
+import styles from './breathing.module.css';
+
+interface TimerProps {
+ onClose: () => void;
+ show: boolean;
+}
+
+export function BreathingExercise({ onClose, show }: TimerProps) {
+ return (
+
+ Breathing Exercise
+
+
+ );
+}
diff --git a/src/components/toolbox/breathing/exercise/exercise.module.css b/src/components/toolbox/breathing/exercise/exercise.module.css
new file mode 100644
index 0000000..0bfd464
--- /dev/null
+++ b/src/components/toolbox/breathing/exercise/exercise.module.css
@@ -0,0 +1,44 @@
+.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(transparent, 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/toolbox/breathing/exercise/exercise.tsx b/src/components/toolbox/breathing/exercise/exercise.tsx
new file mode 100644
index 0000000..da06512
--- /dev/null
+++ b/src/components/toolbox/breathing/exercise/exercise.tsx
@@ -0,0 +1,120 @@
+import { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+
+import styles from './exercise.module.css';
+
+type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
+type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
+
+export function Exercise() {
+ 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: { scale: 1, transition: { duration: durations.exhale } },
+ holdExhale: {
+ scale: 1,
+ transition: { duration: durations.holdExhale || 4 },
+ },
+ holdInhale: {
+ scale: 1.5,
+ transition: { duration: durations.holdInhale || 4 },
+ },
+ inhale: { 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 (
+ <>
+
+
+ `translate(-50%, -50%) ${generatedString}`
+ }
+ />
+ {getLabel(phase)}
+
+
+
+ >
+ );
+}
diff --git a/src/components/toolbox/breathing/exercise/index.ts b/src/components/toolbox/breathing/exercise/index.ts
new file mode 100644
index 0000000..881062d
--- /dev/null
+++ b/src/components/toolbox/breathing/exercise/index.ts
@@ -0,0 +1 @@
+export { Exercise } from './exercise';
diff --git a/src/components/toolbox/breathing/index.ts b/src/components/toolbox/breathing/index.ts
new file mode 100644
index 0000000..323b544
--- /dev/null
+++ b/src/components/toolbox/breathing/index.ts
@@ -0,0 +1 @@
+export { BreathingExercise } from './breathing';
diff --git a/src/components/toolbox/index.ts b/src/components/toolbox/index.ts
index 67924b9..f8725ad 100644
--- a/src/components/toolbox/index.ts
+++ b/src/components/toolbox/index.ts
@@ -1,3 +1,4 @@
export { Notepad } from './notepad';
export { Pomodoro } from './pomodoro';
export { CountdownTimer } from './countdown-timer';
+export { BreathingExercise } from './breathing';