import React, { createContext, useContext, useEffect, useState, useCallback } from "react"; export type Theme = "light" | "dark" | "system"; interface ThemeContextValue { theme: Theme; resolvedTheme: "light" | "dark"; setTheme: (theme: Theme) => void; } const ThemeContext = createContext(undefined); const STORAGE_KEY = "theme"; function getSystemTheme(): "light" | "dark" { if (typeof window === "undefined") return "light"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function getStoredTheme(): Theme { if (typeof window === "undefined") return "system"; try { const stored = localStorage.getItem(STORAGE_KEY); if (stored === "light" || stored === "dark" || stored === "system") { return stored; } } catch { // localStorage not available } return "system"; } function applyTheme(theme: "light" | "dark") { const root = document.documentElement; root.classList.remove("light", "dark"); root.classList.add(theme); } export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within a ThemeProvider"); } return context; } interface ThemeProviderProps { children: React.ReactNode; defaultTheme?: Theme; } export function ThemeProvider({ children, defaultTheme = "system" }: ThemeProviderProps) { const [theme, setThemeState] = useState(() => getStoredTheme() || defaultTheme); const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(() => { const stored = getStoredTheme() || defaultTheme; return stored === "system" ? getSystemTheme() : stored; }); const setTheme = useCallback((newTheme: Theme) => { setThemeState(newTheme); try { localStorage.setItem(STORAGE_KEY, newTheme); } catch { // localStorage not available } }, []); // Apply theme on mount and when theme changes useEffect(() => { const resolved = theme === "system" ? getSystemTheme() : theme; setResolvedTheme(resolved); applyTheme(resolved); }, [theme]); // Listen for system theme changes useEffect(() => { if (theme !== "system") return; const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = (e: MediaQueryListEvent) => { const newTheme = e.matches ? "dark" : "light"; setResolvedTheme(newTheme); applyTheme(newTheme); }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme]); const value: ThemeContextValue = { theme, resolvedTheme, setTheme, }; return React.createElement(ThemeContext.Provider, { value }, children); }