{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-switch",
  "title": "Theme Switch",
  "description": "Animated dark / light mode toggle button with a spring-driven sun ↔ moon icon swap and optional View Transition reveal.",
  "dependencies": [
    "next-themes",
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/components/unlumen/theme-switch/index.tsx",
      "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction SunIcon({ size = 16 }: { size?: number }) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"4\" />\n      <line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"6\" />\n      <line x1=\"12\" y1=\"18\" x2=\"12\" y2=\"22\" />\n      <line x1=\"4.93\" y1=\"4.93\" x2=\"7.76\" y2=\"7.76\" />\n      <line x1=\"16.24\" y1=\"16.24\" x2=\"19.07\" y2=\"19.07\" />\n      <line x1=\"2\" y1=\"12\" x2=\"6\" y2=\"12\" />\n      <line x1=\"18\" y1=\"12\" x2=\"22\" y2=\"12\" />\n      <line x1=\"4.93\" y1=\"19.07\" x2=\"7.76\" y2=\"16.24\" />\n      <line x1=\"16.24\" y1=\"7.76\" x2=\"19.07\" y2=\"4.93\" />\n    </svg>\n  );\n}\n\nfunction MoonIcon({ size = 16 }: { size?: number }) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n    </svg>\n  );\n}\n\n// no-op fallback for browsers that don't support startViewTransition\nfunction applyWithTransition(\n  origin: { x: number; y: number },\n  apply: () => void,\n) {\n  if (!document.startViewTransition || window.innerWidth > 1800) {\n    apply();\n    return;\n  }\n  const { x, y } = origin;\n  const endRadius = Math.hypot(\n    Math.max(x, window.innerWidth - x),\n    Math.max(y, window.innerHeight - y),\n  );\n  document.documentElement.style.setProperty(\"--vt-x\", `${x}px`);\n  document.documentElement.style.setProperty(\"--vt-y\", `${y}px`);\n  document.documentElement.style.setProperty(\"--vt-r\", `${endRadius}px`);\n  document.startViewTransition(apply).ready.catch(() => {});\n}\n\ninterface ThemeSwitchProps {\n  /** @default 16 */\n  iconSize?: number;\n  className?: string;\n}\n\nfunction ThemeSwitch({ iconSize = 16, className }: ThemeSwitchProps) {\n  const { resolvedTheme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const originRef = useRef({ x: 0, y: 0 });\n\n  useEffect(() => setMounted(true), []);\n\n  const toggle = (e: React.MouseEvent<HTMLButtonElement>) => {\n    originRef.current = { x: e.clientX, y: e.clientY };\n    const next = resolvedTheme === \"dark\" ? \"light\" : \"dark\";\n    applyWithTransition(originRef.current, () => setTheme(next));\n  };\n\n  const isDark = resolvedTheme === \"dark\";\n\n  if (!mounted) {\n    // placeholder to prevent layout shift before hydration\n    return (\n      <div\n        aria-hidden\n        className={cn(\n          \"size-9 rounded-full bg-accent border border-border\",\n          className,\n        )}\n      />\n    );\n  }\n\n  return (\n    <motion.button\n      onClick={toggle}\n      className={cn(\n        \"relative flex items-center justify-center size-9 rounded-full\",\n        \"bg-accent border border-border cursor-pointer\",\n        \"text-foreground outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n      whileHover={{ scale: 1.08 }}\n      whileTap={{ scale: 0.88 }}\n      transition={{ type: \"spring\", duration: 0.2, bounce: 0 }}\n      aria-label={isDark ? \"Switch to light mode\" : \"Switch to dark mode\"}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {isDark ? (\n          <motion.span\n            key=\"moon\"\n            initial={{ rotate: -45, scale: 0.5, opacity: 0 }}\n            animate={{ rotate: 0, scale: 1, opacity: 1 }}\n            exit={{ rotate: 45, scale: 0.5, opacity: 0 }}\n            transition={{ type: \"spring\", duration: 0.28, bounce: 0.3 }}\n            className=\"flex items-center justify-center\"\n          >\n            <MoonIcon size={iconSize} />\n          </motion.span>\n        ) : (\n          <motion.span\n            key=\"sun\"\n            initial={{ rotate: 45, scale: 0.5, opacity: 0 }}\n            animate={{ rotate: 0, scale: 1, opacity: 1 }}\n            exit={{ rotate: -45, scale: 0.5, opacity: 0 }}\n            transition={{ type: \"spring\", duration: 0.28, bounce: 0.3 }}\n            className=\"flex items-center justify-center\"\n          >\n            <SunIcon size={iconSize} />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    </motion.button>\n  );\n}\n\nexport { ThemeSwitch, type ThemeSwitchProps };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/theme-switch.tsx"
    }
  ],
  "type": "registry:ui"
}