{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "theme-provider", "dependencies": [], "registryDependencies": [], "files": [ { "path": "src/components/theme-provider.tsx", "content": "/* eslint-disable react-refresh/only-export-components */\nimport * as React from \"react\"\n\ntype Theme = \"dark\" | \"light\" | \"system\"\ntype ResolvedTheme = \"dark\" | \"light\"\n\ntype ThemeProviderProps = {\n children: React.ReactNode\n defaultTheme?: Theme\n storageKey?: string\n disableTransitionOnChange?: boolean\n}\n\ntype ThemeProviderState = {\n theme: Theme\n setTheme: (theme: Theme) => void\n}\n\nconst COLOR_SCHEME_QUERY = \"(prefers-color-scheme: dark)\"\nconst THEME_VALUES: Theme[] = [\"dark\", \"light\", \"system\"]\n\nconst ThemeProviderContext = React.createContext<\n ThemeProviderState | undefined\n>(undefined)\n\nfunction isTheme(value: string | null): value is Theme {\n if (value === null) {\n return false\n }\n\n return THEME_VALUES.includes(value as Theme)\n}\n\nfunction getSystemTheme(): ResolvedTheme {\n if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {\n return \"dark\"\n }\n\n return \"light\"\n}\n\nfunction disableTransitionsTemporarily() {\n const style = document.createElement(\"style\")\n style.appendChild(\n document.createTextNode(\n \"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}\"\n )\n )\n document.head.appendChild(style)\n\n return () => {\n window.getComputedStyle(document.body)\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n style.remove()\n })\n })\n }\n}\n\nfunction isEditableTarget(target: EventTarget | null) {\n if (!(target instanceof HTMLElement)) {\n return false\n }\n\n if (target.isContentEditable) {\n return true\n }\n\n const editableParent = target.closest(\n \"input, textarea, select, [contenteditable='true']\"\n )\n if (editableParent) {\n return true\n }\n\n return false\n}\n\nexport function ThemeProvider({\n children,\n defaultTheme = \"system\",\n storageKey = \"theme\",\n disableTransitionOnChange = true,\n ...props\n}: ThemeProviderProps) {\n const [theme, setThemeState] = React.useState(() => {\n const storedTheme = localStorage.getItem(storageKey)\n if (isTheme(storedTheme)) {\n return storedTheme\n }\n\n return defaultTheme\n })\n\n const setTheme = React.useCallback(\n (nextTheme: Theme) => {\n localStorage.setItem(storageKey, nextTheme)\n setThemeState(nextTheme)\n },\n [storageKey]\n )\n\n const applyTheme = React.useCallback(\n (nextTheme: Theme) => {\n const root = document.documentElement\n const resolvedTheme =\n nextTheme === \"system\" ? getSystemTheme() : nextTheme\n const restoreTransitions = disableTransitionOnChange\n ? disableTransitionsTemporarily()\n : null\n\n root.classList.remove(\"light\", \"dark\")\n root.classList.add(resolvedTheme)\n\n if (restoreTransitions) {\n restoreTransitions()\n }\n },\n [disableTransitionOnChange]\n )\n\n React.useEffect(() => {\n applyTheme(theme)\n\n if (theme !== \"system\") {\n return undefined\n }\n\n const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)\n const handleChange = () => {\n applyTheme(\"system\")\n }\n\n mediaQuery.addEventListener(\"change\", handleChange)\n\n return () => {\n mediaQuery.removeEventListener(\"change\", handleChange)\n }\n }, [theme, applyTheme])\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.repeat) {\n return\n }\n\n if (event.metaKey || event.ctrlKey || event.altKey) {\n return\n }\n\n if (isEditableTarget(event.target)) {\n return\n }\n\n if (event.key.toLowerCase() !== \"d\") {\n return\n }\n\n setThemeState((currentTheme) => {\n const nextTheme =\n currentTheme === \"dark\"\n ? \"light\"\n : currentTheme === \"light\"\n ? \"dark\"\n : getSystemTheme() === \"dark\"\n ? \"light\"\n : \"dark\"\n\n localStorage.setItem(storageKey, nextTheme)\n return nextTheme\n })\n }\n\n window.addEventListener(\"keydown\", handleKeyDown)\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [storageKey])\n\n React.useEffect(() => {\n const handleStorageChange = (event: StorageEvent) => {\n if (event.storageArea !== localStorage) {\n return\n }\n\n if (event.key !== storageKey) {\n return\n }\n\n if (isTheme(event.newValue)) {\n setThemeState(event.newValue)\n return\n }\n\n setThemeState(defaultTheme)\n }\n\n window.addEventListener(\"storage\", handleStorageChange)\n\n return () => {\n window.removeEventListener(\"storage\", handleStorageChange)\n }\n }, [defaultTheme, storageKey])\n\n const value = React.useMemo(\n () => ({\n theme,\n setTheme,\n }),\n [theme, setTheme]\n )\n\n return (\n \n {children}\n \n )\n}\n\nexport const useTheme = () => {\n const context = React.useContext(ThemeProviderContext)\n\n if (context === undefined) {\n throw new Error(\"useTheme must be used within a ThemeProvider\")\n }\n\n return context\n}\n", "type": "registry:component" } ], "type": "registry:component" }