"use client"; import { useCallback, useSyncExternalStore } from "react"; type Theme = "light" | "dark"; const listeners = new Set<() => void>(); function subscribe(cb: () => void): () => void { listeners.add(cb); return () => { listeners.delete(cb); }; } function getSnapshot(): Theme { if (typeof document === "undefined") return "light"; return document.documentElement.classList.contains("dark") ? "dark" : "light"; } function getServerSnapshot(): Theme { return "light"; } type ToggleOrigin = { x: number; y: number }; export function useTheme() { const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const toggleTheme = useCallback((origin?: ToggleOrigin) => { const next: Theme = getSnapshot() === "dark" ? "light" : "dark"; const apply = () => { if (next === "dark") { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } try { localStorage.setItem("pi-theme", next); } catch { // ignore storage errors (private mode, quota, etc.) } listeners.forEach((cb) => cb()); }; const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; const supportsVT = typeof document.startViewTransition === "function"; if (!supportsVT || reduceMotion) { apply(); return; } const x = origin?.x ?? window.innerWidth / 2; const y = origin?.y ?? window.innerHeight / 2; const endRadius = Math.hypot( Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y), ); const transition = document.startViewTransition(apply); transition.ready .then(() => { document.documentElement.animate( { clipPath: [ `circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`, ], }, { duration: 450, easing: "cubic-bezier(0.22, 0.61, 0.36, 1)", pseudoElement: "::view-transition-new(root)", }, ); }) .catch(() => { // transition cancelled — ignore }); }, []); return { theme, toggleTheme, isDark: theme === "dark" }; }