// Mental palate cleanser — 3-min guided routine to transition between // big tasks of different energy types. Lives at the top of the cockpit. // // Phases (60s each): // 1. PARK — capture where you left off (one line → Brain zips) // 2. RESET — body off-screen, 4 box-breaths (4-4-4-4) // 3. AIM — pick next priority, ideally of different "energy" const PHASE_LEN = 60; // seconds per phase const TOTAL = PHASE_LEN * 3; // Rough heuristic mapping priority categories → energy buckets so we can // suggest something of a DIFFERENT energy than what was just touched. // Pulled from CATEGORIES (set in components/categories.jsx). const ENERGY_OF = (tag) => (CATEGORIES[tag] || CATEGORIES.ops).energy; const ENERGY_LABEL = (energy) => { const map = { 'social-deep': 'social · deep', 'social-warm': 'social · warm', 'analytical': 'analytical', 'creative': 'creative', 'admin': 'admin · structural', 'restorative': 'restorative', }; return map[energy] || energy; }; function Cleanser() { const { state, addBrainItem, startBurst, bumpCleanseStreak } = useStore(); const [active, setActive] = React.useState(false); const [elapsed, setElapsed] = React.useState(0); const [park, setPark] = React.useState(''); const [breath, setBreath] = React.useState('in'); // in | hold-in | out | hold-out const parkSavedRef = React.useRef(false); // 1Hz tick React.useEffect(() => { if (!active) return; const id = setInterval(() => setElapsed((e) => e + 1), 1000); return () => clearInterval(id); }, [active]); // box-breath cycle: 4s in, 4s hold, 4s out, 4s hold — driven by elapsed React.useEffect(() => { if (!active) return; const inPhase = phaseIndex(elapsed) === 1; if (!inPhase) return; const sec = elapsed - PHASE_LEN; // 0..60 within phase 2 const slot = sec % 16; if (slot < 4) setBreath('in'); else if (slot < 8) setBreath('hold-in'); else if (slot < 12) setBreath('out'); else setBreath('hold-out'); }, [elapsed, active]); // when crossing into phase 3 OR when ending, save the park note (once) React.useEffect(() => { if (!active) return; if (elapsed >= PHASE_LEN && !parkSavedRef.current && park.trim()) { addBrainItem(`📍 Left off: ${park.trim()}`, 'dump'); parkSavedRef.current = true; } }, [elapsed, active, park, addBrainItem]); // auto-end at 3:00 React.useEffect(() => { if (active && elapsed >= TOTAL) { // optional: chime here bumpCleanseStreak(); } }, [elapsed, active, bumpCleanseStreak]); const start = () => { setActive(true); setElapsed(0); setPark(''); parkSavedRef.current = false; }; const end = () => { if (park.trim() && !parkSavedRef.current) { addBrainItem(`📍 Left off: ${park.trim()}`, 'dump'); parkSavedRef.current = true; } setActive(false); setElapsed(0); }; if (!active) { return (
Palate cleanser Just finished something? 3 minutes to transition.
streak {state.cleanseStreak || 0}
); } const remaining = Math.max(0, TOTAL - elapsed); const m = Math.floor(remaining / 60); const s = remaining % 60; const idx = phaseIndex(elapsed); const phaseElapsed = elapsed - idx * PHASE_LEN; const phasePct = Math.min(100, (phaseElapsed / PHASE_LEN) * 100); const phaseNames = ['Park it', 'Reset', 'Aim']; return (
palate cleanser · phase {idx + 1}/3 · {phaseNames[idx]}
{String(m).padStart(2, '0')}:{String(s).padStart(2, '0')}
{[0, 1, 2].map((i) => { const segPct = i < idx ? 100 : i === idx ? phasePct : 0; return (
0{i + 1} · {phaseNames[i]}
); })}
{idx === 0 && setElapsed(PHASE_LEN)}/>} {idx === 1 && setElapsed(PHASE_LEN * 2)}/>} {idx === 2 && { startBurst(p.id, null, state.burst.length); end(); }} onSkip={end} onDone={end}/>} {elapsed >= TOTAL && ( { startBurst(p.id, null, state.burst.length); end(); }}/> )}
); } function phaseIndex(elapsed) { return Math.min(2, Math.floor(elapsed / PHASE_LEN)); } // ── Phase 1: Park it ──────────────────────────────────────────── function PhasePark({ park, setPark, onNext }) { return ( <>

Where did you just leave off?

One line. Not a status report. Just enough so future-you can pick it back up. Saves to Brain zips automatically.