import { createContext, useContext, useState, useLayoutEffect, type ReactNode, } from "react"; import { flushSync } from "react-dom"; interface ThemeContextValue { theme: string; toggleTheme: () => void; } const ThemeContext = createContext(null); type ViewTransitionDocument = Document & { startViewTransition?: (callback: () => void) => unknown; }; export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("boha-theme") || "dark"; } return "dark"; }); // Apply synchronously (layout effect) so the `data-theme` attribute flips // inside the View Transition's flushSync below — letting the browser capture // the "after" snapshot and cross-fade to it. useLayoutEffect(() => { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("boha-theme", theme); const themeColor = theme === "dark" ? "#12121a" : "#ffffff"; document .querySelector('meta[name="theme-color"]') ?.setAttribute("content", themeColor); }, [theme]); const toggleTheme = () => { const apply = () => setTheme((prev) => (prev === "dark" ? "light" : "dark")); const doc = document as ViewTransitionDocument; const reduceMotion = typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; // View Transitions API: the browser cross-fades a GPU-composited snapshot of // the whole page — smooth at any element count — instead of repainting every // node's colors (which stutters). Falls back to an instant switch where the // API is unavailable or the user prefers reduced motion. if (doc.startViewTransition && !reduceMotion) { doc.startViewTransition(() => flushSync(apply)); } else { apply(); } }; return ( {children} ); } export function useTheme(): ThemeContextValue { const context = useContext(ThemeContext); if (!context) throw new Error("useTheme must be used within a ThemeProvider"); return context; }