@zen-ui-capri/system 0.0.0-stage → 1.1.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.
Files changed (105) hide show
  1. package/.agents/skills/capri-zen-ui/SKILL.md +59 -0
  2. package/AGENTS.md +57 -0
  3. package/CLAUDE.md +7 -0
  4. package/LICENSE +21 -0
  5. package/README.md +93 -2
  6. package/bin/capri.mjs +1187 -0
  7. package/dist/lib/audio/ZenAudioController.d.ts +10 -0
  8. package/dist/lib/audio/ZenAudioController.d.ts.map +1 -0
  9. package/dist/lib/audio/useZenAudioSynth.d.ts +26 -0
  10. package/dist/lib/audio/useZenAudioSynth.d.ts.map +1 -0
  11. package/dist/lib/capri/capri-engine.d.ts +127 -0
  12. package/dist/lib/capri/capri-engine.d.ts.map +1 -0
  13. package/dist/lib/components/ZenAIConstruct.d.ts +24 -0
  14. package/dist/lib/components/ZenAIConstruct.d.ts.map +1 -0
  15. package/dist/lib/components/ZenAmbientBackground.d.ts +8 -0
  16. package/dist/lib/components/ZenAmbientBackground.d.ts.map +1 -0
  17. package/dist/lib/components/ZenBadge.d.ts +21 -0
  18. package/dist/lib/components/ZenBadge.d.ts.map +1 -0
  19. package/dist/lib/components/ZenButton.d.ts +24 -0
  20. package/dist/lib/components/ZenButton.d.ts.map +1 -0
  21. package/dist/lib/components/ZenCard.d.ts +36 -0
  22. package/dist/lib/components/ZenCard.d.ts.map +1 -0
  23. package/dist/lib/components/ZenDataDisplay.d.ts +50 -0
  24. package/dist/lib/components/ZenDataDisplay.d.ts.map +1 -0
  25. package/dist/lib/components/ZenDataTable.d.ts +20 -0
  26. package/dist/lib/components/ZenDataTable.d.ts.map +1 -0
  27. package/dist/lib/components/ZenDossierPanel.d.ts +33 -0
  28. package/dist/lib/components/ZenDossierPanel.d.ts.map +1 -0
  29. package/dist/lib/components/ZenDropdown.d.ts +19 -0
  30. package/dist/lib/components/ZenDropdown.d.ts.map +1 -0
  31. package/dist/lib/components/ZenFeedback.d.ts +24 -0
  32. package/dist/lib/components/ZenFeedback.d.ts.map +1 -0
  33. package/dist/lib/components/ZenForms.d.ts +84 -0
  34. package/dist/lib/components/ZenForms.d.ts.map +1 -0
  35. package/dist/lib/components/ZenHeading.d.ts +15 -0
  36. package/dist/lib/components/ZenHeading.d.ts.map +1 -0
  37. package/dist/lib/components/ZenIcon.d.ts +24 -0
  38. package/dist/lib/components/ZenIcon.d.ts.map +1 -0
  39. package/dist/lib/components/ZenInput.d.ts +12 -0
  40. package/dist/lib/components/ZenInput.d.ts.map +1 -0
  41. package/dist/lib/components/ZenModal.d.ts +12 -0
  42. package/dist/lib/components/ZenModal.d.ts.map +1 -0
  43. package/dist/lib/components/ZenNavbar.d.ts +11 -0
  44. package/dist/lib/components/ZenNavbar.d.ts.map +1 -0
  45. package/dist/lib/components/ZenNavigation.d.ts +52 -0
  46. package/dist/lib/components/ZenNavigation.d.ts.map +1 -0
  47. package/dist/lib/components/ZenOverlays.d.ts +48 -0
  48. package/dist/lib/components/ZenOverlays.d.ts.map +1 -0
  49. package/dist/lib/components/ZenSegmentedControl.d.ts +18 -0
  50. package/dist/lib/components/ZenSegmentedControl.d.ts.map +1 -0
  51. package/dist/lib/components/ZenSwitch.d.ts +11 -0
  52. package/dist/lib/components/ZenSwitch.d.ts.map +1 -0
  53. package/dist/lib/components/ZenTabs.d.ts +17 -0
  54. package/dist/lib/components/ZenTabs.d.ts.map +1 -0
  55. package/dist/lib/components/ZenTimelineCard.d.ts +14 -0
  56. package/dist/lib/components/ZenTimelineCard.d.ts.map +1 -0
  57. package/dist/lib/components/ZenToast.d.ts +18 -0
  58. package/dist/lib/components/ZenToast.d.ts.map +1 -0
  59. package/dist/lib/cosmos/ZenCosmosCanvas.d.ts +10 -0
  60. package/dist/lib/cosmos/ZenCosmosCanvas.d.ts.map +1 -0
  61. package/dist/lib/cosmos/ZenTelemetryHud.d.ts +45 -0
  62. package/dist/lib/cosmos/ZenTelemetryHud.d.ts.map +1 -0
  63. package/dist/lib/index.d.ts +33 -0
  64. package/dist/lib/index.d.ts.map +1 -0
  65. package/dist/lib/index.js +3922 -0
  66. package/dist/lib/theme/ZenThemeProvider.d.ts +47 -0
  67. package/dist/lib/theme/ZenThemeProvider.d.ts.map +1 -0
  68. package/dist/lib/tokens.css +888 -0
  69. package/dist/lib/universal/zen-universal.d.ts +77 -0
  70. package/dist/lib/universal/zen-universal.d.ts.map +1 -0
  71. package/dist/lib/universal/zen-universal.js +18 -0
  72. package/dist/lib/zen-universal-C0PD8AI9.js +1352 -0
  73. package/package.json +78 -4
  74. package/src/lib/audio/ZenAudioController.tsx +114 -0
  75. package/src/lib/audio/useZenAudioSynth.ts +289 -0
  76. package/src/lib/capri/capri-engine.ts +939 -0
  77. package/src/lib/components/ZenAIConstruct.tsx +594 -0
  78. package/src/lib/components/ZenAmbientBackground.tsx +210 -0
  79. package/src/lib/components/ZenBadge.tsx +111 -0
  80. package/src/lib/components/ZenButton.tsx +127 -0
  81. package/src/lib/components/ZenCard.tsx +180 -0
  82. package/src/lib/components/ZenDataDisplay.tsx +349 -0
  83. package/src/lib/components/ZenDataTable.tsx +143 -0
  84. package/src/lib/components/ZenDossierPanel.tsx +120 -0
  85. package/src/lib/components/ZenDropdown.tsx +208 -0
  86. package/src/lib/components/ZenFeedback.tsx +137 -0
  87. package/src/lib/components/ZenForms.tsx +664 -0
  88. package/src/lib/components/ZenHeading.tsx +63 -0
  89. package/src/lib/components/ZenIcon.tsx +220 -0
  90. package/src/lib/components/ZenInput.tsx +120 -0
  91. package/src/lib/components/ZenModal.tsx +103 -0
  92. package/src/lib/components/ZenNavbar.tsx +106 -0
  93. package/src/lib/components/ZenNavigation.tsx +300 -0
  94. package/src/lib/components/ZenOverlays.tsx +386 -0
  95. package/src/lib/components/ZenSegmentedControl.tsx +113 -0
  96. package/src/lib/components/ZenSwitch.tsx +63 -0
  97. package/src/lib/components/ZenTabs.tsx +103 -0
  98. package/src/lib/components/ZenTimelineCard.tsx +64 -0
  99. package/src/lib/components/ZenToast.tsx +96 -0
  100. package/src/lib/cosmos/ZenCosmosCanvas.tsx +931 -0
  101. package/src/lib/cosmos/ZenTelemetryHud.tsx +385 -0
  102. package/src/lib/index.ts +293 -0
  103. package/src/lib/styles/zen-tokens.css +888 -0
  104. package/src/lib/theme/ZenThemeProvider.tsx +328 -0
  105. package/src/lib/universal/zen-universal.ts +743 -0
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@zen-ui-capri/system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "zeN-UI // CAPRI — Universal Multi-Framework Design System (51 Primitives, 40 Vector Icons, 8 Dark/Light Themes & Capri 5-Subroutine AI Co-Pilot)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lukaspabst/zeN-UI.git"
10
+ },
11
+ "main": "./dist/lib/index.js",
12
+ "module": "./dist/lib/index.js",
13
+ "types": "./dist/lib/index.d.ts",
14
+ "bin": {
15
+ "capri": "./bin/capri.mjs"
16
+ },
17
+ "files": [
18
+ "dist/lib",
19
+ "src/lib",
20
+ "bin",
21
+ "AGENTS.md",
22
+ "CLAUDE.md",
23
+ ".agents/skills/capri-zen-ui",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/lib/index.d.ts",
29
+ "import": "./dist/lib/index.js",
30
+ "default": "./src/lib/index.ts"
31
+ },
32
+ "./universal": {
33
+ "types": "./dist/lib/universal/zen-universal.d.ts",
34
+ "import": "./dist/lib/universal/zen-universal.js",
35
+ "default": "./src/lib/universal/zen-universal.ts"
36
+ },
37
+ "./tokens.css": "./src/lib/styles/zen-tokens.css"
38
+ },
39
+ "sideEffects": [
40
+ "**/*.css"
41
+ ],
42
+ "publishConfig": {
43
+ "registry": "https://registry.npmjs.org",
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "dev": "vite --port 5180",
48
+ "build": "tsc --noEmit && vite build && npm run build:lib",
49
+ "build:lib": "tsc -p tsconfig.lib.json && vite build --config vite.lib.config.ts",
50
+ "typecheck": "tsc --noEmit",
51
+ "preview": "vite preview --port 5180",
52
+ "test:e2e": "playwright test",
53
+ "capri": "node bin/capri.mjs",
54
+ "prepublishOnly": "npm run capri -- all && npm run build",
55
+ "publish:npm": "node bin/capri.mjs publish-npm"
56
+ },
57
+ "dependencies": {
58
+ "@gsap/react": "^2.1.2",
59
+ "@react-three/fiber": "^9.5.0",
60
+ "@react-three/postprocessing": "^3.2.2",
61
+ "@tailwindcss/vite": "^4.1.0",
62
+ "gsap": "^3.13.0",
63
+ "lucide-react": "^0.562.0",
64
+ "postprocessing": "^6.39.6",
65
+ "react": "^19.2.3",
66
+ "react-dom": "^19.2.3",
67
+ "tailwindcss": "^4.1.0",
68
+ "three": "^0.182.0"
69
+ },
70
+ "devDependencies": {
71
+ "@playwright/test": "^1.51.0",
72
+ "@types/node": "^22.10.0",
73
+ "@types/react": "^19.0.10",
74
+ "@types/react-dom": "^19.0.4",
75
+ "@types/three": "^0.182.0",
76
+ "@vitejs/plugin-react": "^4.3.4",
77
+ "typescript": "~5.8.2",
78
+ "vite": "^6.2.0"
79
+ }
80
+ }
@@ -0,0 +1,114 @@
1
+ import { Volume2, VolumeX, Disc3 } from 'lucide-react';
2
+ import type { ZenAudioSynthController, ZenSynthPresetId } from './useZenAudioSynth';
3
+ import { ZenButton } from '../components/ZenButton';
4
+ import { ZenSegmentedControl } from '../components/ZenSegmentedControl';
5
+
6
+ export interface ZenAudioControllerProps {
7
+ readonly synth: ZenAudioSynthController;
8
+ readonly mode?: 'pill' | 'deck';
9
+ readonly activeLabel?: string;
10
+ readonly idleLabel?: string;
11
+ readonly className?: string;
12
+ }
13
+
14
+ export function ZenAudioController({
15
+ synth,
16
+ mode = 'pill',
17
+ activeLabel = 'Lo-Fi Radio: ON',
18
+ idleLabel = 'Lo-Fi Background',
19
+ className = '',
20
+ }: ZenAudioControllerProps) {
21
+ if (mode === 'pill') {
22
+ return (
23
+ <ZenButton
24
+ variant="secondary"
25
+ size="sm"
26
+ mono
27
+ pressed={synth.active}
28
+ onClick={synth.toggle}
29
+ aria-label="Toggle procedural ambient synthesizer"
30
+ className={className}
31
+ iconLeft={
32
+ synth.active ? (
33
+ <Volume2 className="w-3.5 h-3.5 animate-pulse" />
34
+ ) : (
35
+ <VolumeX className="w-3.5 h-3.5 text-[var(--zen-accent-primary)]" />
36
+ )
37
+ }
38
+ >
39
+ {synth.active ? activeLabel : idleLabel}
40
+ </ZenButton>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div
46
+ data-zen-occlude="true"
47
+ className={`p-5 rounded-3xl zen-surface-static flex flex-col gap-4 ${className}`.trim()}
48
+ >
49
+ <div className="flex flex-wrap items-center justify-between gap-3">
50
+ <div className="flex items-center gap-3">
51
+ <Disc3
52
+ className={`w-6 h-6 text-[var(--zen-accent-primary)] ${
53
+ synth.active ? 'animate-spin' : ''
54
+ }`}
55
+ />
56
+ <div>
57
+ <div className="zen-telemetry-label text-[var(--zen-accent-primary)]">
58
+ WEB AUDIO PROCEDURAL SYNTHESIZER
59
+ </div>
60
+ <div className="zen-font-display font-bold text-base text-[var(--zen-ink-primary)]">
61
+ Zero-Dependency Acoustic Engine
62
+ </div>
63
+ </div>
64
+ </div>
65
+
66
+ <ZenButton
67
+ variant={synth.active ? 'primary' : 'secondary'}
68
+ size="sm"
69
+ mono
70
+ pressed={synth.active}
71
+ onClick={synth.toggle}
72
+ iconLeft={
73
+ synth.active ? (
74
+ <Volume2 className="w-3.5 h-3.5" />
75
+ ) : (
76
+ <VolumeX className="w-3.5 h-3.5" />
77
+ )
78
+ }
79
+ >
80
+ {synth.active ? 'SYNTH ACTIVE' : 'START SYNTH'}
81
+ </ZenButton>
82
+ </div>
83
+
84
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-2 border-t border-[var(--zen-border-subtle)]">
85
+ <ZenSegmentedControl<ZenSynthPresetId>
86
+ ariaLabel="Select synthesizer voicing preset"
87
+ size="sm"
88
+ value={synth.presetId}
89
+ onChange={synth.setPresetId}
90
+ items={synth.presets.map((p) => ({
91
+ value: p.id,
92
+ label: p.label.split(' (')[0],
93
+ }))}
94
+ />
95
+
96
+ <div className="flex items-center gap-1.5" aria-label="8-step bar sequencer">
97
+ {Array.from({ length: 8 }).map((_, idx) => {
98
+ const isBeat = synth.active && synth.currentStep === idx;
99
+ return (
100
+ <span
101
+ key={idx}
102
+ className={`h-2.5 w-2.5 rounded-full transition-all ${
103
+ isBeat
104
+ ? 'bg-[var(--zen-accent-primary)] scale-125'
105
+ : 'bg-[var(--zen-border-default)]'
106
+ }`}
107
+ />
108
+ );
109
+ })}
110
+ </div>
111
+ </div>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,289 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ export type ZenSynthPresetId = 'lofi-atelier' | 'deep-cosmos' | 'minimal-pulse';
4
+
5
+ export interface ZenSynthPreset {
6
+ readonly id: ZenSynthPresetId;
7
+ readonly label: string;
8
+ readonly bpm: number;
9
+ readonly cutoffHz: number;
10
+ readonly chords: readonly (readonly number[])[];
11
+ }
12
+
13
+ export const ZEN_SYNTH_PRESETS: readonly ZenSynthPreset[] = [
14
+ {
15
+ id: 'lofi-atelier',
16
+ label: 'Lo-Fi Atelier (Dm9 -> Cmaj9)',
17
+ bpm: 73,
18
+ cutoffHz: 520,
19
+ chords: [
20
+ [146.83, 220.0, 261.63, 329.63, 392.0],
21
+ [130.81, 196.0, 246.94, 293.66, 349.23],
22
+ [116.54, 174.61, 220.0, 261.63, 329.63],
23
+ [110.0, 164.81, 207.65, 246.94, 311.13],
24
+ ],
25
+ },
26
+ {
27
+ id: 'deep-cosmos',
28
+ label: 'Deep Cosmos (Alpha Cen Drone)',
29
+ bpm: 56,
30
+ cutoffHz: 390,
31
+ chords: [
32
+ [110.0, 164.81, 220.0, 277.18, 329.63],
33
+ [98.0, 146.83, 196.0, 246.94, 293.66],
34
+ [87.31, 130.81, 174.61, 220.0, 261.63],
35
+ [92.5, 138.59, 185.0, 233.08, 277.18],
36
+ ],
37
+ },
38
+ {
39
+ id: 'minimal-pulse',
40
+ label: 'Swiss Precision (Quartal Telemetry)',
41
+ bpm: 84,
42
+ cutoffHz: 680,
43
+ chords: [
44
+ [164.81, 220.0, 293.66, 392.0],
45
+ [146.83, 196.0, 261.63, 349.23],
46
+ [130.81, 174.61, 233.08, 311.13],
47
+ [146.83, 220.0, 293.66, 440.0],
48
+ ],
49
+ },
50
+ ];
51
+
52
+ export interface UseZenAudioSynthOptions {
53
+ readonly defaultPreset?: ZenSynthPresetId;
54
+ readonly defaultVolume?: number;
55
+ }
56
+
57
+ export interface ZenAudioSynthController {
58
+ readonly active: boolean;
59
+ readonly presetId: ZenSynthPresetId;
60
+ readonly volume: number;
61
+ readonly currentStep: number;
62
+ readonly presets: readonly ZenSynthPreset[];
63
+ readonly toggle: () => void;
64
+ readonly setPresetId: (id: ZenSynthPresetId) => void;
65
+ readonly setVolume: (nextVol: number) => void;
66
+ readonly playMicroClick: (pitchHz?: number) => void;
67
+ }
68
+
69
+ export function useZenAudioSynth(
70
+ options?: UseZenAudioSynthOptions
71
+ ): ZenAudioSynthController {
72
+ const [active, setActive] = useState(false);
73
+ const [presetId, setPresetId] = useState<ZenSynthPresetId>(
74
+ options?.defaultPreset ?? 'lofi-atelier'
75
+ );
76
+ const [volume, setVolumeState] = useState<number>(
77
+ options?.defaultVolume ?? 0.8
78
+ );
79
+ const [currentStep, setCurrentStep] = useState<number>(0);
80
+
81
+ const audioCtxRef = useRef<AudioContext | null>(null);
82
+ const masterGainRef = useRef<GainNode | null>(null);
83
+ const snareBufferRef = useRef<AudioBuffer | null>(null);
84
+ const timerRef = useRef<number | null>(null);
85
+ const volumeRef = useRef<number>(volume);
86
+
87
+ const ensureAudioGraph = useCallback((): {
88
+ readonly ctx: AudioContext;
89
+ readonly master: GainNode;
90
+ readonly snareBuffer: AudioBuffer;
91
+ } | null => {
92
+ if (typeof window === 'undefined') return null;
93
+ const AudioContextClass = window.AudioContext;
94
+ if (!AudioContextClass) return null;
95
+
96
+ if (!audioCtxRef.current || audioCtxRef.current.state === 'closed') {
97
+ const ctx = new AudioContextClass();
98
+ const master = ctx.createGain();
99
+ master.gain.setValueAtTime(volumeRef.current, ctx.currentTime);
100
+ master.connect(ctx.destination);
101
+
102
+ const bufferSize = Math.floor(ctx.sampleRate * 0.09);
103
+ const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
104
+ const output = noiseBuffer.getChannelData(0);
105
+ for (let i = 0; i < bufferSize; i++) {
106
+ output[i] =
107
+ (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.35));
108
+ }
109
+
110
+ audioCtxRef.current = ctx;
111
+ masterGainRef.current = master;
112
+ snareBufferRef.current = noiseBuffer;
113
+ }
114
+
115
+ const ctx = audioCtxRef.current;
116
+ const master = masterGainRef.current;
117
+ const snareBuffer = snareBufferRef.current;
118
+ if (!ctx || !master || !snareBuffer) return null;
119
+
120
+ if (ctx.state === 'suspended') {
121
+ void ctx.resume();
122
+ }
123
+
124
+ return { ctx, master, snareBuffer };
125
+ }, []);
126
+
127
+ const setVolume = useCallback((nextVol: number) => {
128
+ const clamped = Math.max(0, Math.min(1, nextVol));
129
+ volumeRef.current = clamped;
130
+ setVolumeState(clamped);
131
+ if (masterGainRef.current && audioCtxRef.current) {
132
+ masterGainRef.current.gain.setTargetAtTime(
133
+ clamped,
134
+ audioCtxRef.current.currentTime,
135
+ 0.04
136
+ );
137
+ }
138
+ }, []);
139
+
140
+ const playMicroClick = useCallback(
141
+ (pitchHz = 880) => {
142
+ const graph = ensureAudioGraph();
143
+ if (!graph) return;
144
+ const { ctx, master } = graph;
145
+ const now = ctx.currentTime;
146
+ const osc = ctx.createOscillator();
147
+ const gain = ctx.createGain();
148
+ osc.type = 'sine';
149
+ osc.frequency.setValueAtTime(pitchHz, now);
150
+ osc.frequency.exponentialRampToValueAtTime(pitchHz * 0.5, now + 0.035);
151
+ gain.gain.setValueAtTime(0.025, now);
152
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.04);
153
+ osc.connect(gain);
154
+ gain.connect(master);
155
+ osc.start(now);
156
+ osc.stop(now + 0.045);
157
+ },
158
+ [ensureAudioGraph]
159
+ );
160
+
161
+ useEffect(() => {
162
+ return () => {
163
+ if (timerRef.current !== null) {
164
+ window.clearInterval(timerRef.current);
165
+ }
166
+ if (audioCtxRef.current && audioCtxRef.current.state !== 'closed') {
167
+ void audioCtxRef.current.close();
168
+ }
169
+ };
170
+ }, []);
171
+
172
+ useEffect(() => {
173
+ if (!active) {
174
+ if (timerRef.current !== null) {
175
+ window.clearInterval(timerRef.current);
176
+ timerRef.current = null;
177
+ }
178
+ return;
179
+ }
180
+
181
+ const graph = ensureAudioGraph();
182
+ if (!graph) return;
183
+ const { ctx, master, snareBuffer } = graph;
184
+
185
+ const preset =
186
+ ZEN_SYNTH_PRESETS.find((p) => p.id === presetId) ?? ZEN_SYNTH_PRESETS[0];
187
+ const stepMs = Math.round((60 / preset.bpm / 2) * 1000);
188
+
189
+ const scheduleStep = (stepIdx: number) => {
190
+ const now = ctx.currentTime;
191
+ const barStep = stepIdx % 8;
192
+ const chordIdx = Math.floor(stepIdx / 8) % preset.chords.length;
193
+ const chord = preset.chords[chordIdx];
194
+
195
+ if (barStep === 0 || barStep === 3) {
196
+ chord.forEach((freq, idx) => {
197
+ const osc = ctx.createOscillator();
198
+ const gain = ctx.createGain();
199
+ const filter = ctx.createBiquadFilter();
200
+ const vibrato = ctx.createOscillator();
201
+ const vibGain = ctx.createGain();
202
+
203
+ osc.type = idx === 0 ? 'sine' : 'triangle';
204
+ osc.frequency.setValueAtTime(freq, now);
205
+
206
+ vibrato.frequency.setValueAtTime(4.2, now);
207
+ vibGain.gain.setValueAtTime(idx === 0 ? 0.2 : 1.1, now);
208
+ vibrato.connect(vibGain);
209
+ vibGain.connect(osc.frequency);
210
+
211
+ filter.type = 'lowpass';
212
+ filter.frequency.setValueAtTime(preset.cutoffHz, now);
213
+
214
+ const peakGain = idx === 0 ? 0.042 : 0.018;
215
+ const dur = barStep === 0 ? 1.45 : 1.95;
216
+ gain.gain.setValueAtTime(0.0001, now);
217
+ gain.gain.linearRampToValueAtTime(peakGain, now + 0.08);
218
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
219
+
220
+ osc.connect(filter);
221
+ filter.connect(gain);
222
+ gain.connect(master);
223
+
224
+ osc.start(now);
225
+ vibrato.start(now);
226
+ osc.stop(now + dur + 0.05);
227
+ vibrato.stop(now + dur + 0.05);
228
+ });
229
+ }
230
+
231
+ if (barStep === 0 || barStep === 5) {
232
+ const kickOsc = ctx.createOscillator();
233
+ const kickGain = ctx.createGain();
234
+ kickOsc.type = 'sine';
235
+ kickOsc.frequency.setValueAtTime(108, now);
236
+ kickOsc.frequency.exponentialRampToValueAtTime(38, now + 0.14);
237
+ kickGain.gain.setValueAtTime(0.065, now);
238
+ kickGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.24);
239
+ kickOsc.connect(kickGain);
240
+ kickGain.connect(master);
241
+ kickOsc.start(now);
242
+ kickOsc.stop(now + 0.26);
243
+ }
244
+
245
+ if (barStep === 2 || barStep === 6) {
246
+ const noise = ctx.createBufferSource();
247
+ noise.buffer = snareBuffer;
248
+ const snareFilter = ctx.createBiquadFilter();
249
+ snareFilter.type = 'bandpass';
250
+ snareFilter.frequency.setValueAtTime(1150, now);
251
+ const snareGain = ctx.createGain();
252
+ snareGain.gain.setValueAtTime(0.022, now);
253
+ snareGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.09);
254
+ noise.connect(snareFilter);
255
+ snareFilter.connect(snareGain);
256
+ snareGain.connect(master);
257
+ noise.start(now);
258
+ }
259
+ };
260
+
261
+ let step = 0;
262
+ setCurrentStep(0);
263
+ scheduleStep(0);
264
+ timerRef.current = window.setInterval(() => {
265
+ step += 1;
266
+ setCurrentStep(step % 8);
267
+ scheduleStep(step);
268
+ }, stepMs);
269
+
270
+ return () => {
271
+ if (timerRef.current !== null) {
272
+ window.clearInterval(timerRef.current);
273
+ timerRef.current = null;
274
+ }
275
+ };
276
+ }, [active, presetId, ensureAudioGraph]);
277
+
278
+ return {
279
+ active,
280
+ presetId,
281
+ volume,
282
+ currentStep,
283
+ presets: ZEN_SYNTH_PRESETS,
284
+ toggle: () => setActive((prev) => !prev),
285
+ setPresetId,
286
+ setVolume,
287
+ playMicroClick,
288
+ };
289
+ }