{ "$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) {\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 \n \n \n {/* Mask carves the crescent from the center circle */}\n \n \n \n \n\n {/* Center body — small sun circle or large crescent moon */}\n \n\n {/* Rays — shrink and rotate when dark */}\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\nexport default AnimatedThemeToggler;\n", "type": "registry:ui", "target": "components/ruixen/animated-theme-toggler.tsx" } ] }