import * as React from 'react'; /** The source's geometry and timing, now defaults rather than module constants. */ export declare const AUDIO_VISUALIZER_DEFAULT_SIZE = 56; /** * 16, from the Figma atoms `.agent-waiting` and `.user-waiting`, which supersede cr-web-demo where * the two disagree. The source drew 18. Every other geometry constant is derived from `size` and * this, so the change is a default rather than a reshape — and `size` was already 56 and the gap * already 4, which is what Figma specifies. */ export declare const AUDIO_VISUALIZER_DEFAULT_BAR_COUNT = 16; export declare const AUDIO_VISUALIZER_DEFAULT_LINGER_MS = 1500; /** RMS at which the bars reach full height (~loud speech). Ported unchanged. */ export declare const AUDIO_VISUALIZER_LEVEL_SCALE = 0.06; /** * Asymmetric smoothing: fast attack, slow release. * * This is the whole character of the meter and is ported verbatim. A single * symmetric coefficient makes it either laggy on transients or twitchy on decay; * rising four times faster than it falls is what makes it read as a level meter * rather than a random-bar animation. */ export declare const AUDIO_VISUALIZER_ATTACK = 0.28; export declare const AUDIO_VISUALIZER_RELEASE = 0.07; /** How often new random target heights are picked (ms). Ported unchanged. */ export declare const AUDIO_VISUALIZER_TARGET_UPDATE_MS = 180; /** * Below this much distance from rest, in px, a bar counts as settled. * * Exists only because the release lerp is asymptotic: it approaches `minHeight` * without ever arriving, so "all bars are at rest" is not a reachable state and the * idle-stop check would never fire. A twentieth of a pixel is well under one device * pixel at any sane DPR, so snapping there is invisible. */ export declare const AUDIO_VISUALIZER_SETTLE_EPSILON = 0.05; /** * The bar color vocabulary. * * The source picks between `#EF223A` and `white` from an `isAgent` boolean. Both * halves of that are wrong for a design system: the hex cannot follow a theme and * `no-hardcoded-color` would flag it, `white` is invisible on a light page and only * ever worked because cr-web-demo is dark-only, and `isAgent` is the consuming app's * domain vocabulary rather than a visual one. A semantic `tone` mapped to a `fill-*` * token utility replaces it. * * Applied through `className` on each `` — never a `fill` attribute, and in * particular never from inside the animation loop, which is what the source does * (`rect.setAttribute('fill', fill)` on all 18 bars on every frame, ~1000 redundant * attribute writes a second for a value that changes only when a prop does). * * A bar is a non-text graphic, so WCAG 1.4.11 wants 3:1 against the surface. Measured * against `--color-surface-base` in both themes: * * | tone | token | light | dark | * | ----------- | --------------------------- | ------ | ------- | * | accent | `--color-icon-accent` | 5.03:1 | 9.78:1 | * | neutral | `--color-icon-primary` | 19.37:1| 17.85:1 | * | destructive | `--color-icon-destructive` | 7.09:1 | 6.13:1 | * | success | `--color-icon-success` | 6.00:1 | 10.71:1 | * | waveform-agent | `--color-icon-waveform-agent` | — | 4.57:1 | * * `neutral` is the honest translation of the source's `white`: `--color-icon-primary` * resolves to `#ffffff` in dark, so the dark rendering is identical, and to `#000d25` * in light instead of vanishing. `destructive` is the closest semantic home for the * source's brand red; `--color-icon-accent` is Strix's accent and is what a caller * who just wants "the accent one" should reach for. */ export declare const AUDIO_VISUALIZER_TONES: { readonly accent: "fill-icon-accent"; readonly neutral: "fill-icon-primary"; readonly destructive: "fill-icon-destructive"; readonly success: "fill-icon-success"; /** * FIGMA'S OWN AGENT WAVEFORM, added because neither shipped tone was it. * * `.agent-waiting` fills its bars `color/brand/accesible-red-dark-theme` #ef223a — **4.57:1** on the * page. The composed screens drew `accent`, which resolves to `--palette-primary-300` #9ec1fd: a * **blue** at 10.61:1, a better ratio and a different hue. `destructive` is #d97e79, a muted salmon, * so it is not Figma's red either — which is why this is a fifth tone rather than a re-mapping. * * `--color-icon-accent` is deliberately NOT re-pointed: `Spinner`, `Toast` and six other components * share it, and a speaking-agent waveform is not what "the accent icon" means. `neutral` needs * nothing — `--color-icon-primary` is already #ffffff in dark, which is Figma's `brand/white` exactly. */ readonly 'waveform-agent': "fill-icon-waveform-agent"; }; export type AudioVisualizerTone = keyof typeof AUDIO_VISUALIZER_TONES; declare const audioVisualizerStyles: (props?: import("class-variance-authority/types").ClassProp | undefined) => string; declare const audioVisualizerBarStyles: (props?: ({ tone?: "accent" | "neutral" | "destructive" | "success" | "waveform-agent" | null | undefined; } & import("class-variance-authority/types").ClassProp) | undefined) => string; export interface AudioVisualizerGeometry { barCount: number; /** SVG width, `barCount * barStep`. */ width: number; /** SVG height, i.e. `size`. */ height: number; barWidth: number; /** Bar pitch: width plus the gap to the next bar. */ barStep: number; /** Resting height, and the floor every animated height is clamped to. */ minHeight: number; /** Height at `normalizedLevel === 1` for the centre bar with no jitter. */ maxHeight: number; /** Index of the bell's peak. */ center: number; /** Bell width, in bar indices. */ sigma: number; } /** * Derive every geometry constant from `size` and `barCount`. * * The source hardcodes eight numbers at a single size. They are all proportional to * its 56px height, so one scale factor reproduces them exactly at the default and * gives a coherent meter at any other size: at `size = 56, barCount = 18` this * returns the source's 216x56 box, 8px bars on a 12px pitch, and a 4..54 height range. */ export declare function audioVisualizerGeometry(size?: number, barCount?: number): AudioVisualizerGeometry; /** * The target height for one bar: a jittered Gaussian across the bar array. * * Ported verbatim, including the `0.5 + Math.random() * 0.5` jitter, which is what * stops the bell from looking like a static arch. Exported as a pure function so the * shape can be asserted directly instead of inferred from painted attributes. * * `minHeight` is the floor: at `normalizedLevel === 0` every bar returns exactly it. */ export declare function bellTarget(normalizedLevel: number, index: number, geometry: AudioVisualizerGeometry): number; export interface AudioVisualizerProps extends Omit, 'children'> { /** * Whether audio is currently flowing. Falling edges are followed by * `lingerMs` of continued animation. * @default false */ isSpeaking?: boolean; /** * Live RMS level, read from `.current` on every frame and never through state. * Anything in the region of `0.06` is loud speech; see * {@link AUDIO_VISUALIZER_LEVEL_SCALE}. * * **Omitting it means no motion, `isSpeaking` or not.** There is nothing to read, so every * bar target is exactly `minHeight` and the meter renders identically to its resting state. * That is deliberate — a level meter that invents levels is lying about audio — but it is * easy to trip over, and `showcase/figma-animations`' `voice` shipped it that way. Stories * and demos want a synthetic ref: see `audio-visualizer.story-utils`. */ audioLevelRef?: React.RefObject | React.MutableRefObject; /** * Semantic bar color. * @default 'accent' */ tone?: AudioVisualizerTone; /** * Overall height in px. Every other geometry constant scales from it. * @default 56 */ size?: number; /** * Number of bars. Also widens the bell, which is defined as `barCount / 4.5`. * @default 18 */ barCount?: number; /** * How long the meter keeps animating after `isSpeaking` goes false. * @default 1500 */ lingerMs?: number; /** * Honour `prefers-reduced-motion: reduce` by parking the bars and never starting * the frame loop. Turn it off only for a meter whose motion is the content itself * — e.g. a level calibration tool where a static bar conveys nothing. * @default true */ respectReducedMotion?: boolean; /** * Hide the meter from assistive technology. * * True by default, deliberately. A level meter conveys no information that is not * already carried by the thing it decorates: whether an agent is speaking is * application state, and a screen-reader user needs *that* announced, from wherever * it lives, not a graphic whose only content is a number changing sixty times a * second. Pass `decorative={false}` for the rare case where the meter is the only * indication that audio is live; it then renders `role="img"` with `label` as its * accessible name. * * Either way there is no live region here and there must not be one — an * `aria-live` audio level is a screen reader talking over the audio it is measuring. * @default true */ decorative?: boolean; /** * Accessible name when `decorative` is false. Ignored otherwise. * @default 'Audio level' */ label?: string; } /** * A bar meter that follows a live audio level. * * The algorithm is ported faithfully from cr-web-demo's `AudioVisualizer`, because it * is deliberate and correct: one `requestAnimationFrame` loop mutating `height` and * `y` on the `` elements directly, with **zero React re-renders per frame**; * asymmetric attack/release smoothing on both the global level and each bar; random * target heights regenerated on a 180ms interval under a Gaussian envelope; and a * 1500ms post-speech linger so the meter decays instead of snapping to rest. * * Five things changed, all of them things the source gets wrong rather than merely * differently: * * 1. **`prefers-reduced-motion`.** The source runs its loop unconditionally, forever, * for every user. An 18-bar meter animating continuously is the case WCAG 2.3.3 * exists for. Under a reduce preference the bars park at `minHeight` and no frame * is ever requested. The preference is tracked with a `matchMedia` change listener, * not sampled at mount, so toggling it at the OS level takes effect immediately. * `respectReducedMotion={false}` is the escape hatch. * 2. **The loop stops when idle.** The source's loop runs for the lifetime of the * component, burning a frame callback and 18 attribute writes forever on a silent * page. This cancels the frame once the level has decayed, the linger has expired, * and every bar has actually reached rest — checked, so it cannot freeze mid-decay * — and starts again on the next activity. **Including when there is no * `audioLevelRef` at all**: this claim used to be false in exactly that case, because * `isSpeaking` kept the meter "active" and active vetoed stopping, so a meter with no * possible input spun forever doing the thing this item criticises. * 3. **Token colors.** See {@link AUDIO_VISUALIZER_TONES}. The loop never touches * `fill`. * 4. **A decision about the accessibility tree.** The source renders a nameless, * roleless ``, which is an accident rather than a choice. See `decorative`. * 5. **`size` and `barCount` are props**, with the source's values as defaults and * every derived constant computed by {@link audioVisualizerGeometry}. * * @example * @example */ export declare const AudioVisualizer: React.ForwardRefExoticComponent>; export { audioVisualizerStyles, audioVisualizerBarStyles }; //# sourceMappingURL=audio-visualizer.d.ts.map