{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-toggle",
  "title": "Theme Toggle",
  "dependencies": [
    "@phosphor-icons/react"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/button.json",
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/components/ui/theme-toggle.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport type * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport { Moon, Sun } from \"@phosphor-icons/react/dist/ssr\";\nexport type Theme = \"light\" | \"dark\" | \"system\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\nfunction getSystemTheme(): ResolvedTheme {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction resolveTheme(theme: Theme): ResolvedTheme {\n  return theme === \"system\" ? getSystemTheme() : theme;\n}\n\nconst emptySubscribe = () => () => {};\n\nfunction useMounted() {\n  return useSyncExternalStore(\n    emptySubscribe,\n    () => true,\n    () => false,\n  );\n}\n\nfunction ThemeIcon({\n  isDark,\n  mounted,\n  className,\n}: {\n  isDark: boolean;\n  mounted: boolean;\n  className?: string;\n}): React.ReactElement {\n  if (!mounted) {\n    return <span className={cn(\"inline-block\", className)} />;\n  }\n\n  return (\n    <span\n      className={cn(\n        \"relative inline-flex items-center justify-center overflow-hidden\",\n        className,\n      )}\n    >\n      <span\n        className={cn(\n          \"absolute inset-0 flex items-center justify-center transition-[opacity,transform] duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n          isDark ? \"scale-100 rotate-0 opacity-100\" : \"scale-0 rotate-90 opacity-0\",\n        )}\n      >\n        <Sun className=\"size-full\" aria-hidden />\n      </span>\n      <span\n        className={cn(\n          \"absolute inset-0 flex items-center justify-center transition-[opacity,transform] duration-[var(--duration-state)] ease-[var(--ease-standard)]\",\n          isDark ? \"scale-0 -rotate-90 opacity-0\" : \"scale-100 rotate-0 opacity-100\",\n        )}\n      >\n        <Moon className=\"size-full\" aria-hidden />\n      </span>\n    </span>\n  );\n}\n\nexport interface ThemeToggleProps\n  extends Omit<React.ComponentProps<\"button\">, \"children\"> {\n  theme?: Theme;\n  onThemeChange?: (theme: ResolvedTheme) => void;\n  /** localStorage key for persisting the theme. Set to `false` to disable. */\n  storageKey?: string | false;\n  /** Whether to apply the `.dark` class to `<html>`. */\n  applyClass?: boolean;\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst iconSizeBySize: Record<NonNullable<ThemeToggleProps[\"size\"]>, string> = {\n  sm: \"size-3.5\",\n  md: \"size-4\",\n  lg: \"size-5\",\n};\n\nexport function ThemeToggle({\n  className,\n  theme: controlledTheme,\n  onThemeChange,\n  storageKey = \"patch-ui-theme\",\n  applyClass = true,\n  size = \"md\",\n  ...props\n}: ThemeToggleProps): React.ReactElement {\n  const mounted = useMounted();\n\n  // Lazy initializer reads persisted / system theme at mount so first render reflects it (no flash).\n  const [internalTheme, setInternalTheme] = useState<ResolvedTheme>(() => {\n    if (typeof window === \"undefined\") return \"light\";\n    if (storageKey !== false) {\n      const stored = localStorage.getItem(storageKey);\n      if (stored === \"light\" || stored === \"dark\") return stored;\n    }\n    return getSystemTheme();\n  });\n\n  const resolved: ResolvedTheme =\n    controlledTheme !== undefined\n      ? resolveTheme(controlledTheme)\n      : internalTheme;\n\n  useEffect(() => {\n    if (!applyClass || !mounted) return;\n    document.documentElement.classList.toggle(\"dark\", resolved === \"dark\");\n  }, [resolved, applyClass, mounted]);\n\n  const toggle = useCallback(() => {\n    const next: ResolvedTheme = resolved === \"light\" ? \"dark\" : \"light\";\n    if (controlledTheme === undefined) {\n      setInternalTheme(next);\n      if (storageKey !== false) {\n        localStorage.setItem(storageKey, next);\n      }\n    }\n    onThemeChange?.(next);\n  }, [resolved, controlledTheme, onThemeChange, storageKey]);\n\n  const isDark = resolved === \"dark\";\n\n  return (\n    <Button\n      variant=\"tertiary\"\n      size={size}\n      onClick={toggle}\n      className={cn(\"overflow-hidden\", className)}\n      data-slot=\"theme-toggle\"\n      data-theme={mounted ? resolved : undefined}\n      aria-label={\n        mounted\n          ? `Switch to ${isDark ? \"light\" : \"dark\"} mode`\n          : \"Toggle theme\"\n      }\n      icon={\n        <ThemeIcon\n          isDark={isDark}\n          mounted={mounted}\n          className={iconSizeBySize[size]}\n        />\n      }\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/theme-toggle.tsx"
    }
  ],
  "type": "registry:ui"
}