import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
export type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue {
theme: Theme;
resolvedTheme: 'light' | 'dark';
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
function getSystemTheme(): 'light' | 'dark' {
if (typeof window !== 'undefined') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
}
interface ThemeProviderProps {
children: ReactNode;
defaultTheme?: Theme;
}
export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(
defaultTheme === 'system' ? getSystemTheme() : defaultTheme
);
// Sync theme when defaultTheme prop changes
useEffect(() => {
setTheme(defaultTheme);
}, [defaultTheme]);
useEffect(() => {
if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => setResolvedTheme(mediaQuery.matches ? 'dark' : 'light');
handleChange();
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
} else {
setResolvedTheme(theme);
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}