{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feedback",
  "title": "Sound and haptics",
  "description": "Opt-in sound and haptics for Bevel controls: <BevelFeedback sound haptics> and useBevelFeedback(). Synthesised with Web Audio; silent outside the provider.",
  "files": [
    {
      "path": "registry/bevel/lib/feedback.ts",
      "content": "\"use client\"\n\n/*\n * Opt-in sound and haptics for Bevel controls.\n *\n *   <BevelFeedback sound haptics>…</BevelFeedback>\n *   const { click, detent, thunk, toggle } = useBevelFeedback()\n *\n * Off by default, and a no-op outside the provider: components call the hook\n * unconditionally and stay silent unless an app turns feedback on. Sounds are\n * synthesised with the Web Audio API (short filtered noise bursts and a low\n * thump), no audio files. The AudioContext is created on the first sound,\n * which always comes from a user gesture, so browsers allow it to start.\n * Haptics use navigator.vibrate where it exists (Android); elsewhere they do\n * nothing.\n */\n\nimport * as React from \"react\"\n\ntype FeedbackSound = \"click\" | \"detent\" | \"thunk\" | \"toggle\"\n\ntype BevelFeedbackValue = {\n  /** A button or key going down. */\n  click: () => void\n  /** A rotary detent or a small step: the lightest tick. */\n  detent: () => void\n  /** Something heavy landing: a breaker, a plug seating, a punch clock. */\n  thunk: () => void\n  /** A switch snapping over. */\n  toggle: () => void\n}\n\ntype BevelFeedbackProps = {\n  /** Play sounds. Default off. */\n  sound?: boolean\n  /** Vibrate on devices that can. Default off. */\n  haptics?: boolean\n  /** 0 to 1. */\n  volume?: number\n  children?: React.ReactNode\n}\n\nconst noop = () => {}\nconst silent: BevelFeedbackValue = { click: noop, detent: noop, thunk: noop, toggle: noop }\n\nconst BevelFeedbackContext = React.createContext<BevelFeedbackValue>(silent)\n\n/* One AudioContext for the page, made on first use. */\nlet audio: AudioContext | null = null\nlet noise: AudioBuffer | null = null\n\nfunction context() {\n  if (typeof window === \"undefined\") return null\n  const Ctor = window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext\n  if (!Ctor) return null\n  if (!audio) {\n    audio = new Ctor()\n    // Half a second of white noise, reused by every burst.\n    noise = audio.createBuffer(1, Math.ceil(audio.sampleRate * 0.5), audio.sampleRate)\n    const data = noise.getChannelData(0)\n    for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1\n  }\n  if (audio.state === \"suspended\") void audio.resume()\n  return audio\n}\n\n/** A burst of band-passed noise that dies away: the body of every click. */\nfunction burst(ctx: AudioContext, at: number, { freq, q, length, gain }: { freq: number; q: number; length: number; gain: number }) {\n  const source = ctx.createBufferSource()\n  source.buffer = noise\n  const filter = ctx.createBiquadFilter()\n  filter.type = \"bandpass\"\n  filter.frequency.value = freq\n  filter.Q.value = q\n  const env = ctx.createGain()\n  env.gain.setValueAtTime(gain, at)\n  env.gain.exponentialRampToValueAtTime(0.0001, at + length)\n  source.connect(filter).connect(env).connect(ctx.destination)\n  source.start(at, Math.random() * 0.4, length + 0.01)\n}\n\n/** A falling sine: weight behind a click. */\nfunction thump(ctx: AudioContext, at: number, { from, to, length, gain }: { from: number; to: number; length: number; gain: number }) {\n  const osc = ctx.createOscillator()\n  osc.frequency.setValueAtTime(from, at)\n  osc.frequency.exponentialRampToValueAtTime(to, at + length)\n  const env = ctx.createGain()\n  env.gain.setValueAtTime(gain, at)\n  env.gain.exponentialRampToValueAtTime(0.0001, at + length)\n  osc.connect(env).connect(ctx.destination)\n  osc.start(at)\n  osc.stop(at + length + 0.01)\n}\n\nconst recipes: Record<FeedbackSound, (ctx: AudioContext, volume: number) => void> = {\n  click: (ctx, v) => {\n    const t = ctx.currentTime\n    burst(ctx, t, { freq: 3200, q: 1.2, length: 0.012, gain: 0.5 * v })\n  },\n  detent: (ctx, v) => {\n    const t = ctx.currentTime\n    burst(ctx, t, { freq: 5200, q: 2, length: 0.006, gain: 0.35 * v })\n  },\n  toggle: (ctx, v) => {\n    // The lever leaves one contact and snaps onto the other.\n    const t = ctx.currentTime\n    burst(ctx, t, { freq: 2600, q: 1.4, length: 0.008, gain: 0.3 * v })\n    burst(ctx, t + 0.022, { freq: 3800, q: 1.4, length: 0.014, gain: 0.55 * v })\n  },\n  thunk: (ctx, v) => {\n    const t = ctx.currentTime\n    thump(ctx, t, { from: 140, to: 50, length: 0.09, gain: 0.7 * v })\n    burst(ctx, t, { freq: 900, q: 0.8, length: 0.03, gain: 0.45 * v })\n  },\n}\n\nconst vibrations: Record<FeedbackSound, number | number[]> = {\n  click: 8,\n  detent: 4,\n  toggle: [6, 24, 8],\n  thunk: 22,\n}\n\n/**\n * Turns on sound and haptics for every Bevel control inside it. Both are off\n * unless you pass them, so a settings toggle can drive this directly.\n */\nfunction BevelFeedback({ sound = false, haptics = false, volume = 0.6, children }: BevelFeedbackProps) {\n  const value = React.useMemo<BevelFeedbackValue>(() => {\n    if (!sound && !haptics) return silent\n    const play = (kind: FeedbackSound) => () => {\n      if (sound) {\n        const ctx = context()\n        if (ctx) recipes[kind](ctx, Math.min(1, Math.max(0, volume)))\n      }\n      if (haptics && typeof navigator !== \"undefined\" && \"vibrate\" in navigator) navigator.vibrate(vibrations[kind])\n    }\n    return { click: play(\"click\"), detent: play(\"detent\"), thunk: play(\"thunk\"), toggle: play(\"toggle\") }\n  }, [sound, haptics, volume])\n  return React.createElement(BevelFeedbackContext.Provider, { value }, children)\n}\n\n/** click, detent, thunk and toggle. Silent outside <BevelFeedback> or when it's off. */\nfunction useBevelFeedback() {\n  return React.useContext(BevelFeedbackContext)\n}\n\nexport { BevelFeedback, useBevelFeedback }\nexport type { BevelFeedbackProps, BevelFeedbackValue, FeedbackSound }\n",
      "type": "registry:lib"
    }
  ],
  "categories": [
    "foundation"
  ],
  "type": "registry:lib"
}