'use client'; /** * Persistent audio preferences (volume / mute / per-event toggles). * * One zustand store per `storageKey` — instances with different keys * persist independently (chat / alerts / per-product). Cross-tab sync * via the `storage` event ships with zustand's persist middleware. */ import { create, type StoreApi, type UseBoundStore } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; export interface AudioPrefsState { /** 0..1 master volume. */ volume: number; /** Master mute (overrides per-event toggles). */ muted: boolean; /** Per-event opt-out — `false` silences a single trigger. */ enabled: Partial>; setVolume: (v: number) => void; setMuted: (m: boolean) => void; setEventEnabled: (event: E, enabled: boolean) => void; } const clamp01 = (v: number): number => { if (!Number.isFinite(v)) return 1; return v < 0 ? 0 : v > 1 ? 1 : v; }; /** * Per-key registry so repeated calls with the same `storageKey` return * the same store instance — necessary for cross-component state sharing * (e.g. ChatHeader audio toggle + ChatLauncher bus reading the same key). */ const registry = new Map>>>(); export function createAudioPrefsStore( storageKey: string, ): UseBoundStore>> { const cached = registry.get(storageKey); if (cached) return cached as unknown as UseBoundStore>>; const store = create>()( persist( (set) => ({ volume: 1, muted: false, enabled: {}, setVolume: (v) => set({ volume: clamp01(v) }), setMuted: (m) => set({ muted: !!m }), setEventEnabled: (event, enabled) => set((s) => ({ enabled: { ...s.enabled, [event]: enabled } })), }), { name: storageKey, storage: createJSONStorage(() => { if (typeof window === 'undefined') { return { getItem: () => null, setItem: () => undefined, removeItem: () => undefined, }; } return window.localStorage; }), partialize: (s) => ({ volume: s.volume, muted: s.muted, enabled: s.enabled }), version: 1, }, ), ); registry.set(storageKey, store as unknown as UseBoundStore>>); return store; } /** * React hook helper — returns the persisted prefs store hook for a given * `storageKey`. Callers can use it as a normal zustand hook with selectors. * * @example * ```ts * const usePrefs = useAudioPrefs<'sent' | 'received'>('myapp.audio'); * const muted = usePrefs((s) => s.muted); * ``` */ export function useAudioPrefs( storageKey: string, ): UseBoundStore>> { return createAudioPrefsStore(storageKey); }