{"_id":"voice-glow","_rev":"2-b4f755180c81996ed56ca1193cdd5466","name":"voice-glow","dist-tags":{"latest":"0.2.1"},"versions":{"0.2.0":{"name":"voice-glow","version":"0.2.0","keywords":["react","animation","voice","audio","microphone","beam","glow","effect","css"],"author":{"name":"Jakub Antalik"},"license":"MIT","_id":"voice-glow@0.2.0","maintainers":[{"name":"jakubkubo","email":"jakubja@gmail.com"}],"homepage":"https://libraries.dev","bugs":{"url":"https://github.com/Jakubantalik/Libraries.dev/issues"},"dist":{"shasum":"f49c6f1eceadf1b99741012c6c695aadcf808bf2","tarball":"https://registry.npmjs.org/voice-glow/-/voice-glow-0.2.0.tgz","fileCount":6,"integrity":"sha512-AsglDdWohs31Q+PAGIz1jWDqAfXYXnBDUzBP/Dxadkcf8xs4vZ6/qV8TiE0Gz4xGygQfuGZtBQGAz/t6s8qA0w==","signatures":[{"sig":"MEYCIQDpB7zmr5O0Cix3UCLpUlj9XLpewIU0IGDMc1G75dmv4gIhAIdrjBjDdKjLz4rFQ66hTWG7cRZ+1EB7PuKj/DqoJ2++","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":147669},"main":"dist/index.cjs.js","type":"module","types":"dist/index.d.ts","module":"dist/index.es.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.es.js"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs.js"}}},"gitHead":"90a10ad2c0d36a1b6bc38fdf28f473be07a0de22","scripts":{"dev":"vite","build":"vite build","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"_npmUser":{"name":"jakubkubo","email":"jakubja@gmail.com"},"repository":{"url":"git+https://github.com/Jakubantalik/Libraries.dev.git","type":"git","directory":"packages/voice-glow"},"_npmVersion":"10.8.2","description":"Sound-reactive glow for React — a centered, colorful beam along the bottom edge that rises and blooms with voice intensity","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"vite":"^5.0.0","react":"^18.2.0","react-dom":"^18.2.0","typescript":"^5.3.0","@types/react":"^18.2.0","vite-plugin-dts":"^3.7.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0"},"_npmOperationalInternal":{"tmp":"tmp/voice-glow_0.2.0_1789660588290_0.029911411269629262","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"_id":"voice-glow@0.2.1","bugs":{"url":"https://github.com/Jakubantalik/Libraries.dev/issues"},"dist":{"shasum":"6b206e1e9f7a395f85a1d7db7aa9d8ebfcba9cc0","tarball":"https://registry.npmjs.org/voice-glow/-/voice-glow-0.2.1.tgz","fileCount":6,"integrity":"sha512-Lpc+Ppd788kCDC88p9Hr8u3eFQh0X/hdISX76hqx1/x4BZiidrmCuQRLOE+jGOIkqB1BdaFBJ7F2rg1GsST6tw==","signatures":[{"sig":"MEUCIGH0fckTZJnDU0oZ4nbdVff6nvrLnc0Ct6R2g9kF7YCUAiEAuZC8ulrIMIiMcgr+TfJpciCExL+G4Of0KZatYJP6zSU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGfSO2scaFWowd7ZxUA1gjFgUk2FHJXLXSb6DsXVeRkJAiEA3XjUMwo4XTDSsmETmIK/o6LfzxavZ6Cxs/xLx+H9lRw="}],"unpackedSize":147663},"main":"dist/index.cjs","name":"voice-glow","type":"module","types":"dist/index.d.ts","author":{"name":"Jakub Antalik"},"module":"dist/index.es.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.es.js"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs"}}},"gitHead":"740b349cd601710388c9334abd0320718d32b286","license":"MIT","scripts":{"dev":"vite","build":"vite build","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"version":"0.2.1","_npmUser":{"name":"jakubkubo","email":"jakubja@gmail.com"},"homepage":"https://libraries.dev","keywords":["react","animation","voice","audio","microphone","beam","glow","effect","css"],"repository":{"url":"git+https://github.com/Jakubantalik/Libraries.dev.git","type":"git","directory":"packages/voice-glow"},"_npmVersion":"10.8.2","description":"Sound-reactive glow for React — a centered, colorful beam along the bottom edge that rises and blooms with voice intensity","directories":{},"maintainers":[{"name":"jakubkubo","email":"jakubja@gmail.com"}],"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"vite":"^5.0.0","react":"^18.2.0","react-dom":"^18.2.0","typescript":"^5.3.0","@types/react":"^18.2.0","vite-plugin-dts":"^3.7.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0"},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/voice-glow_0.2.1_1790092548700_0.03673334461135247"}}},"time":{"created":"2026-09-17T15:56:28.095Z","modified":"2026-09-22T15:55:48.981Z","0.2.0":"2026-09-17T15:56:28.434Z","0.2.1":"2026-09-22T15:55:48.790Z"},"bugs":{"url":"https://github.com/Jakubantalik/Libraries.dev/issues"},"author":{"name":"Jakub Antalik"},"license":"MIT","homepage":"https://libraries.dev","keywords":["react","animation","voice","audio","microphone","beam","glow","effect","css"],"repository":{"url":"git+https://github.com/Jakubantalik/Libraries.dev.git","type":"git","directory":"packages/voice-glow"},"description":"Sound-reactive glow for React — a centered, colorful beam along the bottom edge that rises and blooms with voice intensity","maintainers":[{"name":"jakubkubo","email":"jakubja@gmail.com"}],"readme":"# Voice\n\n`voice-glow` on npm.\n\nSound-reactive glow for React. A centered, colorful beam along the bottom edge of any element — a chat input, a search bar, a card — that rises and blooms with the intensity of a voice. Feed it a microphone stream, or drive it yourself.\n\nIt is the voice-shaped sibling of [`border-beam`](../border-beam)'s `line` type: the same three-layer construction (edge stroke, inner light, blurred bloom), the same eight palettes, but the glow sits still at the center and its height follows the sound instead of traveling.\n\n## Install\n\n```bash\nnpm install voice-glow\n```\n\n## Quick start\n\n```tsx\nimport { VoiceBeam, useMicrophone } from 'voice-glow';\n\nfunction Composer() {\n  const mic = useMicrophone();\n\n  return (\n    <>\n      <VoiceBeam stream={mic.stream}>\n        <div style={{ padding: 32, borderRadius: 20, background: '#1d1d1d' }}>\n          Listening…\n        </div>\n      </VoiceBeam>\n      <button onClick={mic.state === 'live' ? mic.stop : mic.start}>\n        {mic.state === 'live' ? 'Stop' : 'Listen'}\n      </button>\n    </>\n  );\n}\n```\n\nThe component wraps your content and overlays the glow. It auto-detects the `border-radius` of the first child element. Call `mic.start()` from a click: browsers only grant the microphone — and Safari only starts audio — inside a user gesture.\n\n## Types\n\nThe glow is authored for a ~350px chat input. `type` retunes its geometry for other hosts — every geometry prop below still overrides the preset:\n\n```tsx\n<VoiceBeam type=\"pill\">      {/* a ~150×44 recording pill: glow pulled in, shallow bend, thin band */}\n  <RecordingPill />\n</VoiceBeam>\n\n<VoiceBeam type=\"mobile\">    {/* the bottom of a phone screen: wider range, taller rise, broad band */}\n  <VoiceScreen />\n</VoiceBeam>\n```\n\n`scale` sizes the whole effect as one thing — lobes, spacing, range, bend, band, core, blur and flow all multiply by it — on top of the preset and any individual geometry prop:\n\n```tsx\n<VoiceBeam type=\"pill\" scale={1.3}>\n```\n\nThe wrapper carries `data-voice-type`. `voiceDefaults`, `voiceTypePresets` and `resolveVoiceDefaults(type)` are exported so a host can read the numbers a preset resolves to.\n\n## Driving it yourself\n\nWithout a `stream`, the beam follows `level` (0–1). Pass a number when it changes occasionally, or a getter when it changes every frame — the getter is sampled by the driver without re-rendering your tree:\n\n```tsx\n// From your own analyser, a playback meter, a speech API's volume event…\n<VoiceBeam level={() => meter.current}>\n  <Card />\n</VoiceBeam>\n\n// Or a plain value\n<VoiceBeam level={speaking ? 0.8 : 0}>\n  <Card />\n</VoiceBeam>\n```\n\nSet `idle={0}` if you want nothing at all while silent; by default the beam keeps a soft breathing presence so it never looks dead.\n\n## Response\n\nFour knobs shape how the glow answers the sound:\n\n```tsx\n<VoiceBeam\n  sensitivity={2}     // input gain — raise for quiet sources\n  threshold={0.03}    // noise gate: below this is silence\n  attack={0.1}        // seconds to rise\n  release={0.5}       // seconds to settle\n>\n```\n\nAnd four shape the glow itself:\n\n```tsx\n<VoiceBeam\n  reach={1.4}   // how tall it grows at full level\n  spread={0.6}  // how far it widens; the side lobes drift outward too\n  flow={80}     // px/s the spectrum travels sideways at full level\n  bands         // low / mid / high bands move the lobes independently (default)\n>\n```\n\n`flow` is what makes the colours move: while a voice is heard the lobes slide left to right (right to left for a negative value), wrapping around at the edges, so every colour takes a turn at the centre; as the voice stops the flow settles with it. `flow={0}` holds the spectrum still.\n\nWith `bands` on, the center lobe follows the lows, its neighbours the mids and the outer pair the highs, so a voice makes the colors ripple outward rather than one blob pumping. When driving with `level`, the bands are synthesised from it, so the lobes still dance.\n\n## Processing state\n\nOnce the voice is captured and something is working on it — transcribing, thinking — flip `processing` on. The lobes gather into one compact beam that travels the glow's range left to right and back, eased at each end and looped — `border-beam`'s traveling `line` type, confined to the voice glow, its colours still flowing inside it — and the glow is held lit so the beam has colour. Both blend in and out smoothly, so a listening → processing → idle flow needs no choreography:\n\n```tsx\n<VoiceBeam\n  stream={listening ? mic.stream : null}\n  processing={thinking}\n  processingDuration={1.1}  // seconds per pass\n  processingLevel={0.55}    // how lit the glow is held meanwhile\n  processingEase={0.6}      // seconds the morph in and out takes\n  processingTravel={1.55}   // how far it sweeps to each side\n  processingCurve={2.1}     // how it eases into each turn (1 = constant speed)\n>\n```\n\nThe wrapper carries `data-processing` while it is on. The distortion settles out in about a quarter second and stays off while processing — once it is gone the wrapper also carries `data-voice-warp=\"off\"` and the warp layers leave the paint, so the travelling beam stays smooth where SVG filters are slow (WebKit) — and it eases back over about a second as processing ends.\n\n## Shape\n\n`bend` is the gravity: as the voice rises, the ceiling the glow is masked to gains extra height at the centre — none at the ends — and a faint rim traces that curve, so the glow's top contour humps upward like space bending around the voice. It is in px at full level (default 60; 0 keeps the plain ellipse). `rangeWidth` sets how wide the hump is.\n\nThe **band** is the light along that contour — an organic bell, `exp(-(|x| / spread)^curve)`, drawn on its own canvas layer and flattening onto the edge at both ends. It fades in with the bend and reacts to the voice on its own — its red and blue fringes split further from the core and it thickens as the level rises, and it turns with the glow's hue drift:\n\n```tsx\n<VoiceBeam\n  bandStrength={1.55}   // opacity; 0 hides it\n  bandWidth={2.15}      // thickness\n  bandPosition={0.35}   // peak height as a fraction of the glow's ceiling\n  bandCurve={1.75}      // bell exponent: < 2 exponential / cusp-like, 2 gaussian, > 2 flat-topped\n  bandSpread={0.87}     // bell width vs the half-range: small = spike with long tails, large = dome\n  bandSkew={0.12}       // asymmetry; positive widens the right, steepens the left\n  bandOffset={-27}      // px shift of the whole line; negative sinks it\n  bandTail={0.59}       // the ends rise again in the corners (× the peak)\n  bandTailPosition={0.67} // the rise starts 67% of the way to the edge, full lift at the corner\n  bandTailCurve={2.4}   // higher = a sharper hook\n  bandTailOverflow={15} // runs 15px past each side; the component crops the hook\n  bandAberration={0.89} // how far the red / blue fringes split, 0–1\n>\n```\n\n**Distortion** warps the glow under the band sideways: a slowly drifting noise field displaces the inner light and the bloom horizontally (an SVG `feDisplacementMap`), stronger as the voice rises, so the colours shimmer and stretch like light through bent space. Only the glow *under* the band line warps — the layers are split at the line with a per-frame `clip-path` — and it is independent of `bandStrength`, so the line can be invisible and the warp strong. The edge stroke stays crisp. It is off while `processing` (see above).\n\n```tsx\n<VoiceBeam\n  distortion={0.62}       // 0 turns the filter off\n  distortionDetail={2.3}  // < 1 broad slow waves, > 1 finer ripples\n>\n```\n\nTwelve multipliers (defaults are the tuned geometry) reshape the resting glow; the voice still drives the motion on top:\n\n```tsx\n<VoiceBeam\n  bend={24}          // px the top contour humps up at full level\n  glowWidth={1.2}    // every lobe wider\n  glowHeight={0.8}   // every lobe shorter\n  lobeSpacing={1.3}  // lobes further apart (and a longer flow ring)\n  rangeWidth={1.5}   // the visible ellipse wider\n  rangeHeight={1}    // … and taller\n  softness={1.2}     // softer lobe edges (below 1: crisper)\n  coreSize={1.4}     // the white hot spot at the centre\n  strokeScale={1}    // colours in the 1px edge stroke\n  innerScale={1}     // the soft light inside the element\n  innerHeight={1.5}  // … reaching further in\n  bloomScale={1}     // the blurred halo\n  bloomHeight={1.3}  // … climbing higher\n>\n```\n\n## Color variants\n\n```tsx\n<VoiceBeam colorVariant=\"colorful\" />  {/* Full spectrum (default) */}\n<VoiceBeam colorVariant=\"mono\" />      {/* Grayscale */}\n<VoiceBeam colorVariant=\"ocean\" />     {/* Blue-purple tones */}\n<VoiceBeam colorVariant=\"sunset\" />    {/* Orange-yellow-red tones */}\n<VoiceBeam colorVariant=\"forest\" />    {/* Green-teal tones */}\n<VoiceBeam colorVariant=\"candy\" />     {/* Pink-magenta tones */}\n<VoiceBeam colorVariant=\"ice\" />       {/* Cyan-pale blue tones */}\n<VoiceBeam colorVariant=\"gold\" />      {/* Amber-yellow tones */}\n```\n\nAll variants except `mono` drift slowly through a hue cycle (`hueRange`, `hueDuration`); `staticColors` holds them still.\n\nYour own colours, over any variant:\n\n```tsx\n<VoiceBeam\n  colors={['#ffc915', '#7ec4ff', '#b428e6', '#eb64a0', '#ffb07a', '#9aa0ff', '#7fd9ee']}  // 7 lobes: centre, then pairs outward\n  bandColors={{ core: '#c58bff', above: '#ff7ab6', mid: '#7ec4ff', below: '#2dffab' }}      // the band's ridge and fringes\n>\n```\n\nSlots you leave out keep the variant's colour for the theme. Light mode ships a candy palette (gold, sky, violet, rose, peach) with a violet-cored band; dark keeps the deeper spectrum and a white-cored band.\n\n## Theme\n\n```tsx\n<VoiceBeam theme=\"dark\" />   {/* Dark background (default) */}\n<VoiceBeam theme=\"light\" />  {/* Light background */}\n<VoiceBeam theme=\"auto\" />   {/* Follows prefers-color-scheme */}\n```\n\n## Strength and play / pause\n\n```tsx\n<VoiceBeam strength={0.7} active={listening} onDeactivate={() => console.log('faded out')}>\n  <Card />\n</VoiceBeam>\n```\n\n`strength` scales the beam layers only, never the children. `active={false}` fades the beam out and stops the audio analysis.\n\n## Props\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Content to wrap |\n| `type` | `'default' \\| 'pill' \\| 'mobile'` | `'default'` | Host preset; sets the geometry defaults |\n| `scale` | `number` | `1` | Size of the whole effect — every px dimension at once |\n| `stream` | `MediaStream \\| null` | — | Audio to react to; wins over `level` |\n| `level` | `number \\| () => number` | `0` | Manual drive, 0–1 |\n| `sensitivity` | `number` | `3.1` | Input gain on the analysed audio |\n| `threshold` | `number` | `0.015` | Noise gate, 0–1 |\n| `attack` | `number` | `0.325` | Seconds to rise toward a louder level |\n| `release` | `number` | `0.86` | Seconds to settle after the sound drops |\n| `idle` | `number` | `0.23` | Resting presence while silent, 0–1 |\n| `breatheDuration` | `number` | `5.2` | Period of the idle breathing, seconds |\n| `reach` | `number` | `1.2` | Height gain at full level (1.8 on light; 1.35 for `pill`, 3 for `mobile`) |\n| `spread` | `number` | `1.05` | Width gain at full level (0.8 on light; 1.1 for `pill`, 0.45 for `mobile`) |\n| `bands` | `boolean` | `true` | Let the frequency bands move the lobes independently |\n| `flow` | `number` | `48` | Sideways travel of the spectrum, px/s at full level; negative reverses, 0 holds |\n| `processing` | `boolean` | `false` | Gather the glow into a beam that travels its range, ping-pong, while work is in progress |\n| `processingDuration` | `number` | `1.1` | Seconds per beam pass (1.05 for `mobile`) |\n| `processingLevel` | `number` | `0.55` | Level the glow is held at while processing (0.35 for `mobile`) |\n| `processingEase` | `number` | `0.6` | Seconds the morph into / out of processing takes |\n| `cornerFollow` | `number` | `0.45` | The glow rides the corner arcs while processing; the band line always does (0 for `pill`, 0.4 for `mobile`) |\n| `processingTravel` | `number` | `1.55` | How far the beam travels to each side, × half the lobe ring (2 for `pill`, 1 for `mobile`) |\n| `processingCurve` | `number` | `2.1` | How the sweep eases into each turn: 1 constant speed with sharp turns, 2 smooth, higher dwells at the ends |\n| `colorVariant` | `'colorful' \\| 'mono' \\| 'ocean' \\| 'sunset' \\| 'forest' \\| 'candy' \\| 'ice' \\| 'gold'` | `'colorful'` | Color palette |\n| `colors` | `string[]` | — | Up to 7 lobe colours overriding the palette |\n| `bandColors` | `{ core?, above?, mid?, below? }` | — | The band's ridge and fringe colours |\n| `theme` | `'dark' \\| 'light' \\| 'auto'` | `'dark'` | Background adaptation |\n| `staticColors` | `boolean` | `false` | Disable the hue drift |\n| `hueRange` | `number` | `24` / `40` | Hue drift range in degrees (dark / light) |\n| `hueDuration` | `number` | `12` / `8.5` | Hue drift period in seconds (dark / light) |\n| `active` | `boolean` | `true` | Whether the effect is on |\n| `paused` | `boolean` | `false` | Freezes the effect in place (glow, band and analysis hold their last frame) without fading it out |\n| `borderRadius` | `number` | auto-detected | Custom border radius in px |\n| `brightness` | `number` | `1.15` / `0.95` | Glow brightness multiplier (dark / light; the dark theme itself is 1.1, `pill` runs 1.35, `mobile` 1.2 on dark) |\n| `saturation` | `number` | `1.2` / `1.6` | Glow saturation multiplier (dark / light; `pill` and `mobile` run 1.5 on dark) |\n| `glowSize` | `number` | `1` | Multiplies the bloom blur radius |\n| `strokeOpacity` / `innerOpacity` / `bloomOpacity` | `number` | `1` | Per-layer opacity multipliers on the theme's own (same as the `--voice-*-opacity` CSS hooks) |\n| `bend` | `number` | `60` | Px the glow's top contour humps up at the centre at full level; 0 is the plain ellipse |\n| `bandStrength` | `number` | `1.55` (1.8 for `mobile`; on light 1.7, or 2 for `pill`) | Opacity of the band along the contour; 0 hides it |\n| `bandWidth` | `number` | `2.15` | Thickness of the band |\n| `bandPosition` | `number` | `0.35` | Peak height as a fraction of the glow's ceiling |\n| `bandCurve` | `number` | `1.75` | Bell exponent; < 2 exponential, 2 gaussian, > 2 flat-topped |\n| `bandSpread` | `number` | `0.87` | Bell width vs the half-range |\n| `bandSkew` | `number` | `0.12` | Asymmetry, −0.6–0.6 |\n| `bandOffset` | `number` | `-27` | Px shift of the whole band line |\n| `bandTail` | `number` | `0.59` | Rise of the band's ends in the corners, × the peak; 0 for `pill` |\n| `bandTailPosition` | `number` | `0.67` | Where the rise starts, × the centre-to-edge distance; full lift at the corner |\n| `bandTailCurve` | `number` | `2.4` | Exponent of the rise; higher = a sharper hook at the corner |\n| `bandTailOverflow` | `number` | `15` | Px the band runs past each side, so the hook is cropped by the component |\n| `bandAberration` | `number` | `0.89` | Chromatic split of the band's fringes, 0–1 |\n| `distortion` (off on WebKit / Safari for large hosts, see notes) | `number` | `0.62` | Horizontal warp of the glow under the band, 0–1; 0 removes the filter |\n| `distortionDetail` | `number` | `2.3` | Grain of the distortion noise |\n| `glowWidth` / `glowHeight` | `number` | `0.65` / `1.25` | Width / height of every lobe, all layers |\n| `lobeSpacing` | `number` | `0.85` | Distance between lobes, and the flow ring |\n| `rangeWidth` / `rangeHeight` | `number` | `0.75` / `1` | The visible ellipse the glow is masked to |\n| `softness` | `number` | `1.07` | Lobe edge fade: below 1 crisper, above 1 softer |\n| `coreSize` | `number` | `1` | The white hot spot at the centre |\n| `coreLight` | `number` | `0` | The epicentre: a white wash under the band line so the source reads lighter than the band, 0–3 (1.8 on light; past 1 the solid core widens and the wash grows) |\n| `coreLightWidth` / `coreLightHeight` | `number` | `1` | Size of the epicentre wash |\n| `strokeScale` | `number` | `1` | Size of the colours in the edge stroke |\n| `innerScale` / `innerHeight` | `number` | `1` | Size and reach of the inner light |\n| `bloomScale` / `bloomHeight` | `number` | `1` | Size and climb of the blurred halo |\n| `strength` | `number` | `1` / `0.8` | Effect opacity (0–1), beam layers only (dark / light; `mobile` is 1 on both) |\n| `className` | `string` | — | Additional class on the wrapper |\n| `style` | `CSSProperties` | — | Additional inline styles on the wrapper |\n| `css` | `string` | — | Extra CSS appended after the generated stylesheet; write `{id}` for the instance id |\n| `onLevel` | `(level: number) => void` | — | Called every frame with the smoothed level |\n| `onActivate` | `() => void` | — | Called when the fade-in completes |\n| `onDeactivate` | `() => void` | — | Called when the fade-out completes |\n\nAll standard `HTMLDivElement` attributes are also forwarded to the wrapper. The wrapper carries `data-listening` while a stream is attached and `data-processing` while processing, for styling the host.\n\n### CSS hooks\n\nThe generated stylesheet reads a few custom properties with a fallback of 1, so a host can retune a layer without touching the props:\n\n| Variable | Layer |\n|----------|-------|\n| `--voice-stroke-opacity` | The colored 1px edge stroke |\n| `--voice-inner-opacity` | The soft light inside the element |\n| `--voice-bloom-opacity` | The blurred halo |\n| `--voice-band-opacity` | The band along the bend |\n| `--voice-hue-base` | A fixed hue shift added to the drift (`deg`) |\n\nThe driver also writes `--vb-level-{id}` (the smoothed 0–1 level) on the wrapper each frame, for anything else in the host that wants to follow the voice.\n\n## `useMicrophone`\n\n```tsx\nconst { stream, state, error, supported, start, stop } = useMicrophone({\n  constraints: { deviceId: '…' },  // extra getUserMedia audio constraints\n  autoStart: false,\n});\n```\n\n`state` is one of `idle`, `requesting`, `live`, `denied`, `unsupported`, `error`. By default the hook turns the browser's echo cancellation, noise suppression and auto gain off, so the beam sees the real dynamics of the voice; pass `constraints: {}` to keep the browser defaults. The stream is stopped on unmount.\n\nThe hook is a convenience; any `MediaStream` with an audio track works, including a remote WebRTC peer's — a voice call UI can light up with the other side's voice.\n\n## How it works\n\n`VoiceBeam` renders a wrapper `<div>` with three bottom-centered layers, clipped to the element:\n\n- **`::after`** — the stroke: the palette painted into the 1px edge ring, with a hot white core at the center\n- **`::before`** — the inner glow: the same lobes as soft light inside the element, faded off at the corners\n- **`[data-voice-beam-bloom]`** — the bloom: a blurred halo above the content\n- **`[data-voice-beam-warp=\"inner\"]` / `[data-voice-beam-warp=\"bloom\"]`** — with `distortion` on, mirrors of the two soft layers clipped to below the band line, carrying the displacement filter\n- **`[data-voice-beam-band]`** — a canvas the driver draws the band on, above everything\n\nEvery lobe's size and position multiplies a per-instance custom property. A single shared `requestAnimationFrame` loop (capped at ~60 fps) reads the audio each frame — RMS level plus low / mid / high band energy from one `AnalyserNode` per instance — shapes it (gain, gate, soft saturation), follows it with an attack/release envelope, advances the flow, folds in the idle breathing and the hue drift, and writes the properties. The browser does the painting; the loop is a few arithmetic ops per instance.\n\nThe Web Audio graph is one shared `AudioContext`, one source node per stream (reference-counted, so several beams can share a microphone), and nothing connected to the output — the audio is analysed, never played. The loop pauses while the instance is inactive or scrolled offscreen, and under `prefers-reduced-motion` the breathing, flow and hue drift stop while the reaction to sound stays, since that is a meter rather than decoration.\n\n## Project structure\n\n```\nvoice-glow/\n├── src/\n│   ├── index.ts           # Public exports\n│   ├── VoiceBeam.tsx      # React component\n│   ├── types.ts           # TypeScript type definitions\n│   ├── styles.ts          # CSS generation engine, palettes and lobe geometry\n│   ├── voiceDriver.ts     # Shared rAF loop: analysis, envelope, custom properties\n│   ├── audio.ts           # Shared AudioContext and analyser leases\n│   └── useMicrophone.ts   # getUserMedia hook\n├── dist/                  # Built output (ESM + CJS + types)\n├── package.json\n├── LICENSE\n└── README.md\n```\n\n## Requirements\n\n- React 18+\n- Web Audio API and `getUserMedia` for the microphone path (every modern browser; a secure context — `https` or `localhost` — is required for the microphone)\n- CSS `@property` for the interpolated fade in / out (Chrome 85+, Safari 15.4+, Firefox 128+); without it the fade steps rather than tweens\n- Phones: the soft layers (inner light, bloom, their warp mirrors, the epicentre) are rastered at half resolution and scaled back up by the compositor — the same picture for blurred gradients at a quarter of the raster and filter work — the band canvas caps its backing store at 2×, and when a device still cannot hold 60 the driver updates every other frame (the glow's dynamics are far slower than 30 Hz) and probes full rate again every few seconds. The halving is explicit (a per-layer factor on every length), so it runs on every engine.\n- WebKit / Safari notes: the `distortion` warp is off there on hosts larger than about 60,000 px² (a chat input keeps it, a phone screen or its crop does not) (Safari evaluates SVG filters on HTML content on the CPU every paint, which on a phone-sized host costs an order of magnitude in frame rate; a chat input or pill keeps it), and the band's blur runs in CSS on two canvases instead of the 2D context's `filter`, which Safari lacks. The look otherwise matches Chromium.\n\n## Accessibility\n\nThe effect layers are purely decorative and use `pointer-events: none`. They do not affect keyboard navigation or screen readers. Under `prefers-reduced-motion: reduce` the idle breathing, the flow and the hue drift are disabled. The microphone is only ever requested by your own call to `start()`.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}