{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"theme-toggle","type":"registry:ui","title":"Theme Toggle","description":"A day/night switch: the sun rolls across, its rays retract as a shadow bites it into a crescent, clouds drift off and stars burst out and twinkle.","dependencies":["motion"],"registryDependencies":[],"files":[{"path":"registry/ui/theme-toggle.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useId, useState } from \"react\";\nimport { motion, useAnimate, useReducedMotion } from \"motion/react\";\n\nexport interface ThemeToggleProps {\n  /** Controlled: true = dark. */\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onChange?: (dark: boolean) => void;\n  /** Track height in px (width is double). */\n  size?: number;\n  label?: string;\n  className?: string;\n}\n\nconst css = `@keyframes theme-toggle-twinkle{0%,100%{opacity:1}50%{opacity:.35}}\n.theme-toggle-star{animation:theme-toggle-twinkle 2.4s ease-in-out infinite}\n@media (prefers-reduced-motion:reduce){.theme-toggle-star{animation:none}}`;\n\n// Star resting spots as fractions of the track, left of the moon.\nconst stars = [\n  { x: 0.12, y: 0.3, s: 0.22 },\n  { x: 0.3, y: 0.62, s: 0.16 },\n  { x: 0.42, y: 0.22, s: 0.13 },\n  { x: 0.2, y: 0.76, s: 0.1 },\n  { x: 0.5, y: 0.52, s: 0.1 },\n];\nconst rays = Array.from({ length: 8 }, (_, i) => (i * Math.PI) / 4);\n\nexport function ThemeToggle({\n  checked,\n  defaultChecked = false,\n  onChange,\n  size = 32,\n  label = \"Dark mode\",\n  className,\n}: ThemeToggleProps) {\n  const reduce = useReducedMotion();\n  const [inner, setInner] = useState(defaultChecked);\n  const dark = checked ?? inner;\n  const mask = `tt-${useId().replace(/[^\\w-]/g, \"\")}`;\n\n  const w = size * 2;\n  const pad = size * 0.1;\n  const knob = size - pad * 2;\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 320, damping: 20 };\n  const soft = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 200, damping: 22 };\n\n  const [squish, animateSquish] = useAnimate();\n\n  const toggle = () => {\n    // Stretch along the travel, then settle round again.\n    if (!reduce) animateSquish(squish.current, { scaleX: [1, 1.3, 1], scaleY: [1, 0.82, 1] }, { duration: 0.45 });\n    setInner(!dark);\n    onChange?.(!dark);\n  };\n\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={dark}\n      aria-label={label}\n      onClick={toggle}\n      className={`relative shrink-0 overflow-hidden rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${className ?? \"\"}`}\n      style={{ width: w, height: size }}\n    >\n      <style href=\"theme-toggle\" precedence=\"default\">\n        {css}\n      </style>\n      <span aria-hidden className=\"absolute inset-0 bg-[linear-gradient(135deg,#7dd3fc,#0ea5e9)]\" />\n      <motion.span\n        aria-hidden\n        className=\"absolute inset-0 bg-[linear-gradient(135deg,#0b1026,#312e81)]\"\n        initial={false}\n        animate={{ opacity: dark ? 1 : 0 }}\n        transition={{ duration: reduce ? 0 : 0.45 }}\n      />\n      <span aria-hidden className=\"absolute inset-0 rounded-full shadow-[inset_0_2px_6px_rgb(0_0_0/.35)]\" />\n\n      {/* Clouds drift away when night falls. */}\n      {[\n        { x: 0.62, y: 0.52, s: 0.42 },\n        { x: 0.76, y: 0.28, s: 0.3 },\n      ].map((c, i) => (\n        <motion.span\n          key={i}\n          aria-hidden\n          className=\"absolute rounded-full bg-white/90\"\n          style={{ left: c.x * w, top: c.y * size, width: c.s * size * 1.8, height: c.s * size }}\n          initial={false}\n          animate={dark ? { x: size * 0.8, opacity: 0 } : { x: 0, opacity: 1 }}\n          transition={{ ...soft, delay: dark || reduce ? 0 : 0.15 + i * 0.06 }}\n        />\n      ))}\n\n      {/* Stars burst out of the moon as it lands, then twinkle. */}\n      {stars.map((s, i) => (\n        <motion.svg\n          key={i}\n          aria-hidden\n          viewBox=\"0 0 10 10\"\n          className=\"absolute\"\n          style={{ left: s.x * w, top: s.y * size, width: s.s * size, height: s.s * size, marginLeft: (-s.s * size) / 2, marginTop: (-s.s * size) / 2 }}\n          initial={false}\n          animate={dark ? { x: 0, y: 0, scale: 1, opacity: 1, rotate: 0 } : { x: w * 0.7 - s.x * w, y: size / 2 - s.y * size, scale: 0, opacity: 0, rotate: -90 }}\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 260, damping: 14, delay: dark ? 0.18 + i * 0.05 : 0 }}\n        >\n          <path\n            className=\"theme-toggle-star\"\n            style={{ animationDelay: `${i * 0.37}s` }}\n            fill=\"#fef9c3\"\n            d=\"M5 0 6.1 3.9 10 5 6.1 6.1 5 10 3.9 6.1 0 5 3.9 3.9Z\"\n          />\n        </motion.svg>\n      ))}\n\n      <motion.span\n        aria-hidden\n        className=\"absolute rounded-full\"\n        style={{ top: pad, left: pad, width: knob, height: knob }}\n        initial={false}\n        animate={{\n          x: dark ? w - knob - pad * 2 : 0,\n          boxShadow: dark ? \"0 0 10px 1px rgb(226 232 240 / .35)\" : \"0 0 14px 3px rgb(253 224 71 / .7)\",\n        }}\n        transition={{ x: spring, boxShadow: { duration: 0.4 } }}\n      >\n        <span ref={squish} className=\"block size-full\">\n        <motion.svg\n          viewBox=\"0 0 24 24\"\n          className=\"size-full overflow-visible\"\n          initial={false}\n          animate={{ rotate: dark ? 360 : 0 }}\n          transition={spring}\n        >\n          <defs>\n            <mask id={mask}>\n              <rect x=\"-6\" y=\"-6\" width=\"36\" height=\"36\" fill=\"white\" />\n              {/* The bite that carves the crescent. */}\n              <motion.circle\n                r=\"9\"\n                fill=\"black\"\n                initial={false}\n                animate={dark ? { cx: 18.5, cy: 6.5 } : { cx: 34, cy: -10 }}\n                transition={soft}\n              />\n            </mask>\n          </defs>\n          <motion.g initial={false} animate={{ scale: dark ? 0 : 1, rotate: dark ? -90 : 0, opacity: dark ? 0 : 1 }} transition={soft}>\n            {rays.map((a) => (\n              <line\n                key={a}\n                x1={12 + Math.cos(a) * 8.6}\n                y1={12 + Math.sin(a) * 8.6}\n                x2={12 + Math.cos(a) * 11}\n                y2={12 + Math.sin(a) * 11}\n                stroke=\"#fde047\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n              />\n            ))}\n          </motion.g>\n          <motion.circle\n            cx=\"12\"\n            cy=\"12\"\n            mask={`url(#${mask})`}\n            initial={false}\n            animate={{ r: dark ? 10 : 6.2, fill: dark ? \"#e2e8f0\" : \"#fde047\" }}\n            transition={soft}\n          />\n          <motion.g initial={false} animate={{ opacity: dark ? 1 : 0 }} transition={{ duration: reduce ? 0 : 0.3, delay: dark ? 0.2 : 0 }}>\n            <circle cx=\"8\" cy=\"14\" r=\"1.8\" fill=\"#cbd5e1\" />\n            <circle cx=\"12\" cy=\"18.5\" r=\"1.1\" fill=\"#cbd5e1\" />\n          </motion.g>\n        </motion.svg>\n        </span>\n      </motion.span>\n    </button>\n  );\n}\n"}]}