since3-website/modules/shared/ui/components/sound-toggle.tsx
2026-09-25 00:45:33 +02:00

60 lines
1.9 KiB
TypeScript

"use client"
import { useSyncExternalStore } from "react"
import { SOUND_CHANGE_EVENT, isEnabled, play, setEnabled } from "@/lib/sound"
import { cn } from "@/lib/utils"
function subscribe(onChange: () => void) {
window.addEventListener(SOUND_CHANGE_EVENT, onChange)
window.addEventListener("storage", onChange)
return () => {
window.removeEventListener(SOUND_CHANGE_EVENT, onChange)
window.removeEventListener("storage", onChange)
}
}
// Each bar gets its own speed so the equalizer looks organic
const BAR_DURATIONS = ["0.9s", "0.65s", "1.1s", "0.8s"]
/** Equalizer bars that dance while sound is on and lie flat while it is off. */
export function SoundToggle({ className }: { className?: string }) {
const enabled = useSyncExternalStore(subscribe, isEnabled, () => false)
const toggle = () => {
const next = !enabled
setEnabled(next)
// Forced, so switching off is still audible as feedback
play(next ? "on" : "off", { force: true })
}
return (
<button
type="button"
data-sound="off"
onClick={toggle}
aria-pressed={enabled}
aria-label={enabled ? "Sound ausschalten" : "Sound einschalten"}
className={cn(
"flex h-10 items-center gap-2.5 rounded-full px-2 font-mono text-label",
className
)}
>
<span aria-hidden="true" className="flex h-3.5 items-end gap-0.5">
{BAR_DURATIONS.map((duration, index) => (
<span
key={index}
style={{ animationDuration: duration, animationDelay: `${index * -0.23}s` }}
className={cn(
"h-full w-0.5 origin-bottom bg-current transition-[scale] duration-300",
enabled ? "animate-equalizer" : "scale-y-25"
)}
/>
))}
</span>
<span aria-hidden="true" className="hidden opacity-80 md:inline">
{enabled ? "Sound an" : "Sound aus"}
</span>
</button>
)
}