{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-switch",
  "title": "Theme Switch",
  "description": "Light and dark toggle with sun-moon icon swap and four page transitions.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "@ikas/button",
    "@ikas/motion",
    "@ikas/theme"
  ],
  "files": [
    {
      "path": "registry/ikas/theme-switch.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { flushSync } from \"react-dom\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { MoonIcon, SunIcon } from \"lucide-react\"\n\nimport { FADE_IN, FADE_OUT, ICON_SPRING, INSTANT } from \"@/lib/motion\"\nimport { Button, type ButtonProps } from \"@/components/ui/button\"\n\ntype Theme = \"light\" | \"dark\"\n\n/** The latest transition; an earlier one finishing late must not clear its attribute. */\nlet latestRun = 0\ntype ThemeTransition = \"reveal\" | \"eclipse\" | \"split\" | \"rise\" | \"none\"\n\ntype ThemeSwitchProps = Omit<ButtonProps, \"children\" | \"onClick\" | \"asChild\" | \"loading\"> & {\n  /** Current appearance. Drives the icon and aria-pressed. */\n  theme: Theme\n  /** Called with the next appearance; apply it here (a class on <html>, next-themes setTheme…). Runs inside the page transition. */\n  onThemeChange: (theme: Theme) => void\n  /** How the new appearance enters the page: reveal grows a circle from the switch, eclipse slides a disc in from the side, split opens from a vertical seam, rise swells up from the bottom on a curve. */\n  transition?: ThemeTransition\n  /** Square icon button without the text. */\n  iconOnly?: boolean\n  /** Visible text next to the icon. */\n  label?: React.ReactNode\n}\n\n/** Rotation and scale ride the icon spring; opacity fades on the shared tween. */\nconst enter = { ...ICON_SPRING, opacity: FADE_IN }\n\n/** How long after mount a theme change still counts as the stored preference arriving, not a toggle. */\nconst SETTLE_MS = 300\n\n/** False right after mount, so a page that loads dark swaps its icon in place instead of spinning it. */\nfunction useSettled() {\n  const [settled, setSettled] = React.useState(false)\n  React.useEffect(() => {\n    const timer = setTimeout(() => setSettled(true), SETTLE_MS)\n    return () => clearTimeout(timer)\n  }, [])\n  return settled\n}\n\n/** Light and dark toggle. Sun and moon trade places on one rotation; the page follows with a view transition. */\nfunction ThemeSwitch({\n  theme,\n  onThemeChange,\n  transition = \"reveal\",\n  iconOnly = false,\n  label = \"Temayı değiştir\",\n  variant = \"outline\",\n  size,\n  className,\n  ...props\n}: ThemeSwitchProps) {\n  const reduce = useReducedMotion() ?? false\n  const settled = useSettled()\n  const next: Theme = theme === \"light\" ? \"dark\" : \"light\"\n  const animate = settled && !reduce\n\n  function toggle(trigger: HTMLElement) {\n    // Read the media query on click too: the hook keeps its first value when the setting changes later.\n    const still = reduce || matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    if (transition === \"none\" || still || !document.startViewTransition) {\n      onThemeChange(next)\n      return\n    }\n    const root = document.documentElement\n    const { left, top, width, height } = trigger.getBoundingClientRect()\n    const x = left + width / 2\n    const y = top + height / 2\n    root.style.setProperty(\"--theme-x\", `${x}px`)\n    root.style.setProperty(\"--theme-y\", `${y}px`)\n    root.style.setProperty(\"--theme-r\", `${Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))}px`)\n    root.dataset.themeTransition = transition\n    const token = ++latestRun\n    const run = document.startViewTransition(() => flushSync(() => onThemeChange(next)))\n    run.finished.finally(() => {\n      if (token === latestRun) delete root.dataset.themeTransition\n    })\n  }\n\n  return (\n    <Button\n      type=\"button\"\n      data-slot=\"theme-switch\"\n      data-theme={theme}\n      data-transition={transition}\n      aria-label={iconOnly ? (theme === \"light\" ? \"Koyu temaya geç\" : \"Açık temaya geç\") : undefined}\n      aria-pressed={theme === \"dark\"}\n      variant={variant}\n      size={size ?? (iconOnly ? \"icon-sm\" : \"sm\")}\n      className={className}\n      onClick={(event) => toggle(event.currentTarget)}\n      {...props}\n    >\n      <span className=\"relative grid size-4 place-items-center\">\n        {/* The leaving icon turns a quarter clockwise and the arriving one follows from a quarter behind, so every swap spins one way. */}\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={theme}\n            aria-hidden=\"true\"\n            className=\"absolute grid place-items-center\"\n            initial={animate ? { opacity: 0, scale: 0.5, rotate: -90 } : false}\n            animate={{ opacity: 1, scale: 1, rotate: 0 }}\n            exit={animate ? { opacity: 0, scale: 0.5, rotate: 90, transition: FADE_OUT } : { opacity: 0, transition: INSTANT }}\n            transition={animate ? enter : INSTANT}\n          >\n            {theme === \"light\" ? <SunIcon data-anim=\"spin\" /> : <MoonIcon data-anim=\"wiggle\" />}\n          </motion.span>\n        </AnimatePresence>\n      </span>\n      {!iconOnly && label}\n    </Button>\n  )\n}\n\nexport { ThemeSwitch, type ThemeSwitchProps, type Theme, type ThemeTransition }\n",
      "type": "registry:component",
      "target": "@components/ikas/theme-switch.tsx"
    }
  ],
  "docs": "https://builders.ikas.com/tr/docs/app-development/ui-kit/components/theme-switch",
  "categories": [
    "motion",
    "theme"
  ],
  "type": "registry:component"
}