# Sound (Lumen Halo): prompt.md (v1.0.0)

- id: `glass-sound` · version 1.0.0 · component · free
- category: Feedback
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`
- npm dependencies: @web-kits/audio@^0.2
- registry dependencies: https://beautiful-ui.dev/r/beautiful-ui.json
- docs: https://beautiful-ui.dev/components/glass-sound
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

The Lumen sound system: a provider that gives every glass component its cues and every click a soft glass tap, synthesised in the browser (no audio files), with a persisted mute toggle and a 19-cue vocabulary for presses, toggles, surfaces, values and outcomes.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `@web-kits/audio`.
- Source: `components/beautiful-ui/glass-sound.tsx`, shared code in `lib/beautiful-ui/`.
- Exports to keep: `GlassSound`, `GlassSoundProvider`, `GlassSoundToggle`, and every exported type.
- CSS: none to add. Themes, tokens and keyframes are inlined by the component (a deduped `<style>` built with `lumenThemeCss` from `lib/beautiful-ui/glass.tsx`); retheme through the `--glass-*` variables, never with Tailwind colour classes inside the component.

```tsx
import { GlassSoundProvider, GlassSoundToggle, useGlassSound } from "@/components/beautiful-ui/glass-sound";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `GlassSound` | — | The sound provider (alias of GlassSoundProvider): wrap the app once. |
| `GlassSoundProvider` | — | Plays the Lumen cues for every glass component inside it, from their data-slot and data-sound attributes. |
| `GlassSoundToggle` | — | The mute toggle, remembered across visits. |

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `GlassSoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`, import from `@/components/beautiful-ui/glass-sound`); `GlassSoundToggle` is its mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/beautiful-ui/glass-sound.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- sound, ui sounds, click sounds, audio feedback, sound effects, mute toggle, haptic-like feedback, earcons, sonic branding
- Give a product or marketing site tactile feedback: wrap the app in GlassSoundProvider and every Lumen component, and every button or link inside, answers
- A mute control in the header: GlassSoundToggle (or render your own with its render prop); the choice is saved
- Your own components: useGlassSound().play("success") for outcomes, or data-sound="copy" on a button for the click layer
- Recording product videos: sound works on every route the provider wraps

### Not when
- Pages where people work in silence for hours (dense data entry): pass clicks="components" or "off", or defaultMuted
- Notifications that must be noticed: sound is a flourish here, never the only signal (always pair it with something visible)
- Long audio, music or voice: this is a synth for 10–500ms cues

## Mistakes
- Don't mount a provider per component: one around the app (nested providers work, the innermost answers)
- Don't call play() for a click on an element that already has data-sound (or is a plain button inside a clicks="all" provider): it plays twice. Mark it data-sound="none" if you play your own cue
- Don't play sounds from effects on mount or from timers: only in answer to something the visitor did or an outcome they are waiting for
- Install @web-kits/audio (the shadcn CLI does it). It is imported lazily: pages without a provider never download it
- The provider renders one display: contents element; selectors like body > main still need adjusting if you wrap <main> directly

## Usage

```tsx
import { GlassSoundProvider, GlassSoundToggle, useGlassSound } from "@/components/beautiful-ui/glass-sound";

// app/layout.tsx (a client wrapper): sound for everything inside
<GlassSoundProvider>
  <header>
    … <GlassSoundToggle />
  </header>
  {children}
</GlassSoundProvider>

// Your own outcomes
function SaveButton() {
  const sfx = useGlassSound();
  return <button data-sound="none" onClick={async () => sfx.play((await save()) ? "success" : "error")}>Save</button>;
}

// The click layer, declaratively
<button data-sound="copy">Copy link</button>
<button data-sound="destructive">Delete</button>
<div role="tablist">{tabs.map((t, i) => <button role="tab" data-sound="select" data-sound-index={i}>{t}</button>)}</div>
<section data-sound="off">No sound in here</section>

// Quieter for a dense app: only Lumen components click, and one chart is silent
<GlassSoundProvider clicks="components" volume={0.6}>
  <GlassTrend sound={false} />
</GlassSoundProvider>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `GlassSoundProvider enabled` | `boolean` | `true` | Master switch. false: nothing plays and the engine never loads. |
| `volume` | `number` | `0.8` | 0–1, applied to every cue. |
| `muted / defaultMuted / onMutedChange` | `boolean / boolean / (muted) => void` | `— / false` | Controlled or uncontrolled mute. Uncontrolled, the saved choice wins over defaultMuted. |
| `storageKey` | `string \| null` | `"beautiful-ui-sound"` | localStorage key for the mute choice ("on" / "off"). null: don't persist. Reads and writes never throw (private mode). |
| `cues` | `Partial<Record<GlassSoundCue, GlassSoundDefinition \| false>>` |  | Replace a cue with your own synth definition, or silence it: { tick: false }. |
| `clicks` | `"all" \| "components" \| "off"` | `"all"` | The click layer. all: every button, link, tab, option, menu item, checkbox, switch, summary and [data-sound] element inside plays tap (or its data-sound cue); disabled ones play blocked. components: only inside Lumen components. off: only the cues components play themselves. |
| `spatial` | `boolean` | `true` | Pans each click slightly (±0.25) toward where it happened on screen. |
| `engine` | `GlassSoundEngine` | `@web-kits/audio, loaded on idle` | Swap the audio backend: { preload?, unlock, play(definition, options) }. |
| `data-sound (any element)` | `GlassSoundCue \| "none" \| "off"` |  | The click layer's markup API. A cue name plays that cue on press; "none" means the element plays its own; "off" silences the whole subtree. data-sound-index="n" pitches select and key up the scale. |
| `useGlassSound(sound?)` | `(GlassSoundProp) => { play(cue, { index?, pitch?, volume?, pan? }), enabled, available, muted, setMuted }` |  | Play cues from your components. Without a provider play() is a silent no-op. |
| `useGlassSoundOnChange(value, cue, sound?)` | `hook` |  | Plays a cue when a prop changes to a new value (an error arriving, a status turning success), never on mount. |
| `GlassSoundToggle` | `{ size = 32, theme, labels, render?, …button props }` |  | The glass mute button (aria-pressed = sound on). Unmuting answers with a swoosh. render={({ muted, toggle, available }) => …} for your own control. Renders nothing without a provider. |
| `sound (every Lumen component)` | `boolean \| "subtle"` | `true` | Per instance: false silences it (its cues and its clicks), "subtle" plays at 55%. |

Full docs: https://beautiful-ui.dev/components/glass-sound

## Customising
- Colours: the family variables (`--glass-accent`, `--glass-good`, `--glass-series-1` …) on the component or any ancestor; the neutral glass is `--lg-*` in `lib/beautiful-ui/glass.tsx`.
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| Enter / Space (toggle) | Turn sound on or off |
| Enter / Space (anything inside) | Plays the element's cue, like a click |

## Motion inventory

| Interaction | What moves |
|---|---|
| Presses | tap: a 34ms F#6 glint with a falling edge (±40 cents of variation so repeats never sound the same); select: D6 up the D major pentatonic by position; destructive: a soft low knock |
| Toggles and surfaces | toggleOn rises B5 → E6, toggleOff falls; open rises with a breath of air, close falls, swoosh is filtered noise sweeping up |
| Values | tick (12ms D7) and sliderTick (8ms, the quietest) for scrubbing and arrow keys; key pitches OTP digits by position |
| Outcomes and named | success: a fifth up; error: two soft knocks down; warning: the same note twice; copy: a quick glint up; notification: a soft D major bell; chirp, command and blocked |
| Restraint | Per-cue throttles (35ms taps, 25–45ms ticks), rapid sequences damp to 45%, nothing on hover or mount. The toggle's icon waves fade in and out (.3s); reduced motion makes it instant |

## Accessibility contract (preserve when editing)
- Sound is always an extra, never the only signal: every cue accompanies a visible change
- GlassSoundToggle is a toggle button (aria-pressed = sound on) with an accessible name and a tooltip title; focus ring as every glass control
- Nothing plays before the visitor interacts with the page (browser autoplay rules), or while the tab is hidden
- Keyboard activation (Enter / Space) sounds like a click; arrow navigation inside components uses the quietest cues
- Motion settings do not mute sound (they are separate preferences): offer the toggle

## Install

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json
```

