{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-theme-toggler",
  "type": "registry:ui",
  "title": "Theme Toggler",
  "description": "Sun↔moon morph toggle with spring physics, SVG mask crescent, and switch-click audio.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/animated-theme-toggler.tsx",
      "content": "\"use client\";\n\nimport { useState, useEffect, useRef, useId } from \"react\";\nimport { motion } from \"motion/react\";\n\n/**\n * Animated Theme Toggler — sun↔moon morph.\n *\n * Sun rays shrink and rotate away.\n * Center circle swells into a moon body.\n * A mask carves the crescent. Spring physics throughout.\n * A soft switch-click sounds on toggle.\n *\n * No shadcn. No lucide. Just motion/react.\n */\n\n/* ── Types ── */\n\nexport interface AnimatedThemeTogglerProps {\n  sound?: boolean;\n}\n\n/* ── Audio ── */\n\nlet _ctx: AudioContext | null = null;\nlet _buf: AudioBuffer | null = null;\n\nfunction audioCtx() {\n  if (!_ctx) {\n    _ctx = new (window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext })\n        .webkitAudioContext)();\n  }\n  if (_ctx.state === \"suspended\") _ctx.resume();\n  return _ctx;\n}\n\nfunction ensureBuf(ac: AudioContext): AudioBuffer {\n  if (_buf && _buf.sampleRate === ac.sampleRate) return _buf;\n  const rate = ac.sampleRate;\n  const len = Math.floor(rate * 0.006);\n  const buf = ac.createBuffer(1, len, rate);\n  const ch = buf.getChannelData(0);\n  for (let i = 0; i < len; i++) {\n    const t = i / len;\n    const sine = Math.sin(2 * Math.PI * 3400 * t);\n    const noise = Math.random() * 2 - 1;\n    ch[i] = (sine * 0.6 + noise * 0.4) * (1 - t) ** 3;\n  }\n  _buf = buf;\n  return buf;\n}\n\nfunction tick(last: React.MutableRefObject<number>) {\n  const now = performance.now();\n  if (now - last.current < 80) return;\n  last.current = now;\n  try {\n    const ac = audioCtx();\n    const buf = ensureBuf(ac);\n    const src = ac.createBufferSource();\n    const gain = ac.createGain();\n    src.buffer = buf;\n    gain.gain.value = 0.08;\n    src.connect(gain);\n    gain.connect(ac.destination);\n    src.start();\n  } catch {\n    /* silent */\n  }\n}\n\n/* ── Component ── */\n\nexport function AnimatedThemeToggler({\n  sound = true,\n}: AnimatedThemeTogglerProps) {\n  const rawId = useId();\n  const maskId = `att${rawId.replace(/:/g, \"\")}`;\n  const lastSnd = useRef(0);\n  const isFirst = useRef(true);\n  const [isDark, setIsDark] = useState(false);\n\n  useEffect(() => {\n    setIsDark(document.documentElement.classList.contains(\"dark\"));\n    requestAnimationFrame(() => {\n      isFirst.current = false;\n    });\n  }, []);\n\n  const toggle = () => {\n    const dark = document.documentElement.classList.toggle(\"dark\");\n    setIsDark(dark);\n    if (sound) tick(lastSnd);\n  };\n\n  const spring = isFirst.current\n    ? { duration: 0 }\n    : { type: \"spring\" as const, stiffness: 380, damping: 30 };\n\n  return (\n    <>\n      <style>{`\n        .att-btn{--at-ink:rgba(0,0,0,0.82)}\n        .dark .att-btn,[data-theme=\"dark\"] .att-btn{--at-ink:rgba(255,255,255,0.82)}\n      `}</style>\n      <motion.button\n        className=\"att-btn\"\n        onClick={toggle}\n        whileHover={{ scale: 1.1 }}\n        whileTap={{ scale: 0.86 }}\n        transition={{ type: \"spring\", stiffness: 400, damping: 25 }}\n        style={{\n          background: \"none\",\n          border: \"none\",\n          cursor: \"pointer\",\n          padding: 6,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: \"var(--at-ink)\",\n          borderRadius: 8,\n          outline: \"none\",\n          WebkitTapHighlightColor: \"transparent\",\n        }}\n        aria-label=\"Toggle theme\"\n      >\n        <motion.svg\n          width=\"20\"\n          height=\"20\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n          initial={false}\n          animate={{ rotate: isDark ? 270 : 0 }}\n          transition={spring}\n          style={{ overflow: \"visible\" }}\n        >\n          {/* Mask carves the crescent from the center circle */}\n          <mask id={maskId}>\n            <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\" />\n            <motion.circle\n              initial={false}\n              animate={{ cx: isDark ? 17 : 33, cy: isDark ? 8 : 0 }}\n              transition={spring}\n              r=\"9\"\n              fill=\"black\"\n            />\n          </mask>\n\n          {/* Center body — small sun circle or large crescent moon */}\n          <motion.circle\n            cx=\"12\"\n            cy=\"12\"\n            fill=\"currentColor\"\n            stroke=\"none\"\n            mask={`url(#${maskId})`}\n            initial={false}\n            animate={{ r: isDark ? 9 : 5 }}\n            transition={spring}\n          />\n\n          {/* Rays — shrink and rotate when dark */}\n          <motion.g\n            initial={false}\n            animate={{\n              opacity: isDark ? 0 : 1,\n              scale: isDark ? 0 : 1,\n              rotate: isDark ? -30 : 0,\n            }}\n            transition={spring}\n            style={{ transformOrigin: \"12px 12px\" }}\n          >\n            <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n            <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n            <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n            <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n            <line x1=\"5.64\" y1=\"5.64\" x2=\"4.22\" y2=\"4.22\" />\n            <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n            <line x1=\"5.64\" y1=\"18.36\" x2=\"4.22\" y2=\"19.78\" />\n            <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n          </motion.g>\n        </motion.svg>\n      </motion.button>\n    </>\n  );\n}\n\nexport default AnimatedThemeToggler;\n",
      "type": "registry:ui",
      "target": "components/ruixen/animated-theme-toggler.tsx"
    }
  ]
}