sonics 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Philip Poloczek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # sonics
2
+
3
+ **Tiny, dependency-free UI sounds you can feel.** Synthesised in the browser with the Web Audio API — no audio files, no network payload, ~3 KB.
4
+
5
+ `sonics` is the audio sibling of a haptics library: a small kit for adding _tasteful_ microinteraction sounds (clicks, taps, toggles, confirmations) to a web UI. Every sound is a plain, serialisable object, so you can design one in the [playground](https://github.com/OWNER/sonics), copy the spec, and replay it anywhere.
6
+
7
+ ```js
8
+ import sonics from "sonics";
9
+
10
+ sonics("click"); // that's it
11
+ ```
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install sonics
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```js
22
+ import sonics, { play, armAutoUnlock } from "sonics";
23
+
24
+ // Browsers block audio until the first user gesture. Call once on load:
25
+ armAutoUnlock();
26
+
27
+ play("click"); // a built-in preset
28
+
29
+ const tap = sonics.sound("tap"); // bind a reusable trigger
30
+ button.addEventListener("click", tap);
31
+
32
+ play("click", { volume: 0.6, rate: 1.2, humanize: 0.5 }); // tweak per play
33
+ ```
34
+
35
+ **Presets:** `click` · `tap` · `tick` · `pop` · `toggleOn` · `toggleOff` · `success` · `error`
36
+
37
+ ## Roll your own
38
+
39
+ A sound is a spec — nothing more:
40
+
41
+ ```js
42
+ const doorbell = {
43
+ volume: 0.9,
44
+ ticks: [
45
+ { at: 0, gain: 0.5, freq: 2637, q: 8, decay: 0.006, tail: 0.9 }, // E7
46
+ { at: 0.09, gain: 0.55, freq: 3520, q: 8, decay: 0.008, tail: 0.9 }, // A7
47
+ ],
48
+ };
49
+ play(doorbell);
50
+ ```
51
+
52
+ | Tick field | meaning | default |
53
+ | ---------- | ---------------------------------------- | -------- |
54
+ | `at` | start time (s), relative to sound start | `0` |
55
+ | `gain` | loudness of this tick (0..1) | `0.5` |
56
+ | `freq` | resonant frequency (Hz) | `3120` |
57
+ | `q` | band-pass Q — higher = more tonal/ringy | `7` |
58
+ | `decay` | amplitude decay time constant (s) | `0.0032` |
59
+ | `noise` | noise-burst "breath" mix (0..1) | `1` |
60
+ | `tail` | pure resonant sine tail level (0..1) | `0.6` |
61
+ | `bright` | high attack "tick" transient (0..1) | `0.55` |
62
+ | `partial` | high-partial multiplier (`freq*partial`) | `3.3` |
63
+
64
+ ## Design & export
65
+
66
+ ```js
67
+ import { encode, decode, toWav } from "sonics";
68
+
69
+ const str = encode(mySpec); // URL-safe, shareable string
70
+ play(decode(str)); // replay anywhere
71
+
72
+ const blob = await toWav("click"); // bake to a 16-bit WAV Blob if you prefer a file
73
+ ```
74
+
75
+ ## React
76
+
77
+ Zero extra dependencies — React is a peer.
78
+
79
+ ```jsx
80
+ import { SonicsProvider, useSound, SonicsButton, useSonics } from "sonics/react";
81
+
82
+ function App() {
83
+ return (
84
+ <SonicsProvider>
85
+ <Toolbar />
86
+ </SonicsProvider>
87
+ );
88
+ }
89
+
90
+ function Toolbar() {
91
+ const save = useSound("click");
92
+ const { enabled, toggle } = useSonics();
93
+ return (
94
+ <>
95
+ <button
96
+ onClick={() => {
97
+ doSave();
98
+ save();
99
+ }}
100
+ >
101
+ Save
102
+ </button>
103
+ <SonicsButton sound="toggleOn" onClick={next}>
104
+ Next
105
+ </SonicsButton>
106
+ <label>
107
+ <input type="checkbox" checked={enabled} onChange={toggle} /> Sound
108
+ </label>
109
+ </>
110
+ );
111
+ }
112
+ ```
113
+
114
+ - `<SonicsProvider>` — shared enabled/volume state, persisted, defaults off under `prefers-reduced-motion`, arms auto-unlock.
115
+ - `useSound(sound, opts)` — a stable trigger callback.
116
+ - `useSonics()` — `{ enabled, setEnabled, toggle, volume, setVolume, play }`; safe without a provider.
117
+ - `<SonicsButton>` — a button (or any `as=` element) that plays on `click` / `pointerdown` / `hover`.
118
+
119
+ ## Good-citizen defaults
120
+
121
+ - Won't play in a background tab (`document.hidden`).
122
+ - One shared `AudioContext`, resumed on first gesture via `armAutoUnlock()`.
123
+ - Global `setMuted()` / `setVolume()`; the React provider wires these to a persisted toggle and respects `prefers-reduced-motion`.
124
+
125
+ ## Credit
126
+
127
+ `sonics` occupies a specific empty cell: **synth-based** (not file playback), **tasteful UI** (not retro game SFX), tiny/zero-dep, with a designer. It stands on the shoulders of [ZzFX](https://github.com/KilledByAPixel/ZzFX) (Frank Force), [web-haptics](https://github.com/lochie/web-haptics) (Lochie Axon — the sibling-modality design→export loop this mirrors), [sfxr](https://www.drpetter.se/project_sfxr.html) (DrPetter) and its web ports [jsfxr](https://github.com/chr15m/jsfxr) / [jfxr](https://jfxr.frozenfractal.com/), [snd-lib](https://snd.dev/), and [use-sound](https://github.com/joshwcomeau/use-sound) (Josh W. Comeau). The default `click` preset was reverse-engineered from the ElevenLabs onboarding sound. See the root README for the full story.
128
+
129
+ ## License
130
+
131
+ MIT © Philip Poloczek
@@ -0,0 +1,213 @@
1
+ /*!
2
+ * sonics — tiny, dependency-free UI sounds you can feel.
3
+ *
4
+ * Synthesises pleasant microinteraction sounds at runtime with the Web Audio
5
+ * API. No audio files, no dependencies. Every sound is a plain, serialisable
6
+ * spec object, so you can design one, encode it to a string, share it, and
7
+ * replay it anywhere.
8
+ *
9
+ * The physical model (per "tick"): a short burst of noise strikes a resonant
10
+ * body (a band-pass filter), which rings at `freq` and decays in `decay`
11
+ * seconds. Stack two ticks a few ms apart and you get the "press + release"
12
+ * of a mechanical switch — the thing that makes a click feel tactile.
13
+ *
14
+ * Reverse-engineered from the ElevenLabs onboarding click. See README.
15
+ * License: MIT.
16
+ */
17
+ /** A single impulse. All fields optional; sensible defaults are filled in. */
18
+ interface Tick {
19
+ /** Start time in seconds, relative to the sound's start. Default 0. */
20
+ at?: number;
21
+ /** Loudness of this tick, 0..1. Default 0.5. */
22
+ gain?: number;
23
+ /** Resonant frequency in Hz. Default 3120. */
24
+ freq?: number;
25
+ /** Band-pass Q — higher is more tonal/ringing. Default 7. */
26
+ q?: number;
27
+ /** Amplitude decay time constant in seconds. Default 0.0032. */
28
+ decay?: number;
29
+ /** Amount of noise-burst "breath", 0..1. Default 1. */
30
+ noise?: number;
31
+ /** Level of the pure resonant sine tail, 0..1. Default 0.6. */
32
+ tail?: number;
33
+ /** Level of the high attack transient / "tick", 0..1. Default 0.55. */
34
+ bright?: number;
35
+ /** High-partial frequency multiplier (freq * partial). Default 3.3. */
36
+ partial?: number;
37
+ }
38
+ /** A complete sound: master volume plus one or more ticks. */
39
+ interface Sound {
40
+ /** Master gain for the whole sound. Default 0.9. */
41
+ volume?: number;
42
+ ticks: Tick[];
43
+ /** Optional label, purely for humans. */
44
+ name?: string;
45
+ }
46
+ interface PlayOptions {
47
+ /** Per-play volume multiplier. Default 1. */
48
+ volume?: number;
49
+ /** Pitch/speed: scales every freq and 1/time. Default 1. */
50
+ rate?: number;
51
+ /** 0..1 — random pitch/gain jitter so repeats aren't identical. Default 0. */
52
+ humanize?: number;
53
+ /** Delay before playing, in seconds. Default 0. */
54
+ when?: number;
55
+ }
56
+ interface RenderOptions {
57
+ sampleRate?: number;
58
+ /** Extra silence appended after the sound, in seconds. Default 0.02. */
59
+ tail?: number;
60
+ }
61
+ type SoundInput = Sound | PresetName | (string & {});
62
+ declare const DEFAULT_TICK: Required<Tick>;
63
+ /** Is the Web Audio API available in this environment? */
64
+ declare function isSupported(): boolean;
65
+ /** Get (or lazily create) the shared AudioContext + master gain node. */
66
+ declare function context(): AudioContext;
67
+ /**
68
+ * Browsers suspend audio until a user gesture. Call this from a click/keydown,
69
+ * or call `armAutoUnlock()` once and forget about it. Safe to call often.
70
+ */
71
+ declare function unlock(): Promise<void>;
72
+ /** Resume audio automatically on the first pointer/key/touch gesture. */
73
+ declare function armAutoUnlock(): void;
74
+ declare function setMuted(m: boolean): void;
75
+ declare function isMuted(): boolean;
76
+ /** Master volume, 0..1 (or higher). Applied to every sound. */
77
+ declare function setVolume(v: number): void;
78
+ declare function getVolume(): number;
79
+ /** Play a sound now. `sound` is a spec, a preset name, or an encoded string. */
80
+ declare function play(sound: SoundInput, opts?: PlayOptions): void;
81
+ /** Returns a bound trigger function for a sound. */
82
+ declare function sound(soundOrName: SoundInput, opts?: PlayOptions): () => void;
83
+ /** Render a spec to a mono AudioBuffer without playing it. */
84
+ declare function render(sound: SoundInput, opts?: RenderOptions): Promise<AudioBuffer>;
85
+ /** Render a spec and return a 16-bit PCM WAV Blob you can download. */
86
+ declare function toWav(sound: SoundInput, opts?: RenderOptions): Promise<Blob>;
87
+ /** Encode a spec to a URL-safe string you can save, share, or put in a link. */
88
+ declare function encode(spec: Sound): string;
89
+ /** Decode a string produced by `encode` back into a spec. */
90
+ declare function decode(str: string): Sound;
91
+ declare const presets: {
92
+ /** The ElevenLabs-style double click: two ticks, the release slightly louder. */
93
+ click: {
94
+ volume: number;
95
+ ticks: {
96
+ at: number;
97
+ gain: number;
98
+ freq: number;
99
+ q: number;
100
+ decay: number;
101
+ }[];
102
+ };
103
+ /** A single soft tap — lighter than a full click. */
104
+ tap: {
105
+ volume: number;
106
+ ticks: {
107
+ at: number;
108
+ gain: number;
109
+ freq: number;
110
+ q: number;
111
+ decay: number;
112
+ bright: number;
113
+ }[];
114
+ };
115
+ /** Tiny, dry — good for list/keyboard ticks. */
116
+ tick: {
117
+ volume: number;
118
+ ticks: {
119
+ at: number;
120
+ gain: number;
121
+ freq: number;
122
+ q: number;
123
+ decay: number;
124
+ tail: number;
125
+ bright: number;
126
+ }[];
127
+ };
128
+ /** Rounder, lower — a "pop". */
129
+ pop: {
130
+ volume: number;
131
+ ticks: {
132
+ at: number;
133
+ gain: number;
134
+ freq: number;
135
+ q: number;
136
+ decay: number;
137
+ bright: number;
138
+ tail: number;
139
+ }[];
140
+ };
141
+ /** Two ticks rising in pitch = "on". */
142
+ toggleOn: {
143
+ volume: number;
144
+ ticks: {
145
+ at: number;
146
+ gain: number;
147
+ freq: number;
148
+ q: number;
149
+ decay: number;
150
+ }[];
151
+ };
152
+ /** Two ticks falling in pitch = "off". */
153
+ toggleOff: {
154
+ volume: number;
155
+ ticks: {
156
+ at: number;
157
+ gain: number;
158
+ freq: number;
159
+ q: number;
160
+ decay: number;
161
+ }[];
162
+ };
163
+ /** Gentle two-note rise for confirmations. */
164
+ success: {
165
+ volume: number;
166
+ ticks: {
167
+ at: number;
168
+ gain: number;
169
+ freq: number;
170
+ q: number;
171
+ decay: number;
172
+ tail: number;
173
+ noise: number;
174
+ }[];
175
+ };
176
+ /** Two low, close ticks for errors — dull, not harsh. */
177
+ error: {
178
+ volume: number;
179
+ ticks: {
180
+ at: number;
181
+ gain: number;
182
+ freq: number;
183
+ q: number;
184
+ decay: number;
185
+ tail: number;
186
+ noise: number;
187
+ bright: number;
188
+ }[];
189
+ };
190
+ };
191
+ type PresetName = keyof typeof presets;
192
+ interface Sonics {
193
+ (sound: SoundInput, opts?: PlayOptions): void;
194
+ play: typeof play;
195
+ sound: typeof sound;
196
+ presets: typeof presets;
197
+ render: typeof render;
198
+ toWav: typeof toWav;
199
+ encode: typeof encode;
200
+ decode: typeof decode;
201
+ context: typeof context;
202
+ unlock: typeof unlock;
203
+ armAutoUnlock: typeof armAutoUnlock;
204
+ isSupported: typeof isSupported;
205
+ setMuted: typeof setMuted;
206
+ isMuted: typeof isMuted;
207
+ setVolume: typeof setVolume;
208
+ getVolume: typeof getVolume;
209
+ DEFAULT_TICK: typeof DEFAULT_TICK;
210
+ }
211
+ declare const sonics: Sonics;
212
+
213
+ export { DEFAULT_TICK, type PlayOptions, type PresetName, type RenderOptions, type Sonics, type Sound, type SoundInput, type Tick, armAutoUnlock, context, decode, sonics as default, encode, getVolume, isMuted, isSupported, play, presets, render, setMuted, setVolume, sound, toWav, unlock };
@@ -0,0 +1,213 @@
1
+ /*!
2
+ * sonics — tiny, dependency-free UI sounds you can feel.
3
+ *
4
+ * Synthesises pleasant microinteraction sounds at runtime with the Web Audio
5
+ * API. No audio files, no dependencies. Every sound is a plain, serialisable
6
+ * spec object, so you can design one, encode it to a string, share it, and
7
+ * replay it anywhere.
8
+ *
9
+ * The physical model (per "tick"): a short burst of noise strikes a resonant
10
+ * body (a band-pass filter), which rings at `freq` and decays in `decay`
11
+ * seconds. Stack two ticks a few ms apart and you get the "press + release"
12
+ * of a mechanical switch — the thing that makes a click feel tactile.
13
+ *
14
+ * Reverse-engineered from the ElevenLabs onboarding click. See README.
15
+ * License: MIT.
16
+ */
17
+ /** A single impulse. All fields optional; sensible defaults are filled in. */
18
+ interface Tick {
19
+ /** Start time in seconds, relative to the sound's start. Default 0. */
20
+ at?: number;
21
+ /** Loudness of this tick, 0..1. Default 0.5. */
22
+ gain?: number;
23
+ /** Resonant frequency in Hz. Default 3120. */
24
+ freq?: number;
25
+ /** Band-pass Q — higher is more tonal/ringing. Default 7. */
26
+ q?: number;
27
+ /** Amplitude decay time constant in seconds. Default 0.0032. */
28
+ decay?: number;
29
+ /** Amount of noise-burst "breath", 0..1. Default 1. */
30
+ noise?: number;
31
+ /** Level of the pure resonant sine tail, 0..1. Default 0.6. */
32
+ tail?: number;
33
+ /** Level of the high attack transient / "tick", 0..1. Default 0.55. */
34
+ bright?: number;
35
+ /** High-partial frequency multiplier (freq * partial). Default 3.3. */
36
+ partial?: number;
37
+ }
38
+ /** A complete sound: master volume plus one or more ticks. */
39
+ interface Sound {
40
+ /** Master gain for the whole sound. Default 0.9. */
41
+ volume?: number;
42
+ ticks: Tick[];
43
+ /** Optional label, purely for humans. */
44
+ name?: string;
45
+ }
46
+ interface PlayOptions {
47
+ /** Per-play volume multiplier. Default 1. */
48
+ volume?: number;
49
+ /** Pitch/speed: scales every freq and 1/time. Default 1. */
50
+ rate?: number;
51
+ /** 0..1 — random pitch/gain jitter so repeats aren't identical. Default 0. */
52
+ humanize?: number;
53
+ /** Delay before playing, in seconds. Default 0. */
54
+ when?: number;
55
+ }
56
+ interface RenderOptions {
57
+ sampleRate?: number;
58
+ /** Extra silence appended after the sound, in seconds. Default 0.02. */
59
+ tail?: number;
60
+ }
61
+ type SoundInput = Sound | PresetName | (string & {});
62
+ declare const DEFAULT_TICK: Required<Tick>;
63
+ /** Is the Web Audio API available in this environment? */
64
+ declare function isSupported(): boolean;
65
+ /** Get (or lazily create) the shared AudioContext + master gain node. */
66
+ declare function context(): AudioContext;
67
+ /**
68
+ * Browsers suspend audio until a user gesture. Call this from a click/keydown,
69
+ * or call `armAutoUnlock()` once and forget about it. Safe to call often.
70
+ */
71
+ declare function unlock(): Promise<void>;
72
+ /** Resume audio automatically on the first pointer/key/touch gesture. */
73
+ declare function armAutoUnlock(): void;
74
+ declare function setMuted(m: boolean): void;
75
+ declare function isMuted(): boolean;
76
+ /** Master volume, 0..1 (or higher). Applied to every sound. */
77
+ declare function setVolume(v: number): void;
78
+ declare function getVolume(): number;
79
+ /** Play a sound now. `sound` is a spec, a preset name, or an encoded string. */
80
+ declare function play(sound: SoundInput, opts?: PlayOptions): void;
81
+ /** Returns a bound trigger function for a sound. */
82
+ declare function sound(soundOrName: SoundInput, opts?: PlayOptions): () => void;
83
+ /** Render a spec to a mono AudioBuffer without playing it. */
84
+ declare function render(sound: SoundInput, opts?: RenderOptions): Promise<AudioBuffer>;
85
+ /** Render a spec and return a 16-bit PCM WAV Blob you can download. */
86
+ declare function toWav(sound: SoundInput, opts?: RenderOptions): Promise<Blob>;
87
+ /** Encode a spec to a URL-safe string you can save, share, or put in a link. */
88
+ declare function encode(spec: Sound): string;
89
+ /** Decode a string produced by `encode` back into a spec. */
90
+ declare function decode(str: string): Sound;
91
+ declare const presets: {
92
+ /** The ElevenLabs-style double click: two ticks, the release slightly louder. */
93
+ click: {
94
+ volume: number;
95
+ ticks: {
96
+ at: number;
97
+ gain: number;
98
+ freq: number;
99
+ q: number;
100
+ decay: number;
101
+ }[];
102
+ };
103
+ /** A single soft tap — lighter than a full click. */
104
+ tap: {
105
+ volume: number;
106
+ ticks: {
107
+ at: number;
108
+ gain: number;
109
+ freq: number;
110
+ q: number;
111
+ decay: number;
112
+ bright: number;
113
+ }[];
114
+ };
115
+ /** Tiny, dry — good for list/keyboard ticks. */
116
+ tick: {
117
+ volume: number;
118
+ ticks: {
119
+ at: number;
120
+ gain: number;
121
+ freq: number;
122
+ q: number;
123
+ decay: number;
124
+ tail: number;
125
+ bright: number;
126
+ }[];
127
+ };
128
+ /** Rounder, lower — a "pop". */
129
+ pop: {
130
+ volume: number;
131
+ ticks: {
132
+ at: number;
133
+ gain: number;
134
+ freq: number;
135
+ q: number;
136
+ decay: number;
137
+ bright: number;
138
+ tail: number;
139
+ }[];
140
+ };
141
+ /** Two ticks rising in pitch = "on". */
142
+ toggleOn: {
143
+ volume: number;
144
+ ticks: {
145
+ at: number;
146
+ gain: number;
147
+ freq: number;
148
+ q: number;
149
+ decay: number;
150
+ }[];
151
+ };
152
+ /** Two ticks falling in pitch = "off". */
153
+ toggleOff: {
154
+ volume: number;
155
+ ticks: {
156
+ at: number;
157
+ gain: number;
158
+ freq: number;
159
+ q: number;
160
+ decay: number;
161
+ }[];
162
+ };
163
+ /** Gentle two-note rise for confirmations. */
164
+ success: {
165
+ volume: number;
166
+ ticks: {
167
+ at: number;
168
+ gain: number;
169
+ freq: number;
170
+ q: number;
171
+ decay: number;
172
+ tail: number;
173
+ noise: number;
174
+ }[];
175
+ };
176
+ /** Two low, close ticks for errors — dull, not harsh. */
177
+ error: {
178
+ volume: number;
179
+ ticks: {
180
+ at: number;
181
+ gain: number;
182
+ freq: number;
183
+ q: number;
184
+ decay: number;
185
+ tail: number;
186
+ noise: number;
187
+ bright: number;
188
+ }[];
189
+ };
190
+ };
191
+ type PresetName = keyof typeof presets;
192
+ interface Sonics {
193
+ (sound: SoundInput, opts?: PlayOptions): void;
194
+ play: typeof play;
195
+ sound: typeof sound;
196
+ presets: typeof presets;
197
+ render: typeof render;
198
+ toWav: typeof toWav;
199
+ encode: typeof encode;
200
+ decode: typeof decode;
201
+ context: typeof context;
202
+ unlock: typeof unlock;
203
+ armAutoUnlock: typeof armAutoUnlock;
204
+ isSupported: typeof isSupported;
205
+ setMuted: typeof setMuted;
206
+ isMuted: typeof isMuted;
207
+ setVolume: typeof setVolume;
208
+ getVolume: typeof getVolume;
209
+ DEFAULT_TICK: typeof DEFAULT_TICK;
210
+ }
211
+ declare const sonics: Sonics;
212
+
213
+ export { DEFAULT_TICK, type PlayOptions, type PresetName, type RenderOptions, type Sonics, type Sound, type SoundInput, type Tick, armAutoUnlock, context, decode, sonics as default, encode, getVolume, isMuted, isSupported, play, presets, render, setMuted, setVolume, sound, toWav, unlock };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var l={at:0,gain:.5,freq:3120,q:7,decay:.0032,noise:1,tail:.6,bright:.55,partial:3.3},w=4e-4,v=1e-4,g=typeof window<"u"?(window.AudioContext||window.webkitAudioContext)??null:null,p=null,m=null,T=null,A=false,y=1,k=false;function B(){return !!g}function h(){if(!g)throw new Error("sonics: Web Audio API not available");return p||(p=new g,m=p.createGain(),m.gain.value=y,m.connect(p.destination),T=S(p,.03)),p}function C(){let e=h();return e.state==="suspended"?e.resume():Promise.resolve()}function P(){if(k||typeof window>"u")return;k=true;let e=["pointerdown","keydown","touchstart"],n=()=>{C(),e.forEach(a=>window.removeEventListener(a,n));};e.forEach(a=>window.addEventListener(a,n,{once:true,passive:true}));}function E(e){A=!!e;}function _(){return A}function N(e){y=Math.max(0,e),m&&(m.gain.value=y);}function D(){return y}function S(e,n){let a=e.createBuffer(1,Math.max(1,Math.floor(e.sampleRate*n)),e.sampleRate),u=a.getChannelData(0);for(let c=0;c<u.length;c++)u[c]=Math.random()*2-1;return a}function O(e,n,a,u,c){let t={...l,...u},o=a+t.at;if(t.noise>0){let i=e.createBufferSource();i.buffer=c;let r=e.createBiquadFilter();r.type="bandpass",r.frequency.value=t.freq,r.Q.value=t.q;let s=e.createGain();s.gain.setValueAtTime(0,o),s.gain.linearRampToValueAtTime(t.gain*t.noise,o+w),s.gain.exponentialRampToValueAtTime(v,o+t.decay),i.connect(r).connect(s).connect(n),i.start(o),i.stop(o+t.decay+.02);}if(t.tail>0){let i=e.createOscillator();i.type="sine",i.frequency.value=t.freq;let r=e.createGain();r.gain.setValueAtTime(0,o),r.gain.linearRampToValueAtTime(t.gain*t.tail,o+w),r.gain.exponentialRampToValueAtTime(v,o+t.decay*1.25),i.connect(r).connect(n),i.start(o),i.stop(o+t.decay*1.5+.02);}if(t.bright>0){let i=e.createOscillator();i.type="sine",i.frequency.value=t.freq*t.partial;let r=e.createGain();r.gain.setValueAtTime(0,o),r.gain.linearRampToValueAtTime(t.gain*.3*t.bright,o+w*.75),r.gain.exponentialRampToValueAtTime(v,o+.004),i.connect(r).connect(n),i.start(o),i.stop(o+.02);}}function R(e){let n=0;for(let a of e.ticks??[]){let u={...l,...a};n=Math.max(n,u.at+u.decay*1.5+.03);}return n||.05}function q(e,n={}){if(!g||A||typeof document<"u"&&document.hidden)return;let a=V(e);if(!a.ticks?.length)return;let u=h();u.state==="suspended"&&u.resume();let{volume:c=1,rate:t=1,humanize:o=0,when:i=0}=n,r=u.createGain();r.gain.value=(a.volume??.9)*c,r.connect(m);let s=u.currentTime+Math.max(0,i)+.005,d=b=>1+(Math.random()*2-1)*b;for(let b of a.ticks){let f={...b};t!==1&&(f.freq=(f.freq??l.freq)*t,f.at=(f.at??0)/t,f.decay=(f.decay??l.decay)/t),o>0&&(f.freq=(f.freq??l.freq)*d(.03*o),f.gain=(f.gain??l.gain)*d(.12*o)),O(u,r,s,f,T);}let I=R(a)/t;setTimeout(()=>{try{r.disconnect();}catch{}},(I+.1)*1e3);}function F(e,n){return ()=>q(e,n)}async function U(e,n={}){let{sampleRate:a=44100,tail:u=.02}=n,c=V(e),t=R(c)+u,o=typeof window<"u"?window.OfflineAudioContext||window.webkitOfflineAudioContext:null;if(!o)throw new Error("sonics: OfflineAudioContext not available");let i=new o(1,Math.ceil(a*t),a),r=S(i,.03),s=i.createGain();s.gain.value=c.volume??.9,s.connect(i.destination);for(let d of c.ticks)O(i,s,.001,d,r);return i.startRendering()}async function G(e,n){return W(await U(e,n))}function W(e){let n=e.getChannelData(0),a=e.sampleRate,u=n.length,c=new ArrayBuffer(44+u*2),t=new DataView(c),o=(r,s)=>{for(let d=0;d<s.length;d++)t.setUint8(r+d,s.charCodeAt(d));};o(0,"RIFF"),t.setUint32(4,36+u*2,true),o(8,"WAVE"),o(12,"fmt "),t.setUint32(16,16,true),t.setUint16(20,1,true),t.setUint16(22,1,true),t.setUint32(24,a,true),t.setUint32(28,a*2,true),t.setUint16(32,2,true),t.setUint16(34,16,true),o(36,"data"),t.setUint32(40,u*2,true);let i=44;for(let r=0;r<u;r++){let s=Math.max(-1,Math.min(1,n[r]));t.setInt16(i,s<0?s*32768:s*32767,true),i+=2;}return new Blob([c],{type:"audio/wav"})}function L(e){let n=JSON.stringify(e);return btoa(unescape(encodeURIComponent(n))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function M(e){let n=e.replace(/-/g,"+").replace(/_/g,"/"),a=decodeURIComponent(escape(atob(n)));return JSON.parse(a)}var x={click:{volume:.9,ticks:[{at:0,gain:.5,freq:3120,q:7,decay:.0032},{at:.0276,gain:.66,freq:3120,q:7,decay:.0032}]},tap:{volume:.8,ticks:[{at:0,gain:.55,freq:2600,q:6,decay:.004,bright:.4}]},tick:{volume:.7,ticks:[{at:0,gain:.4,freq:4200,q:9,decay:.0018,tail:.3,bright:.7}]},pop:{volume:.9,ticks:[{at:0,gain:.6,freq:1400,q:5,decay:.006,bright:.25,tail:.8}]},toggleOn:{volume:.85,ticks:[{at:0,gain:.45,freq:2600,q:7,decay:.003},{at:.03,gain:.6,freq:3500,q:7,decay:.0035}]},toggleOff:{volume:.85,ticks:[{at:0,gain:.55,freq:3200,q:7,decay:.003},{at:.03,gain:.45,freq:2300,q:7,decay:.0035}]},success:{volume:.85,ticks:[{at:0,gain:.5,freq:2637,q:8,decay:.006,tail:.9,noise:.3},{at:.09,gain:.55,freq:3520,q:8,decay:.008,tail:.9,noise:.3}]},error:{volume:.85,ticks:[{at:0,gain:.55,freq:320,q:4,decay:.02,tail:.9,noise:.2,bright:.05},{at:.12,gain:.55,freq:300,q:4,decay:.02,tail:.9,noise:.2,bright:.05}]}};function V(e){if(typeof e=="string"){if(e in x)return x[e];try{return M(e)}catch{throw new Error(`sonics: unknown preset or bad encoded string "${e}"`)}}return e}var j=Object.assign((e,n)=>q(e,n),{play:q,sound:F,presets:x,render:U,toWav:G,encode:L,decode:M,context:h,unlock:C,armAutoUnlock:P,isSupported:B,setMuted:E,isMuted:_,setVolume:N,getVolume:D,DEFAULT_TICK:l}),K=j;/*!
2
+ * sonics — tiny, dependency-free UI sounds you can feel.
3
+ *
4
+ * Synthesises pleasant microinteraction sounds at runtime with the Web Audio
5
+ * API. No audio files, no dependencies. Every sound is a plain, serialisable
6
+ * spec object, so you can design one, encode it to a string, share it, and
7
+ * replay it anywhere.
8
+ *
9
+ * The physical model (per "tick"): a short burst of noise strikes a resonant
10
+ * body (a band-pass filter), which rings at `freq` and decays in `decay`
11
+ * seconds. Stack two ticks a few ms apart and you get the "press + release"
12
+ * of a mechanical switch — the thing that makes a click feel tactile.
13
+ *
14
+ * Reverse-engineered from the ElevenLabs onboarding click. See README.
15
+ * License: MIT.
16
+ */exports.DEFAULT_TICK=l;exports.armAutoUnlock=P;exports.context=h;exports.decode=M;exports.default=K;exports.encode=L;exports.getVolume=D;exports.isMuted=_;exports.isSupported=B;exports.play=q;exports.presets=x;exports.render=U;exports.setMuted=E;exports.setVolume=N;exports.sound=F;exports.toWav=G;exports.unlock=C;
package/dist/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ var l={at:0,gain:.5,freq:3120,q:7,decay:.0032,noise:1,tail:.6,bright:.55,partial:3.3},w=4e-4,v=1e-4,g=typeof window<"u"?(window.AudioContext||window.webkitAudioContext)??null:null,p=null,m=null,T=null,A=false,y=1,k=false;function B(){return !!g}function h(){if(!g)throw new Error("sonics: Web Audio API not available");return p||(p=new g,m=p.createGain(),m.gain.value=y,m.connect(p.destination),T=S(p,.03)),p}function C(){let e=h();return e.state==="suspended"?e.resume():Promise.resolve()}function P(){if(k||typeof window>"u")return;k=true;let e=["pointerdown","keydown","touchstart"],n=()=>{C(),e.forEach(a=>window.removeEventListener(a,n));};e.forEach(a=>window.addEventListener(a,n,{once:true,passive:true}));}function E(e){A=!!e;}function _(){return A}function N(e){y=Math.max(0,e),m&&(m.gain.value=y);}function D(){return y}function S(e,n){let a=e.createBuffer(1,Math.max(1,Math.floor(e.sampleRate*n)),e.sampleRate),u=a.getChannelData(0);for(let c=0;c<u.length;c++)u[c]=Math.random()*2-1;return a}function O(e,n,a,u,c){let t={...l,...u},o=a+t.at;if(t.noise>0){let i=e.createBufferSource();i.buffer=c;let r=e.createBiquadFilter();r.type="bandpass",r.frequency.value=t.freq,r.Q.value=t.q;let s=e.createGain();s.gain.setValueAtTime(0,o),s.gain.linearRampToValueAtTime(t.gain*t.noise,o+w),s.gain.exponentialRampToValueAtTime(v,o+t.decay),i.connect(r).connect(s).connect(n),i.start(o),i.stop(o+t.decay+.02);}if(t.tail>0){let i=e.createOscillator();i.type="sine",i.frequency.value=t.freq;let r=e.createGain();r.gain.setValueAtTime(0,o),r.gain.linearRampToValueAtTime(t.gain*t.tail,o+w),r.gain.exponentialRampToValueAtTime(v,o+t.decay*1.25),i.connect(r).connect(n),i.start(o),i.stop(o+t.decay*1.5+.02);}if(t.bright>0){let i=e.createOscillator();i.type="sine",i.frequency.value=t.freq*t.partial;let r=e.createGain();r.gain.setValueAtTime(0,o),r.gain.linearRampToValueAtTime(t.gain*.3*t.bright,o+w*.75),r.gain.exponentialRampToValueAtTime(v,o+.004),i.connect(r).connect(n),i.start(o),i.stop(o+.02);}}function R(e){let n=0;for(let a of e.ticks??[]){let u={...l,...a};n=Math.max(n,u.at+u.decay*1.5+.03);}return n||.05}function q(e,n={}){if(!g||A||typeof document<"u"&&document.hidden)return;let a=V(e);if(!a.ticks?.length)return;let u=h();u.state==="suspended"&&u.resume();let{volume:c=1,rate:t=1,humanize:o=0,when:i=0}=n,r=u.createGain();r.gain.value=(a.volume??.9)*c,r.connect(m);let s=u.currentTime+Math.max(0,i)+.005,d=b=>1+(Math.random()*2-1)*b;for(let b of a.ticks){let f={...b};t!==1&&(f.freq=(f.freq??l.freq)*t,f.at=(f.at??0)/t,f.decay=(f.decay??l.decay)/t),o>0&&(f.freq=(f.freq??l.freq)*d(.03*o),f.gain=(f.gain??l.gain)*d(.12*o)),O(u,r,s,f,T);}let I=R(a)/t;setTimeout(()=>{try{r.disconnect();}catch{}},(I+.1)*1e3);}function F(e,n){return ()=>q(e,n)}async function U(e,n={}){let{sampleRate:a=44100,tail:u=.02}=n,c=V(e),t=R(c)+u,o=typeof window<"u"?window.OfflineAudioContext||window.webkitOfflineAudioContext:null;if(!o)throw new Error("sonics: OfflineAudioContext not available");let i=new o(1,Math.ceil(a*t),a),r=S(i,.03),s=i.createGain();s.gain.value=c.volume??.9,s.connect(i.destination);for(let d of c.ticks)O(i,s,.001,d,r);return i.startRendering()}async function G(e,n){return W(await U(e,n))}function W(e){let n=e.getChannelData(0),a=e.sampleRate,u=n.length,c=new ArrayBuffer(44+u*2),t=new DataView(c),o=(r,s)=>{for(let d=0;d<s.length;d++)t.setUint8(r+d,s.charCodeAt(d));};o(0,"RIFF"),t.setUint32(4,36+u*2,true),o(8,"WAVE"),o(12,"fmt "),t.setUint32(16,16,true),t.setUint16(20,1,true),t.setUint16(22,1,true),t.setUint32(24,a,true),t.setUint32(28,a*2,true),t.setUint16(32,2,true),t.setUint16(34,16,true),o(36,"data"),t.setUint32(40,u*2,true);let i=44;for(let r=0;r<u;r++){let s=Math.max(-1,Math.min(1,n[r]));t.setInt16(i,s<0?s*32768:s*32767,true),i+=2;}return new Blob([c],{type:"audio/wav"})}function L(e){let n=JSON.stringify(e);return btoa(unescape(encodeURIComponent(n))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function M(e){let n=e.replace(/-/g,"+").replace(/_/g,"/"),a=decodeURIComponent(escape(atob(n)));return JSON.parse(a)}var x={click:{volume:.9,ticks:[{at:0,gain:.5,freq:3120,q:7,decay:.0032},{at:.0276,gain:.66,freq:3120,q:7,decay:.0032}]},tap:{volume:.8,ticks:[{at:0,gain:.55,freq:2600,q:6,decay:.004,bright:.4}]},tick:{volume:.7,ticks:[{at:0,gain:.4,freq:4200,q:9,decay:.0018,tail:.3,bright:.7}]},pop:{volume:.9,ticks:[{at:0,gain:.6,freq:1400,q:5,decay:.006,bright:.25,tail:.8}]},toggleOn:{volume:.85,ticks:[{at:0,gain:.45,freq:2600,q:7,decay:.003},{at:.03,gain:.6,freq:3500,q:7,decay:.0035}]},toggleOff:{volume:.85,ticks:[{at:0,gain:.55,freq:3200,q:7,decay:.003},{at:.03,gain:.45,freq:2300,q:7,decay:.0035}]},success:{volume:.85,ticks:[{at:0,gain:.5,freq:2637,q:8,decay:.006,tail:.9,noise:.3},{at:.09,gain:.55,freq:3520,q:8,decay:.008,tail:.9,noise:.3}]},error:{volume:.85,ticks:[{at:0,gain:.55,freq:320,q:4,decay:.02,tail:.9,noise:.2,bright:.05},{at:.12,gain:.55,freq:300,q:4,decay:.02,tail:.9,noise:.2,bright:.05}]}};function V(e){if(typeof e=="string"){if(e in x)return x[e];try{return M(e)}catch{throw new Error(`sonics: unknown preset or bad encoded string "${e}"`)}}return e}var j=Object.assign((e,n)=>q(e,n),{play:q,sound:F,presets:x,render:U,toWav:G,encode:L,decode:M,context:h,unlock:C,armAutoUnlock:P,isSupported:B,setMuted:E,isMuted:_,setVolume:N,getVolume:D,DEFAULT_TICK:l}),K=j;/*!
2
+ * sonics — tiny, dependency-free UI sounds you can feel.
3
+ *
4
+ * Synthesises pleasant microinteraction sounds at runtime with the Web Audio
5
+ * API. No audio files, no dependencies. Every sound is a plain, serialisable
6
+ * spec object, so you can design one, encode it to a string, share it, and
7
+ * replay it anywhere.
8
+ *
9
+ * The physical model (per "tick"): a short burst of noise strikes a resonant
10
+ * body (a band-pass filter), which rings at `freq` and decays in `decay`
11
+ * seconds. Stack two ticks a few ms apart and you get the "press + release"
12
+ * of a mechanical switch — the thing that makes a click feel tactile.
13
+ *
14
+ * Reverse-engineered from the ElevenLabs onboarding click. See README.
15
+ * License: MIT.
16
+ */export{l as DEFAULT_TICK,P as armAutoUnlock,h as context,M as decode,K as default,L as encode,D as getVolume,_ as isMuted,B as isSupported,q as play,x as presets,U as render,E as setMuted,N as setVolume,F as sound,G as toWav,C as unlock};
@@ -0,0 +1,53 @@
1
+ import * as react from 'react';
2
+ import { ElementType, ReactNode } from 'react';
3
+ import { SoundInput, PlayOptions } from '../../index.js';
4
+ export { default as sonics } from '../../index.js';
5
+
6
+ interface SonicsControls {
7
+ enabled: boolean;
8
+ setEnabled: (v: boolean) => void;
9
+ toggle: () => void;
10
+ volume: number;
11
+ setVolume: (v: number) => void;
12
+ play: (sound: SoundInput, opts?: PlayOptions) => void;
13
+ }
14
+ interface SonicsProviderProps {
15
+ children?: ReactNode;
16
+ /** Initial enabled state. Defaults to true unless the OS prefers reduced motion. */
17
+ defaultEnabled?: boolean;
18
+ /** Initial volume, 0..1. Default 1. */
19
+ defaultVolume?: number;
20
+ }
21
+ /**
22
+ * Optional provider. Gives descendants a shared enabled/volume state that
23
+ * persists to localStorage, defaults to off when the OS prefers reduced
24
+ * motion, and arms audio auto-unlock on the first gesture.
25
+ */
26
+ declare function SonicsProvider({ children, defaultEnabled, defaultVolume }: SonicsProviderProps): react.FunctionComponentElement<react.ProviderProps<SonicsControls | null>>;
27
+ /** Access the provider's controls. Falls back to a provider-less shim that
28
+ * always plays, so it's safe to call without a <SonicsProvider>. */
29
+ declare function useSonics(): SonicsControls;
30
+ /**
31
+ * The main hook. Returns a stable trigger you can drop onto any handler.
32
+ *
33
+ * const click = useSound("click");
34
+ * <button onClick={click}>Save</button>
35
+ */
36
+ declare function useSound(sound: SoundInput, opts?: PlayOptions): (overrideOpts?: PlayOptions) => void;
37
+ interface SonicsButtonProps {
38
+ sound?: SoundInput;
39
+ soundOpts?: PlayOptions;
40
+ /** Which event fires the sound. Default "click". */
41
+ on?: "click" | "pointerdown" | "hover";
42
+ /** Element or component to render. Default "button". */
43
+ as?: ElementType;
44
+ onClick?: (e: unknown) => void;
45
+ onPointerDown?: (e: unknown) => void;
46
+ onPointerEnter?: (e: unknown) => void;
47
+ children?: ReactNode;
48
+ [prop: string]: unknown;
49
+ }
50
+ /** Drop-in button that plays a sound on click / pointerdown / hover. */
51
+ declare function SonicsButton({ sound, soundOpts, on, as, onClick, onPointerDown, onPointerEnter, children, ...rest }: SonicsButtonProps): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
52
+
53
+ export { SonicsButton, type SonicsButtonProps, type SonicsControls, SonicsProvider, type SonicsProviderProps, useSonics as default, useSonics, useSound };
@@ -0,0 +1,53 @@
1
+ import * as react from 'react';
2
+ import { ElementType, ReactNode } from 'react';
3
+ import { SoundInput, PlayOptions } from '../../index.js';
4
+ export { default as sonics } from '../../index.js';
5
+
6
+ interface SonicsControls {
7
+ enabled: boolean;
8
+ setEnabled: (v: boolean) => void;
9
+ toggle: () => void;
10
+ volume: number;
11
+ setVolume: (v: number) => void;
12
+ play: (sound: SoundInput, opts?: PlayOptions) => void;
13
+ }
14
+ interface SonicsProviderProps {
15
+ children?: ReactNode;
16
+ /** Initial enabled state. Defaults to true unless the OS prefers reduced motion. */
17
+ defaultEnabled?: boolean;
18
+ /** Initial volume, 0..1. Default 1. */
19
+ defaultVolume?: number;
20
+ }
21
+ /**
22
+ * Optional provider. Gives descendants a shared enabled/volume state that
23
+ * persists to localStorage, defaults to off when the OS prefers reduced
24
+ * motion, and arms audio auto-unlock on the first gesture.
25
+ */
26
+ declare function SonicsProvider({ children, defaultEnabled, defaultVolume }: SonicsProviderProps): react.FunctionComponentElement<react.ProviderProps<SonicsControls | null>>;
27
+ /** Access the provider's controls. Falls back to a provider-less shim that
28
+ * always plays, so it's safe to call without a <SonicsProvider>. */
29
+ declare function useSonics(): SonicsControls;
30
+ /**
31
+ * The main hook. Returns a stable trigger you can drop onto any handler.
32
+ *
33
+ * const click = useSound("click");
34
+ * <button onClick={click}>Save</button>
35
+ */
36
+ declare function useSound(sound: SoundInput, opts?: PlayOptions): (overrideOpts?: PlayOptions) => void;
37
+ interface SonicsButtonProps {
38
+ sound?: SoundInput;
39
+ soundOpts?: PlayOptions;
40
+ /** Which event fires the sound. Default "click". */
41
+ on?: "click" | "pointerdown" | "hover";
42
+ /** Element or component to render. Default "button". */
43
+ as?: ElementType;
44
+ onClick?: (e: unknown) => void;
45
+ onPointerDown?: (e: unknown) => void;
46
+ onPointerEnter?: (e: unknown) => void;
47
+ children?: ReactNode;
48
+ [prop: string]: unknown;
49
+ }
50
+ /** Drop-in button that plays a sound on click / pointerdown / hover. */
51
+ declare function SonicsButton({ sound, soundOpts, on, as, onClick, onPointerDown, onPointerEnter, children, ...rest }: SonicsButtonProps): react.ReactElement<any, string | react.JSXElementConstructor<any>>;
52
+
53
+ export { SonicsButton, type SonicsButtonProps, type SonicsControls, SonicsProvider, type SonicsProviderProps, useSonics as default, useSonics, useSound };
@@ -0,0 +1,4 @@
1
+ 'use strict';Object.defineProperty(exports,'__esModule',{value:true});var react=require('react'),index_js=require('../index.js');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var index_js__default=/*#__PURE__*/_interopDefault(index_js);var y=react.createContext(null),b="sonics:prefs";function h(){try{return JSON.parse(localStorage.getItem(b)||"{}")}catch{return {}}}function M({children:n,defaultEnabled:r,defaultVolume:e=1}){let s=react.useMemo(()=>typeof window<"u"?h():{},[]),l=typeof window<"u"&&window.matchMedia?window.matchMedia("(prefers-reduced-motion: reduce)").matches:false,[o,u]=react.useState(s.enabled??r??!l),[i,a]=react.useState(s.volume??e);react.useEffect(()=>{index_js.armAutoUnlock();},[]),react.useEffect(()=>{index_js.setMuted(!o),index_js.setVolume(i);try{localStorage.setItem(b,JSON.stringify({enabled:o,volume:i}));}catch{}},[o,i]);let c=react.useMemo(()=>({enabled:o,setEnabled:u,toggle:()=>u(t=>!t),volume:i,setVolume:a,play:(t,w)=>{o&&index_js.play(t,w);}}),[o,i]);return react.createElement(y.Provider,{value:c},n)}function v(){let n=react.useContext(y);return n||{enabled:true,setEnabled:()=>{},toggle:()=>{},volume:1,setVolume:()=>{},play:(r,e)=>index_js.play(r,e)}}function I(n,r){let{play:e}=v(),s=react.useRef({sound:n,opts:r});return s.current={sound:n,opts:r},react.useCallback(l=>{let{sound:o,opts:u}=s.current;e(o,l?{...u,...l}:u);},[e])}function B({sound:n="click",soundOpts:r,on:e="click",as:s="button",onClick:l,onPointerDown:o,onPointerEnter:u,children:i,...a}){let c=I(n,r);return react.createElement(s,{...a,onClick:t=>{e==="click"&&c(),l?.(t);},onPointerDown:t=>{e==="pointerdown"&&c(),o?.(t);},onPointerEnter:t=>{e==="hover"&&c(),u?.(t);}},i)}var T=v;/*!
2
+ * sonics/react — tiny React bindings for sonics. React is a peer dependency.
3
+ * License: MIT.
4
+ */Object.defineProperty(exports,"sonics",{enumerable:true,get:function(){return index_js__default.default}});exports.SonicsButton=B;exports.SonicsProvider=M;exports.default=T;exports.useSonics=v;exports.useSound=I;
@@ -0,0 +1,4 @@
1
+ import {createContext,useMemo,useState,useEffect,createElement,useContext,useRef,useCallback}from'react';import {armAutoUnlock,setMuted,setVolume,play}from'../index.js';export{default as sonics}from'../index.js';var y=createContext(null),b="sonics:prefs";function h(){try{return JSON.parse(localStorage.getItem(b)||"{}")}catch{return {}}}function M({children:n,defaultEnabled:r,defaultVolume:e=1}){let s=useMemo(()=>typeof window<"u"?h():{},[]),l=typeof window<"u"&&window.matchMedia?window.matchMedia("(prefers-reduced-motion: reduce)").matches:false,[o,u]=useState(s.enabled??r??!l),[i,a]=useState(s.volume??e);useEffect(()=>{armAutoUnlock();},[]),useEffect(()=>{setMuted(!o),setVolume(i);try{localStorage.setItem(b,JSON.stringify({enabled:o,volume:i}));}catch{}},[o,i]);let c=useMemo(()=>({enabled:o,setEnabled:u,toggle:()=>u(t=>!t),volume:i,setVolume:a,play:(t,w)=>{o&&play(t,w);}}),[o,i]);return createElement(y.Provider,{value:c},n)}function v(){let n=useContext(y);return n||{enabled:true,setEnabled:()=>{},toggle:()=>{},volume:1,setVolume:()=>{},play:(r,e)=>play(r,e)}}function I(n,r){let{play:e}=v(),s=useRef({sound:n,opts:r});return s.current={sound:n,opts:r},useCallback(l=>{let{sound:o,opts:u}=s.current;e(o,l?{...u,...l}:u);},[e])}function B({sound:n="click",soundOpts:r,on:e="click",as:s="button",onClick:l,onPointerDown:o,onPointerEnter:u,children:i,...a}){let c=I(n,r);return createElement(s,{...a,onClick:t=>{e==="click"&&c(),l?.(t);},onPointerDown:t=>{e==="pointerdown"&&c(),o?.(t);},onPointerEnter:t=>{e==="hover"&&c(),u?.(t);}},i)}var T=v;/*!
2
+ * sonics/react — tiny React bindings for sonics. React is a peer dependency.
3
+ * License: MIT.
4
+ */export{B as SonicsButton,M as SonicsProvider,T as default,v as useSonics,I as useSound};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "sonics",
3
+ "version": "0.0.0",
4
+ "description": "Tiny, dependency-free UI sounds you can feel. Synthesised in the browser with the Web Audio API — no audio files.",
5
+ "author": "Philip Poloczek",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "main": "dist/index.js",
9
+ "module": "dist/index.mjs",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.js"
16
+ },
17
+ "./react": {
18
+ "types": "./dist/react/index.d.ts",
19
+ "import": "./dist/react/index.mjs",
20
+ "require": "./dist/react/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": [
30
+ "web-audio",
31
+ "sound",
32
+ "ui",
33
+ "microinteraction",
34
+ "click",
35
+ "sound-effects",
36
+ "synthesis",
37
+ "haptics",
38
+ "react",
39
+ "zero-dependency"
40
+ ],
41
+ "peerDependencies": {
42
+ "react": ">=16.8.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@types/react": "^18.3.0",
51
+ "react": "^18.3.0",
52
+ "tsup": "^8.3.0",
53
+ "typescript": "^5.6.0"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/peetzweg/sonics.git",
58
+ "directory": "packages/sonics"
59
+ },
60
+ "homepage": "https://peetzweg.github.io/sonics/",
61
+ "bugs": "https://github.com/peetzweg/sonics/issues",
62
+ "scripts": {
63
+ "build": "tsup",
64
+ "dev": "tsup --watch",
65
+ "typecheck": "tsc --noEmit"
66
+ }
67
+ }