## Credits
- Sound by @web-kits/audio (https://www.npmjs.com/package/@web-kits/audio)

## Source (Base UI build)

### components/beautiful-ui/glass-sound.tsx

```tsx
"use client";

/**
 * Sound (Lumen Halo) v1.0.0 · Beautiful UI
 * Docs: https://beautiful-ui.dev/components/glass-sound · AI guide: ./glass-sound.AI.md
 * Licensed to the purchaser under the Beautiful UI license: https://beautiful-ui.dev/license
 */

/*
 * Glass Sound: the Lumen sound system. Wrap your app once and every Lumen component answers with
 * its cues, and every click inside with a soft tap. The engine (a Web Audio synth, no audio files)
 * loads only when a provider is mounted and sound is on, and nothing plays before the first gesture.
 */

import {
  GlassSoundProvider,
  type GlassSoundProviderProps,
} from "@/lib/beautiful-ui/glass-sound";

export {
  GLASS_SOUND_CUES,
  GLASS_SOUND_GROUPS,
  GLASS_SOUND_INTERACTIVE,
  GLASS_SOUND_PATCH,
  GLASS_SOUND_STORAGE_KEY,
  GLASS_SOUND_THROTTLE,
  GlassSoundIcon,
  GlassSoundProvider,
  GlassSoundToggle,
  createGlassSoundController,
  createWebKitsEngine,
  defaultGlassSoundToggleLabels,
  glassPentatonicStep,
  glassSoundForElement,
  glassSoundRootAttrs,
  isGlassSoundCue,
  readGlassSoundMuted,
  resolveGlassCue,
  useGlassSound,
  useGlassSoundOnChange,
  writeGlassSoundMuted,
} from "@/lib/beautiful-ui/glass-sound";
export type {
  GlassSoundClicks,
  GlassSoundController,
  GlassSoundCue,
  GlassSoundDefinition,
  GlassSoundEngine,
  GlassSoundGroup,
  GlassSoundOverrides,
  GlassSoundPlayOptions,
  GlassSoundProp,
  GlassSoundProviderProps,
  GlassSoundToggleLabels,
  GlassSoundToggleProps,
  GlassSoundToggleState,
} from "@/lib/beautiful-ui/glass-sound";

/** Same as {@link GlassSoundProvider}: wrap your app (or one section) to turn Lumen sound on. */
export function GlassSound(props: GlassSoundProviderProps) {
  return <GlassSoundProvider {...props} />;
}
```

### lib/beautiful-ui/glass-sound.tsx

```tsx
"use client";

/*
 * Lumen sound: the voice of the glass family. A thin layer over a Web Audio synthesiser
 * (@web-kits/audio today). Components never import the engine; they call play(cue) or mark
 * elements with data-sound, and this file decides what that sounds like.
 *
 *   - No provider, no sound: play() is a silent no-op and the engine is never loaded.
 *   - Nothing plays, and no AudioContext exists, before the first user gesture.
 *   - Mute persists in localStorage ("beautiful-ui-sound"), SSR safe, never throws.
 *   - The click layer: inside the provider every click on something interactive answers with a
 *     cue. data-sound="<cue>" picks a richer one, data-sound="none" means the element plays its
 *     own, data-sound="off" silences a whole subtree.
 *
 * The voice: sine glints with a quiet inharmonic partial (×2.756, a struck glass bar), 2ms
 * attacks, no reverb, all in D major pentatonic around D6. Short, soft, never harsh.
 */

import * as React from "react";
import type { Layer, PlayOptions, SoundDefinition } from "@web-kits/audio";
import { GLASS_FOCUS, GlassThemeStyle, mergeLabels, type GlassLabels, type GlassTheme } from "@/lib/beautiful-ui/glass";

// ---------------------------------------------------------------------------------------------
// Vocabulary
// ---------------------------------------------------------------------------------------------

/** Every cue in the Lumen vocabulary, grouped as they are documented. */
export const GLASS_SOUND_GROUPS = {
  presses: ["tap", "select", "destructive"],
  toggles: ["toggleOn", "toggleOff"],
  surfaces: ["open", "close", "swoosh"],
  values: ["tick", "sliderTick", "key"],
  outcomes: ["success", "error", "warning"],
  named: ["copy", "notification", "chirp", "command", "blocked", "lock", "halt"],
} as const;

export type GlassSoundGroup = keyof typeof GLASS_SOUND_GROUPS;
export type GlassSoundCue = (typeof GLASS_SOUND_GROUPS)[GlassSoundGroup][number];
export const GLASS_SOUND_CUES = Object.values(GLASS_SOUND_GROUPS).flat() as GlassSoundCue[];

export function isGlassSoundCue(value: unknown): value is GlassSoundCue {
  return typeof value === "string" && (GLASS_SOUND_CUES as string[]).includes(value);
}

/** One sound, as the engine describes it (layers, envelope, filters). */
export type GlassSoundDefinition = SoundDefinition;

/** What a cue can be told when it plays. */
export interface GlassSoundPlayOptions {
  /** Position in a group (tabs, options, OTP digits): select and key step up D major pentatonic. */
  index?: number;
  /** Pitch shift in semitones (can be fractional). */
  pitch?: number;
  /** Volume multiplier, 0–1. */
  volume?: number;
  /** Stereo position, −1 (left) to 1 (right). */
  pan?: number;
  /** Skip the per-cue throttle (for sequences you schedule yourself). */
  force?: boolean;
  /** Which theme's patch plays the cue. Default "lumen"; the click layer reads data-sound-voice. */
  voice?: GlassSoundVoice;
}

/**
 * A theme's voice: the same cue vocabulary, patched in that theme's sound. Components mark their
 * root with data-sound-voice, so one provider plays each theme in its own voice.
 */
export type GlassSoundVoice = "lumen" | "analogue";
export const GLASS_SOUND_VOICES: readonly GlassSoundVoice[] = ["lumen", "analogue"];
export const isGlassSoundVoice = (value: unknown): value is GlassSoundVoice => value === "lumen" || value === "analogue";

/** A component's `sound` prop: true (default) plays when a provider enables sound, false never, "subtle" at 55%. */
export type GlassSoundProp = boolean | "subtle";

// ---------------------------------------------------------------------------------------------
// The Lumen voice
// ---------------------------------------------------------------------------------------------

const GLASS_RATIO = 2.756; // first overtone of a free glass bar: what makes a sine sound struck
type Freq = number | { start: number; end: number };
const scale = (f: Freq, k: number): Freq => (typeof f === "number" ? f * k : { start: f.start * k, end: f.end * k });

/** A struck glass note: the fundamental and a quiet, faster-fading overtone. */
function glint(freq: Freq, decay: number, gain: number, { delay = 0, attack = 0.002, ring = 0.16 } = {}): Layer[] {
  const layers: Layer[] = [{ source: { type: "sine", frequency: freq }, envelope: { attack, decay }, gain, delay }];
  if (ring > 0) layers.push({ source: { type: "sine", frequency: scale(freq, GLASS_RATIO) }, envelope: { attack, decay: decay * 0.42 }, gain: gain * ring, delay });
  return layers;
}

/** A breath of filtered noise: the air a surface moves. */
function air(from: number, to: number, attack: number, decay: number, gain: number, delay = 0): Layer {
  return {
    source: { type: "noise", color: "pink" },
    filter: { type: "bandpass", frequency: from, resonance: 1.1, envelope: { attack, peak: to, decay } },
    envelope: { attack, decay },
    gain,
    delay,
  };
}

/** A soft, rounded low tone (errors, blocked, destructive): triangle through a lowpass. */
function knock(freq: Freq, decay: number, gain: number, delay = 0, cutoff = 1500): Layer {
  return { source: { type: "triangle", frequency: freq }, filter: { type: "lowpass", frequency: cutoff, resonance: 0.4 }, envelope: { attack: 0.003, decay }, gain, delay };
}

// D major pentatonic around D6: the key every Lumen cue lives in.
const D5 = 587.33, E5 = 659.26, A5 = 880, B5 = 987.77, D6 = 1174.66, E6 = 1318.51, Fs6 = 1479.98, A6 = 1760, D7 = 2349.32, E7 = 2637.02;

/** The designed Lumen patch. Levels are before the provider volume (default 0.8). */
export const GLASS_SOUND_PATCH: Record<GlassSoundCue, GlassSoundDefinition> = {
  // Presses
  tap: { layers: [...glint({ start: 1540, end: Fs6 }, 0.034, 0.05, { ring: 0.12 }), { source: { type: "noise", color: "white" }, filter: { type: "highpass", frequency: 6000 }, envelope: { attack: 0.001, decay: 0.006 }, gain: 0.006 }] },
  select: { layers: glint(D6, 0.055, 0.045, { ring: 0.2 }) },
  destructive: { layers: [knock({ start: E5, end: D5 }, 0.1, 0.07), ...glint(A5 / 2, 0.08, 0.03, { ring: 0 })] },
  // Toggles
  toggleOn: { layers: [...glint(B5, 0.038, 0.04), ...glint(E6, 0.07, 0.045, { delay: 0.048 })] },
  toggleOff: { layers: [...glint(E6, 0.038, 0.04), ...glint(B5, 0.07, 0.04, { delay: 0.048 })] },
  // Surfaces
  open: { layers: [{ source: { type: "sine", frequency: { start: 740, end: D6 } }, envelope: { attack: 0.012, decay: 0.11 }, gain: 0.035 }, air(2200, 5200, 0.02, 0.09, 0.012)] },
  close: { layers: [{ source: { type: "sine", frequency: { start: D6, end: 700 } }, envelope: { attack: 0.006, decay: 0.09 }, gain: 0.03 }, air(3600, 1600, 0.008, 0.08, 0.01)] },
  swoosh: { layers: [air(900, 4200, 0.05, 0.17, 0.045), ...glint(A6, 0.09, 0.012, { delay: 0.07, ring: 0 })] },
  // Values
  tick: { layers: glint(D7, 0.012, 0.016, { ring: 0 }) },
  sliderTick: { layers: glint(E7, 0.008, 0.009, { ring: 0 }) },
  key: { layers: glint(A5 * 1.5, 0.035, 0.03, { ring: 0.12 }) },
  // Outcomes
  success: { layers: [...glint(D6, 0.12, 0.04), ...glint(A6, 0.26, 0.045, { delay: 0.075, ring: 0.22 })] },
  error: { layers: [knock(E5, 0.09, 0.05, 0, 1600), knock(D5 * 0.89, 0.15, 0.05, 0.085, 1400)] },
  warning: { layers: [...glint(A5, 0.06, 0.04), ...glint(A5, 0.08, 0.035, { delay: 0.1 })] },
  // Named
  copy: { layers: [...glint(A6, 0.045, 0.035), ...glint(D7, 0.09, 0.03, { delay: 0.035 })] },
  notification: { layers: [...glint(D6, 0.5, 0.028, { attack: 0.004 }), ...glint(Fs6, 0.45, 0.02, { delay: 0.02, attack: 0.004 }), ...glint(A6, 0.5, 0.018, { delay: 0.04, attack: 0.004 })] },
  chirp: { layers: [{ source: { type: "sine", frequency: { start: 1300, end: 2200 } }, envelope: { attack: 0.004, decay: 0.05 }, gain: 0.028 }] },
  command: { layers: [...glint(A5, 0.08, 0.035), ...glint(E6, 0.08, 0.025, { ring: 0 }), ...glint(A6, 0.14, 0.032, { delay: 0.045 })] },
  blocked: { layers: [knock(330, 0.05, 0.055, 0, 900), knock(330, 0.06, 0.04, 0.07, 900)] },
  // A mechanical seat (Analogue plate presses) and a stopped run (Analogue signal outcomes), in glass.
  lock: { layers: [...glint(Fs6, 0.02, 0.04, { ring: 0 }), ...glint(A5, 0.04, 0.04, { delay: 0.02, ring: 0.1 })] },
  halt: { layers: [knock(D5 / 2, 0.06, 0.06, 0, 1000), knock(D5 / 2.2, 0.07, 0.05, 0.07, 1000)] },
};

// ---------------------------------------------------------------------------------------------
// The Analogue voice: dry, mechanical, single partials. Sine sweeps and 10ms noise ticks with a 3ms
// linear attack and an exponential release, peaking at −18 dBFS (the Analogue Button handoff).
// ---------------------------------------------------------------------------------------------

const sweep = (from: number, to: number, len: number, gain: number, delay = 0): Layer => ({
  source: { type: "sine", frequency: from === to ? from : { start: from, end: to } },
  envelope: { attack: 0.003, decay: len },
  gain,
  delay,
});
const noiseTick = (gain: number, delay = 0): Layer => ({
  source: { type: "noise", color: "white" },
  filter: { type: "highpass", frequency: 3200 },
  envelope: { attack: 0.001, decay: 0.01 },
  gain,
  delay,
});

/** The Analogue patch. Cues it does not define fall back to the Lumen patch. */
export const ANALOGUE_SOUND_PATCH: Partial<Record<GlassSoundCue, GlassSoundDefinition>> = {
  // tap: 1200→1150 Hz, 25ms. tick: 10ms noise through a 3.2 kHz highpass.
  tap: { layers: [sweep(1200, 1150, 0.025, 0.1)] },
  select: { layers: [noiseTick(0.28)] },
  tick: { layers: [noiseTick(0.28)] },
  sliderTick: { layers: [noiseTick(0.16)] },
  key: { layers: [noiseTick(0.22)] },
  // low tap: 520→440 Hz, 45ms (stop and destroy).
  destructive: { layers: [sweep(520, 440, 0.045, 0.12)] },
  // click-lock: 1.4 kHz for 12ms, then 900 Hz for 20ms at +20ms (the plate seating).
  lock: { layers: [sweep(1400, 1300, 0.012, 0.08), sweep(900, 880, 0.02, 0.08, 0.02)] },
  toggleOn: { layers: [sweep(900, 880, 0.02, 0.07), sweep(1400, 1300, 0.012, 0.07, 0.02)] },
  toggleOff: { layers: [sweep(1400, 1300, 0.012, 0.07), sweep(900, 880, 0.02, 0.07, 0.02)] },
  // rise: 330→990 Hz, 120ms (work finished).
  success: { layers: [sweep(330, 990, 0.12, 0.05)] },
  // low double: 196→180 Hz, 50ms, twice, 70ms apart (a run stopped, something failed).
  halt: { layers: [sweep(196, 180, 0.05, 0.12), sweep(196, 180, 0.05, 0.12, 0.07)] },
  error: { layers: [sweep(196, 180, 0.05, 0.12), sweep(196, 180, 0.05, 0.12, 0.07)] },
  blocked: { layers: [sweep(260, 240, 0.03, 0.07)] },
  // pulse: 523 then 784 Hz, 50ms each (a message arrived).
  notification: { layers: [sweep(523, 523, 0.05, 0.06), sweep(784, 784, 0.05, 0.06, 0.06)] },
};

/** Minimum gap between two plays of the same cue (ms). Repeats inside it are dropped. */
export const GLASS_SOUND_THROTTLE: Record<GlassSoundCue, number> = {
  tap: 35, select: 35, destructive: 80, toggleOn: 60, toggleOff: 60, open: 80, close: 80, swoosh: 120,
  tick: 40, sliderTick: 45, key: 25, success: 150, error: 150, warning: 150, copy: 80, notification: 200,
  chirp: 60, command: 100, blocked: 90, lock: 40, halt: 150,
};

/** Tiny per-play variation so repeats never feel mechanical: [cents, volume]. */
const JITTER: Partial<Record<GlassSoundCue, [number, number]>> = {
  tap: [40, 0.08], select: [8, 0.06], tick: [14, 0.1], sliderTick: [14, 0.1], key: [6, 0.06], chirp: [20, 0.05],
};

const PENTATONIC = [0, 2, 4, 7, 9]; // semitones: D E F# A B

/** Semitones for the nth step of the pentatonic scale (wraps up an octave every 5, max two octaves). */
export function glassPentatonicStep(index: number): number {
  const i = Math.max(0, Math.min(10, Math.floor(Number.isFinite(index) ? index : 0)));
  return PENTATONIC[i % 5] + 12 * Math.floor(i / 5);
}

/** The engine call a cue resolves to: definition, detune (cents), volume and pan. Pure, for tests. */
export function resolveGlassCue(
  cue: GlassSoundCue,
  opts: GlassSoundPlayOptions = {},
  { overrides, volume = 0.8 }: { overrides?: GlassSoundOverrides; volume?: number } = {},
): { definition: GlassSoundDefinition; options: PlayOptions } | null {
  const override = overrides?.[cue];
  if (override === false) return null;
  const analogue = opts.voice === "analogue";
  const definition = override ?? (analogue ? ANALOGUE_SOUND_PATCH[cue] : undefined) ?? GLASS_SOUND_PATCH[cue];
  if (!definition) return null;
  let cents = (opts.pitch ?? 0) * 100;
  // Analogue is mechanical: no pentatonic steps and no jitter.
  if (!analogue && opts.index !== undefined && (cue === "select" || cue === "key")) cents += glassPentatonicStep(opts.index) * 100;
  const v = clamp01(volume) * clamp01(opts.volume ?? 1);
  const jitter = analogue ? undefined : JITTER[cue];
  const options: PlayOptions = { volume: v };
  if (cents) options.detune = cents;
  if (opts.pan) options.pan = Math.max(-1, Math.min(1, opts.pan));
  if (jitter) options.jitter = { detune: jitter[0], volume: jitter[1] };
  return { definition, options };
}

const clamp01 = (n: number) => (Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1);

// ---------------------------------------------------------------------------------------------
// Engine: lazily loaded, swappable
// ---------------------------------------------------------------------------------------------

/** The audio backend. Swap it (tests, another synth) through the provider's `engine` prop. */
export interface GlassSoundEngine {
  /** Start loading code (no audio). Called when the page is idle. */
  preload?(): void;
  /** Create or resume the audio context. Only ever called inside a user gesture. */
  unlock(): void;
  /** Play a definition. Must not throw. */
  play(definition: GlassSoundDefinition, options: PlayOptions): void;
}

type WebKits = typeof import("@web-kits/audio");

/** The default engine: @web-kits/audio, imported on demand so pages without sound never load it. */
export function createWebKitsEngine(): GlassSoundEngine {
  let mod: WebKits | null = null;
  let loading: Promise<WebKits | null> | null = null;
  let unlocked = false;
  // The first cue can arrive before the engine has loaded: keep the latest one and play it when ready.
  let pending: [GlassSoundDefinition, PlayOptions] | null = null;
  const players = new WeakMap<GlassSoundDefinition, (o?: PlayOptions) => unknown>();
  const playNow = (m: WebKits, definition: GlassSoundDefinition, options: PlayOptions) => {
    try {
      let p = players.get(definition);
      if (!p) {
        p = m.defineSound(definition);
        players.set(definition, p);
      }
      p(options);
    } catch {}
  };
  const flush = (m: WebKits) => {
    const next = pending;
    pending = null;
    if (next) playNow(m, next[0], next[1]);
  };
  const load = () =>
    (loading ??= import("@web-kits/audio")
      .then((m) => (mod = m))
      .catch(() => null));
  const ready = (m: WebKits) => {
    try {
      m.ensureReady().catch(() => {});
    } catch {}
  };
  return {
    preload() {
      if (hasWebAudio()) void load();
    },
    unlock() {
      if (unlocked || !hasWebAudio()) return;
      unlocked = true;
      if (mod) ready(mod);
      else void load().then((m) => m && ready(m));
    },
    play(definition, options) {
      if (!unlocked) return;
      if (!mod) {
        pending = [definition, options];
        void load().then((m) => {
          if (!m) return;
          try {
            m.ensureReady().then(() => flush(m), () => flush(m));
          } catch {
            flush(m);
          }
        });
        return;
      }
      playNow(mod, definition, options);
    },
  };
}

export function hasWebAudio(): boolean {
  try {
    return typeof window !== "undefined" && typeof window.AudioContext === "function";
  } catch {
    return false;
  }
}

// ---------------------------------------------------------------------------------------------
// Controller: gesture gate, throttle, rapid-fire damping. Framework free, for tests.
// ---------------------------------------------------------------------------------------------

export interface GlassSoundControllerOptions {
  engine: GlassSoundEngine;
  now?: () => number;
}

export interface GlassSoundController {
  /** Call from a user gesture: unlocks the engine. Plays before it are dropped. */
  unlock(): void;
  readonly unlocked: boolean;
  play(cue: GlassSoundCue, opts?: GlassSoundPlayOptions, config?: { overrides?: GlassSoundOverrides; volume?: number }): boolean;
}

export function createGlassSoundController({ engine, now = () => Date.now() }: GlassSoundControllerOptions): GlassSoundController {
  let unlocked = false;
  const last = new Map<string, number>();
  const recent: number[] = [];
  return {
    get unlocked() {
      return unlocked;
    },
    unlock() {
      if (unlocked) return;
      unlocked = true;
      try {
        engine.unlock();
      } catch {}
    },
    play(cue, opts = {}, config = {}) {
      if (!unlocked || !isGlassSoundCue(cue)) return false;
      const t = now();
      const key = opts.index === undefined ? cue : `${cue}:${opts.index}`;
      if (!opts.force && t - (last.get(key) ?? -Infinity) < GLASS_SOUND_THROTTLE[cue]) return false;
      last.set(key, t);
      // Rapid sequences get quieter: every play in the last 600ms takes 12% off, down to 45%.
      while (recent.length && t - recent[0] > 600) recent.shift();
      const damp = Math.max(0.45, 1 - 0.12 * recent.length);
      recent.push(t);
      const resolved = resolveGlassCue(cue, { ...opts, volume: (opts.volume ?? 1) * damp }, config);
      if (!resolved) return false;
      try {
        engine.play(resolved.definition, resolved.options);
      } catch {}
      return true;
    },
  };
}

// ---------------------------------------------------------------------------------------------
// Mute persistence
// ---------------------------------------------------------------------------------------------

export const GLASS_SOUND_STORAGE_KEY = "beautiful-ui-sound";
const muteListeners = new Set<() => void>();

/** The saved choice: true (muted), false (on) or null (never chosen). Never throws. */
export function readGlassSoundMuted(key: string | null = GLASS_SOUND_STORAGE_KEY): boolean | null {
  if (!key) return null;
  try {
    const v = globalThis.localStorage?.getItem(key);
    return v === "off" ? true : v === "on" ? false : null;
  } catch {
    return null;
  }
}

/** Saves the choice ("on" / "off") and tells every provider on the page. Never throws. */
export function writeGlassSoundMuted(muted: boolean, key: string | null = GLASS_SOUND_STORAGE_KEY) {
  if (key) {
    try {
      globalThis.localStorage?.setItem(key, muted ? "off" : "on");
    } catch {}
  }
  for (const l of muteListeners) l();
}

function subscribeMuted(listener: () => void, key: string | null = GLASS_SOUND_STORAGE_KEY) {
  muteListeners.add(listener);
  // Another tab changed this provider's key (or cleared storage).
  const onStorage = (e: StorageEvent) => {
    if (!e.key || e.key === key) listener();
  };
  try {
    window.addEventListener("storage", onStorage);
  } catch {}
  return () => {
    muteListeners.delete(listener);
    try {
      window.removeEventListener("storage", onStorage);
    } catch {}
  };
}

// ---------------------------------------------------------------------------------------------
// The click layer
// ---------------------------------------------------------------------------------------------

/** What the click layer treats as pressable. Anything with data-sound counts too. */
export const GLASS_SOUND_INTERACTIVE =
  'button,a[href],summary,input[type="checkbox"],input[type="radio"],input[type="button"],input[type="submit"],input[type="reset"],select,[role="button"],[role="tab"],[role="option"],[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"],[role="checkbox"],[role="switch"],[role="radio"],[role="link"],[role="gridcell"][tabindex],[data-sound]';

/** "all": every interactive element; "components": only inside Lumen components; "off": only cues components play. */
export type GlassSoundClicks = "all" | "components" | "off";

/**
 * The cue a press on `target` should play, or null. Pure DOM reads, for tests and custom layers.
 * data-sound="off" on an ancestor silences, "none" means the element plays its own cue,
 * data-sound-index gives select / key their pitch.
 */
export function glassSoundForElement(
  target: Element | null,
  clicks: GlassSoundClicks = "all",
): { cue: GlassSoundCue; index?: number; el: Element; subtle: boolean; voice?: GlassSoundVoice } | null {
  if (!target || clicks === "off" || typeof target.closest !== "function") return null;
  if (target.closest('[data-sound="off"]')) return null;
  const el = target.closest(GLASS_SOUND_INTERACTIVE);
  if (!el) return null;
  // Hidden proxy inputs (Base UI and Radix click one for form state after the visible control) never
  // sound: the control the person pressed already did.
  if (el.matches('input[aria-hidden="true"]')) return null;
  if (clicks === "components" && !el.closest("[data-lumen],[data-analogue],[data-sound]")) return null;
  const subtle = Boolean(el.closest('[data-sound-level="subtle"]'));
  const voiceAttr = el.closest("[data-sound-voice]")?.getAttribute("data-sound-voice");
  const voice = isGlassSoundVoice(voiceAttr) ? voiceAttr : undefined;
  const marked = el.getAttribute("data-sound");
  if (marked === "off") return null;
  // A disabled control answers "blocked" even when it normally plays its own cue (a radio disabled by its group).
  const disabled = el.matches(":disabled") || el.getAttribute("aria-disabled") === "true" || el.hasAttribute("data-disabled");
  if (disabled) return { cue: "blocked", el, subtle, voice };
  if (marked === "none") return null;
  // A busy button ignores the press, so it stays silent too.
  if (el.getAttribute("aria-busy") === "true" || el.hasAttribute("data-inert")) return null;
  if (marked && isGlassSoundCue(marked)) {
    const idx = Number(el.getAttribute("data-sound-index"));
    return { cue: marked, index: el.hasAttribute("data-sound-index") && Number.isFinite(idx) ? idx : undefined, el, subtle, voice };
  }
  // Text fields and selects answer to typing, not to being clicked into.
  if (el.matches("select")) return null;
  return { cue: "tap", el, subtle, voice };
}

/** Attributes for a component root: data-sound="off" when sound={false}, the subtle level when "subtle". */
export function glassSoundRootAttrs(sound: GlassSoundProp | undefined): Record<string, string> {
  if (sound === false) return { "data-sound": "off" };
  if (sound === "subtle") return { "data-sound-level": "subtle" };
  return {};
}

// ---------------------------------------------------------------------------------------------
// React
// ---------------------------------------------------------------------------------------------

/** Replace any cue with your own definition, or silence it with false. */
export type GlassSoundOverrides = Partial<Record<GlassSoundCue, GlassSoundDefinition | false>>;

interface GlassSoundContextValue {
  play: (cue: GlassSoundCue, opts?: GlassSoundPlayOptions) => boolean;
  /** Unlock inside a gesture and play even while muted (the toggle's own answer). */
  confirm: (cue: GlassSoundCue) => void;
  muted: boolean;
  enabled: boolean;
  setMuted: (muted: boolean) => void;
}

const GlassSoundContext = React.createContext<GlassSoundContextValue | null>(null);

export interface GlassSoundProviderProps {
  children?: React.ReactNode;
  /** Master switch. false: nothing plays and the engine never loads. Default true. */
  enabled?: boolean;
  /** 0–1. Default 0.8. */
  volume?: number;
  /** Controlled mute (with onMutedChange). Uncontrolled, the saved choice or defaultMuted is used. */
  muted?: boolean;
  /** Muted until the visitor turns sound on. Default false. */
  defaultMuted?: boolean;
  onMutedChange?: (muted: boolean) => void;
  /** localStorage key for the mute choice. null: don't persist. Default "beautiful-ui-sound". */
  storageKey?: string | null;
  /** Replace or silence cues: { tap: myDefinition, tick: false }. */
  cues?: GlassSoundOverrides;
  /** The click layer: "all" (every button, link, tab… inside), "components" (only inside Lumen components) or "off". Default "all". */
  clicks?: GlassSoundClicks;
  /** Pan clicks slightly toward where they happened on screen (±0.25). Default true. */
  spatial?: boolean;
  /** Swap the audio backend. Default: @web-kits/audio, loaded on idle. */
  engine?: GlassSoundEngine;
}

/**
 * Turns sound on for everything inside. Renders one `display: contents` element to scope the
 * click layer, so layout is untouched.
 */
export function GlassSoundProvider({
  children,
  enabled = true,
  volume = 0.8,
  muted: mutedProp,
  defaultMuted = false,
  onMutedChange,
  storageKey = GLASS_SOUND_STORAGE_KEY,
  cues,
  clicks = "all",
  spatial = true,
  engine: engineProp,
}: GlassSoundProviderProps) {
  const [controller] = React.useState(() => {
    const engine = engineProp ?? createWebKitsEngine();
    return { engine, ctl: createGlassSoundController({ engine }) };
  });
  const subscribe = React.useCallback((l: () => void) => subscribeMuted(l, storageKey), [storageKey]);
  const stored = React.useSyncExternalStore(
    subscribe,
    () => readGlassSoundMuted(storageKey),
    () => null,
  );
  // Without a storage key the choice lives here (uncontrolled), so the toggle still works.
  const [own, setOwn] = React.useState<boolean | null>(null);
  const muted = mutedProp ?? (storageKey ? stored : own) ?? defaultMuted;
  const active = enabled && !muted;

  const config = React.useRef({ cues, volume, active, clicks, spatial });
  React.useEffect(() => {
    config.current = { cues, volume, active, clicks, spatial };
  });

  // Unlock on the first gesture anywhere on the page; preload the engine when idle.
  React.useEffect(() => {
    if (!active || typeof window === "undefined") return;
    const { ctl, engine } = controller;
    let idle: number | undefined;
    try {
      const ric = (window as Window & { requestIdleCallback?: (cb: () => void, o?: { timeout: number }) => number }).requestIdleCallback;
      idle = ric ? ric(() => engine.preload?.(), { timeout: 3000 }) : window.setTimeout(() => engine.preload?.(), 1200);
    } catch {}
    if (ctl.unlocked) return;
    const events = ["pointerdown", "keydown", "touchend"] as const;
    const onGesture = (e: Event) => {
      if (!e.isTrusted) return;
      ctl.unlock();
      for (const ev of events) window.removeEventListener(ev, onGesture, true);
    };
    for (const ev of events) window.addEventListener(ev, onGesture, true);
    return () => {
      for (const ev of events) window.removeEventListener(ev, onGesture, true);
      try {
        const cic = (window as Window & { cancelIdleCallback?: (id: number) => void }).cancelIdleCallback;
        if (idle !== undefined) (cic ?? window.clearTimeout)(idle);
      } catch {}
    };
  }, [active, controller]);

  const play = React.useCallback(
    (cue: GlassSoundCue, opts?: GlassSoundPlayOptions) => {
      const c = config.current;
      if (!c.active) return false;
      try {
        if (typeof document !== "undefined" && document.visibilityState === "hidden") return false;
        return controller.ctl.play(cue, opts, { overrides: c.cues, volume: c.volume });
      } catch {
        return false;
      }
    },
    [controller],
  );

  const setMuted = React.useCallback(
    (next: boolean) => {
      // Controlled: the parent owns it. Uncontrolled: persist it, or keep it here without a key.
      if (mutedProp === undefined) {
        if (storageKey) writeGlassSoundMuted(next, storageKey);
        else setOwn(next);
      }
      onMutedChange?.(next);
    },
    [mutedProp, storageKey, onMutedChange],
  );

  const confirm = React.useCallback(
    (cue: GlassSoundCue) => {
      const c = config.current;
      if (!enabled) return;
      try {
        controller.ctl.unlock();
        controller.ctl.play(cue, { force: true }, { overrides: c.cues, volume: c.volume });
      } catch {}
    },
    [controller, enabled],
  );

  const value = React.useMemo<GlassSoundContextValue>(() => ({ play, confirm, muted, enabled, setMuted }), [play, confirm, muted, enabled, setMuted]);

  // The click layer. Mouse and pen answer on press (tactile, no latency); touch waits for the
  // click so scrolling stays silent; keyboard activation (click with detail 0) answers too.
  const pressed = React.useRef<Element | null>(null);
  const wrap = React.useRef<HTMLDivElement>(null);
  // Nested providers: the innermost one around the target answers (a portal outside every
  // provider's DOM goes to the first that sees it). The event is marked so nothing plays twice.
  const owns = (e: React.SyntheticEvent) => {
    const ne = e.nativeEvent as Event & { __glassSound?: boolean };
    const t = e.target as Element | null;
    const nearest = t && typeof t.closest === "function" ? t.closest("[data-glass-sound]") : null;
    if ((nearest && nearest !== wrap.current) || ne.__glassSound) return false;
    ne.__glassSound = true;
    return true;
  };
  const fire = (target: EventTarget | null, x?: number) => {
    const c = config.current;
    if (!c.active || c.clicks === "off") return;
    const hit = glassSoundForElement(target as Element | null, c.clicks);
    if (!hit) return;
    let pan: number | undefined;
    if (c.spatial && typeof window !== "undefined") {
      try {
        const cx = x ?? (() => {
          const r = hit.el.getBoundingClientRect();
          return r.left + r.width / 2;
        })();
        pan = ((cx / Math.max(1, window.innerWidth)) * 2 - 1) * 0.25;
      } catch {}
    }
    play(hit.cue, { index: hit.index, pan, volume: hit.subtle ? 0.55 : 1, voice: hit.voice });
  };

  // Outcome cues from components without audio code: they dispatch a bubbling `beautiful-ui:sound`
  // event ({ cue }) and the provider around them plays it, in the voice their root declares.
  React.useEffect(() => {
    const el = wrap.current;
    if (!el) return;
    const onCue = (e: Event) => {
      const ev = e as CustomEvent<{ cue?: unknown; index?: unknown }> & { __glassSound?: boolean };
      const target = e.target as Element | null;
      const nearest = target && typeof target.closest === "function" ? target.closest("[data-glass-sound]") : null;
      if ((nearest && nearest !== el) || ev.__glassSound) return;
      ev.__glassSound = true;
      const cue = ev.detail?.cue;
      if (!isGlassSoundCue(cue) || !target || target.closest('[data-sound="off"]')) return;
      const voiceAttr = target.closest("[data-sound-voice]")?.getAttribute("data-sound-voice");
      const subtle = Boolean(target.closest('[data-sound-level="subtle"]'));
      // index pitches "select" and "key" by position (a radio's option, a list's row).
      const index = typeof ev.detail?.index === "number" && Number.isFinite(ev.detail.index) ? ev.detail.index : undefined;
      play(cue, { voice: isGlassSoundVoice(voiceAttr) ? voiceAttr : undefined, volume: subtle ? 0.55 : 1, index });
    };
    el.addEventListener("beautiful-ui:sound", onCue);
    // A component portalled outside every provider (a dialog, a popover) still sounds: the first
    // mounted provider answers events whose target has no provider around it.
    ORPHAN_OWNERS.push(el);
    const onOrphan = (e: Event) => {
      const target = e.target as Element | null;
      if (ORPHAN_OWNERS[0] !== el || !target || typeof target.closest !== "function" || target.closest("[data-glass-sound]")) return;
      onCue(e);
    };
    document.addEventListener("beautiful-ui:sound", onOrphan);
    return () => {
      el.removeEventListener("beautiful-ui:sound", onCue);
      document.removeEventListener("beautiful-ui:sound", onOrphan);
      const i = ORPHAN_OWNERS.indexOf(el);
      if (i >= 0) ORPHAN_OWNERS.splice(i, 1);
    };
  }, [play]);

  return (
    <GlassSoundContext.Provider value={value}>
      <div
        ref={wrap}
        data-glass-sound=""
        style={{ display: "contents" }}
        onPointerDownCapture={(e) => {
          pressed.current = null;
          if (e.pointerType === "touch" || e.button !== 0 || !config.current.active || !owns(e)) return;
          controller.ctl.unlock();
          pressed.current = e.target as Element;
          fire(e.target, e.clientX);
        }}
        onClickCapture={(e) => {
          const wasPressed = pressed.current !== null;
          pressed.current = null;
          // Mouse clicks already sounded on press. Touch taps (detail 1, no press) and keyboard (detail 0) sound here.
          if ((wasPressed && e.detail > 0) || !owns(e)) return;
          fire(e.target, e.detail > 0 ? e.clientX : undefined);
        }}
      >
        {children}
      </div>
    </GlassSoundContext.Provider>
  );
}

const NOOP = () => false;
/** Mounted providers in mount order: the first answers cues from portalled components. */
const ORPHAN_OWNERS: HTMLElement[] = [];

/**
 * Play Lumen cues from a component. Without a provider (or with sound off, muted, or
 * `sound={false}`) play() is a silent no-op.
 *
 * @example
 * const sfx = useGlassSound(sound);
 * sfx.play("success");
 * sfx.play("select", { index: 2 });
 */
export function useGlassSound(sound: GlassSoundProp = true, voice?: GlassSoundVoice) {
  const ctx = React.useContext(GlassSoundContext);
  const playCtx = ctx?.play;
  const play = React.useMemo(() => {
    if (!playCtx || sound === false) return NOOP as (cue: GlassSoundCue, opts?: GlassSoundPlayOptions) => boolean;
    const k = sound === "subtle" ? 0.55 : 1;
    return (cue: GlassSoundCue, opts?: GlassSoundPlayOptions) =>
      playCtx(cue, { voice, ...opts, ...(k === 1 ? null : { volume: (opts?.volume ?? 1) * k }) });
  }, [playCtx, sound, voice]);
  return {
    play,
    /** A provider is present. */
    available: ctx !== null,
    /** Sound would play right now (provider enabled, not muted, this instance not silenced). */
    enabled: Boolean(ctx?.enabled && !ctx.muted && sound !== false),
    muted: ctx?.muted ?? true,
    setMuted: ctx?.setMuted ?? (() => {}),
  };
}

/**
 * Plays `cue` when `value` changes to something that matches (not on mount). For outcomes that
 * arrive from props: an error message, a success status.
 */
export function useGlassSoundOnChange<T>(
  value: T,
  cue: GlassSoundCue | ((next: T, prev: T) => GlassSoundCue | null),
  sound: GlassSoundProp = true,
  voice?: GlassSoundVoice,
) {
  const { play } = useGlassSound(sound, voice);
  const prev = React.useRef(value);
  React.useEffect(() => {
    if (Object.is(prev.current, value)) return;
    const before = prev.current;
    prev.current = value;
    const c = typeof cue === "function" ? cue(value, before) : cue;
    if (c) play(c);
  }, [value, cue, play]);
}

// ---------------------------------------------------------------------------------------------
// The toggle
// ---------------------------------------------------------------------------------------------

export interface GlassSoundToggleLabels {
  /** The button's accessible name (it is a toggle: aria-pressed says whether sound is on). */
  sound: string;
  on: string;
  off: string;
}

export const defaultGlassSoundToggleLabels: GlassSoundToggleLabels = { sound: "Sound effects", on: "Sound on", off: "Sound off" };

export interface GlassSoundToggleState {
  muted: boolean;
  toggle: () => void;
  /** A provider is present and enabled. */
  available: boolean;
}

export interface GlassSoundToggleProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
  /** Render your own control. Gets { muted, toggle, available }. */
  render?: (state: GlassSoundToggleState) => React.ReactNode;
  /** Diameter in px. Default 32. */
  size?: number;
  theme?: GlassTheme;
  labels?: GlassLabels<GlassSoundToggleLabels>;
  ref?: React.Ref<HTMLButtonElement>;
}

const TOGGLE_CSS =
  ".gsnd{position:relative;display:inline-grid;place-items:center;flex:none;border:0;padding:0;margin:0;border-radius:999px;cursor:pointer;color:var(--lg-strong);background:var(--lg-cardBg);box-shadow:var(--lg-cardShadow);transition:color .2s,background .2s,transform .35s cubic-bezier(.34,1.5,.64,1);-webkit-tap-highlight-color:transparent}" +
  ".gsnd:hover{color:var(--lg-active)}.gsnd:active{transform:scale(.94)}" +
  ".gsnd .gsnd-w{transition:opacity .3s cubic-bezier(.22,1,.36,1),transform .3s cubic-bezier(.22,1,.36,1);transform-origin:6px 8px}" +
  ".gsnd[aria-pressed=false] .gsnd-w{opacity:0;transform:scale(.6)}.gsnd .gsnd-x{transition:opacity .25s;opacity:0}.gsnd[aria-pressed=false] .gsnd-x{opacity:1}" +
  "@media (prefers-reduced-motion:reduce){.gsnd,.gsnd .gsnd-w,.gsnd .gsnd-x{transition:none}}";

/** The speaker: cell body, two waves when on, a small cross when off. 16px grid, currentColor. */
export function GlassSoundIcon({ muted, size = 16 }: { muted: boolean; size?: number }) {
  const st = { stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", fill: "none" } as const;
  return (
    <svg width={size} height={size} viewBox="0 0 16 16" aria-hidden focusable={false} style={{ display: "block" }} data-muted={muted ? "" : undefined}>
      <path d="M2.5 6.2h2.2L8 3.4v9.2L4.7 9.8H2.5z" {...st} fill="currentColor" fillOpacity={0.18} />
      <g className="gsnd-w">
        <path d="M10.3 6.1a2.7 2.7 0 0 1 0 3.8" {...st} />
        <path d="M12.2 4.3a5.3 5.3 0 0 1 0 7.4" {...st} opacity={0.6} />
      </g>
      <g className="gsnd-x">
        <path d="M10.6 6.3l3.4 3.4M14 6.3l-3.4 3.4" {...st} />
      </g>
    </svg>
  );
}

/**
 * The mute button, in glass. Renders nothing without a provider (there is nothing to toggle).
 * Turning sound on answers with a swoosh; turning it off is silent.
 */
export function GlassSoundToggle({ render, size = 32, theme = "system", labels, className, style, onClick, ref, ...rest }: GlassSoundToggleProps) {
  const ctx = React.useContext(GlassSoundContext);
  const L = React.useMemo(() => mergeLabels(defaultGlassSoundToggleLabels, labels), [labels]);
  const muted = ctx?.muted ?? true;
  const available = Boolean(ctx?.enabled);
  const setMuted = ctx?.setMuted;
  const confirm = ctx?.confirm;
  const toggle = React.useCallback(() => {
    if (!setMuted) return;
    const next = !muted;
    setMuted(next);
    if (!next) confirm?.("swoosh");
  }, [muted, setMuted, confirm]);
  if (!ctx) return null;
  if (render) return <>{render({ muted, toggle, available })}</>;
  return (
    <button
      {...rest}
      ref={ref}
      type="button"
      data-lumen={theme}
      data-sound="none"
      className={className ? `gsnd ${GLASS_FOCUS} ${className}` : `gsnd ${GLASS_FOCUS}`}
      style={{ width: size, height: size, ...style }}
      aria-label={rest["aria-label"] ?? L.sound}
      aria-pressed={!muted}
      title={rest.title ?? (muted ? L.off : L.on)}
      disabled={!available || rest.disabled}
      onClick={(e) => {
        onClick?.(e);
        if (!e.defaultPrevented) toggle();
      }}
    >
      <GlassThemeStyle />
      <style href="beautiful-ui-glass-sound" precedence="medium">
        {TOGGLE_CSS}
      </style>
      <GlassSoundIcon muted={muted} size={Math.round(size / 2)} />
    </button>
  );
}
```
