{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-toggle",
  "title": "Theme Toggle",
  "description": "Manual light/dark control with a click-origin reveal. Ignores system theme.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/button-group.json",
    "https://components.exe.xyz/r/button.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/theme-toggle.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { flushSync } from \"react-dom\"\nimport { Moon, Sun } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { ButtonGroup } from \"@/components/ui/button-group\"\n\nexport const THEME_STORAGE_KEY = \"components:theme\"\nconst LEGACY_THEME_KEYS = [\"ui:theme\"]\n\nexport type Theme = \"light\" | \"dark\"\n\nfunction readStoredTheme(): Theme | null {\n  if (typeof localStorage === \"undefined\") return null\n  try {\n    const keys = [THEME_STORAGE_KEY, ...LEGACY_THEME_KEYS]\n    for (const key of keys) {\n      const value = localStorage.getItem(key)\n      if (value === \"dark\" || value === \"light\") return value\n    }\n  } catch {\n    /* ignore */\n  }\n  return null\n}\n\nfunction readTheme(): Theme {\n  if (typeof document === \"undefined\") return \"light\"\n  if (document.documentElement.classList.contains(\"dark\")) return \"dark\"\n  return readStoredTheme() ?? \"light\"\n}\n\nfunction prefersReducedMotion() {\n  return (\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  )\n}\n\nfunction ensureViewTransitionStyles() {\n  if (typeof document === \"undefined\") return\n  if (document.getElementById(\"plotter-theme-vt\")) return\n  const style = document.createElement(\"style\")\n  style.id = \"plotter-theme-vt\"\n  style.textContent = `\n    ::view-transition-old(root),\n    ::view-transition-new(root) {\n      animation: none;\n      mix-blend-mode: normal;\n    }\n    ::view-transition-old(root) { z-index: 1; }\n    ::view-transition-new(root) { z-index: 9999; }\n  `\n  document.head.appendChild(style)\n}\n\ntype ViewTransition = { ready: Promise<void> }\n\nfunction applyTheme(next: Theme, setTheme: (theme: Theme) => void) {\n  flushSync(() => {\n    document.documentElement.classList.toggle(\"dark\", next === \"dark\")\n    setTheme(next)\n  })\n  try {\n    localStorage.setItem(THEME_STORAGE_KEY, next)\n  } catch {\n    /* ignore quota / private mode */\n  }\n}\n\nfunction revealFromPoint(\n  point: { clientX: number; clientY: number },\n  next: Theme,\n  setTheme: (theme: Theme) => void\n) {\n  const doc = document as Document & {\n    startViewTransition?: (update: () => void) => ViewTransition\n  }\n  if (prefersReducedMotion() || typeof doc.startViewTransition !== \"function\") {\n    applyTheme(next, setTheme)\n    return\n  }\n  ensureViewTransitionStyles()\n  const x = point.clientX\n  const y = point.clientY\n  const endRadius = Math.hypot(\n    Math.max(x, window.innerWidth - x),\n    Math.max(y, window.innerHeight - y)\n  )\n  const transition = doc.startViewTransition(() => applyTheme(next, setTheme))\n  void transition.ready.then(() => {\n    document.documentElement.animate(\n      {\n        clipPath: [\n          `circle(0px at ${x}px ${y}px)`,\n          `circle(${endRadius}px at ${x}px ${y}px)`,\n        ],\n      },\n      {\n        duration: 450,\n        easing: \"cubic-bezier(0.22, 1, 0.36, 1)\",\n        pseudoElement: \"::view-transition-new(root)\",\n      }\n    )\n  })\n}\n\nfunction eventPoint(event: React.SyntheticEvent<HTMLElement>) {\n  const rect = event.currentTarget.getBoundingClientRect()\n  const mouse = event.nativeEvent as MouseEvent\n  const hasMouse =\n    typeof mouse.clientX === \"number\" &&\n    (mouse.clientX !== 0 || mouse.clientY !== 0)\n  return {\n    clientX: hasMouse ? mouse.clientX : rect.left + rect.width / 2,\n    clientY: hasMouse ? mouse.clientY : rect.top + rect.height / 2,\n  }\n}\n\nexport function ThemeScript() {\n  const src = `(function(){try{var k=\"${THEME_STORAGE_KEY}\";var v=localStorage.getItem(k)||localStorage.getItem(\"ui:theme\");document.documentElement.classList.toggle(\"dark\",v===\"dark\")}catch(e){}})();`\n  return <script dangerouslySetInnerHTML={{ __html: src }} />\n}\n\nexport function ThemeToggle({\n  collapsed = false,\n  className,\n}: {\n  collapsed?: boolean\n  className?: string\n}) {\n  const [theme, setTheme] = React.useState<Theme>(\"light\")\n\n  React.useEffect(() => {\n    setTheme(readTheme())\n  }, [])\n\n  function set(next: Theme, event: React.SyntheticEvent<HTMLElement>) {\n    if (next === theme) return\n    revealFromPoint(eventPoint(event), next, setTheme)\n  }\n\n  if (collapsed) {\n    return (\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        size=\"icon\"\n        className={cn(\"size-9 rounded-[2px]\", className)}\n        aria-label=\"Dark mode\"\n        aria-pressed={theme === \"dark\"}\n        onClick={(event) => set(theme === \"dark\" ? \"light\" : \"dark\", event)}\n      >\n        {theme === \"dark\" ? (\n          <Moon className=\"size-3.5\" />\n        ) : (\n          <Sun className=\"size-3.5\" />\n        )}\n      </Button>\n    )\n  }\n\n  return (\n    <ButtonGroup\n      aria-label=\"Theme\"\n      className={cn(\"[&_[data-slot=button]]:min-h-10\", className)}\n    >\n      <Button\n        type=\"button\"\n        variant={theme === \"light\" ? \"secondary\" : \"ghost\"}\n        size=\"sm\"\n        aria-label=\"Light theme\"\n        aria-pressed={theme === \"light\"}\n        onClick={(event) => set(\"light\", event)}\n      >\n        <Sun className=\"size-3.5\" />\n        <span className=\"hidden sm:inline\">Light</span>\n      </Button>\n      <Button\n        type=\"button\"\n        variant={theme === \"dark\" ? \"secondary\" : \"ghost\"}\n        size=\"sm\"\n        aria-label=\"Dark theme\"\n        aria-pressed={theme === \"dark\"}\n        onClick={(event) => set(\"dark\", event)}\n      >\n        <Moon className=\"size-3.5\" />\n        <span className=\"hidden sm:inline\">Dark</span>\n      </Button>\n    </ButtonGroup>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}