{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sfx-bus",
  "title": "SFX Bus",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Cliques de objeto analógico em WebAudio puro, sem arquivo: botão de câmera, dente de knob, alavanca e agulha pousando, com throttle, contexto armado no primeiro gesto e mudo sob prefers-reduced-motion.",
  "dependencies": [
    "react"
  ],
  "files": [
    {
      "path": "registry/visant/lib/sfx-bus.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * SFX Bus: poucos sons, cada um copiado de um objeto analógico, e só onde a\n * pessoa age.\n *\n * Colhido do `visantismo`, onde o arquivo já rodava em duas casas (a landing e\n * o `visant-club`, que carrega uma cópia em `public/sfx.js`). Duas casas com o\n * mesmo arquivo é a definição de item de registry.\n *\n * ── O escopo é deliberadamente mínimo ──────────────────────────────────────\n *\n * Nada de som de seção entrando, de acordeão abrindo, de sucesso ou de erro. Um\n * plano anterior pedia oito eventos; virou poucos, e a razão vale mais que a\n * economia: som que acontece em todo lugar vira ruído de fundo e a pessoa\n * desliga. Som que só acontece onde ela age vira tato.\n *\n * ── A paleta analógica ─────────────────────────────────────────────────────\n *\n * A primeira versão tinha um timbre só, chimbau fechado (ruído em passa-alta).\n * Funciona, mas soa eletrônico. Os cliques gostosos de objeto antigo (tecla de\n * câmera, seletor de toca-discos, alavanca de painel) têm outra receita, e é a\n * mesma pros quatro: **síntese modal**. Uma peça de metal pequena, quando leva\n * uma batida, não faz \"tss\"; ela soa por um instante nas frequências em que\n * ressoa. Então: um sopro de ruído de 1 ms (a batida) passa por um passa-banda\n * estreito (a peça ressoando) e morre em milésimos. Q alto dá o brilho de metal\n * sem virar nota, porque a ressonância dura menos de 15 ms e o ouvido não tem\n * tempo de achar altura. Peça grande ressoa grave, peça pequena ressoa agudo.\n *\n * Cada timbre é uma lista de **camadas** (uma batida cada) em instantes\n * diferentes. O que separa um objeto do outro é quantas batidas, onde cada uma\n * ressoa e o intervalo entre elas:\n *\n * | timbre   | objeto                         | camadas (instante · filtro · Hz · Q · pico · duração)   |\n * |----------|--------------------------------|----------------------------------------------------------|\n * | `click`  | botão de disparo de câmera     | 0 ms · banda · 3800 · 4 · 0.11 · 9 ms  (trava de metal)  |\n * |          |                                | 16 ms · banda · 2400 · 7 · 0.06 · 14 ms (mola volta)     |\n * | `detent` | detente de knob, seletor de    | 0 ms · banda · 2900 · 10 · 0.07 · 6 ms (dente da mola)   |\n * |          | velocidade do toca-discos      |                                                          |\n * | `toggle` | interruptor de alavanca        | 0 ms · banda · 2100 · 5 · 0.035 · 6 ms (sai do dente)    |\n * |          |                                | 22 ms · banda · 1300 · 3 · 0.07 · 18 ms (bate, corpo)    |\n * |          |                                | 22 ms · alta · 4500 · 0.7 · 0.012 · 5 ms (brilho)        |\n * | `drop`   | fole de câmera fechando        | 0 ms · baixa · 240 · 0.7 · 0.07 · 45 ms (toque de couro) |\n * |          | (o ar sai num \"pff\" curto)     | 4 ms · banda · 2400→700 · 0.9 · 0.04 · 140 ms (sopro)    |\n * |          |                                | 30 a 80 ms · alta · 4200 · 0.7 · 0.012 · 4 ms (poeira)   |\n * | `focus`  | anel de foco de lente girando  | 0 ms · banda · 3400 · 8 · 0.03 · 4 ms (dente da catraca) |\n * |          | (slider arrastando, por passo) | 0 ms · banda · 900 · 1.5 · 0.012 · 18 ms (atrito, graxa) |\n * |`complete`| obturador armando e soltando   | 0 ms · banda · 1600 · 4 · 0.04 · 12 ms (alavanca engata) |\n * |          | (segurar ou deslizar até o fim)| 45 ms · banda · 4200 · 5 · 0.09 · 8 ms (lâminas caem)    |\n * |          |                                | 50 ms · banda · 1100 · 2 · 0.05 · 30 ms (corpo responde) |\n *\n * - **`focus` é o mais baixo da paleta** porque vem em rajada mais longa que o\n *   knob (um arraste inteiro). Portão 45 ms: o dente aparece, a rajada não.\n * - **`complete` é o fim de um gesto longo**, então tem três tempos e portão\n *   de 300 ms; a pausa de 45 ms entre armar e soltar é o que lê como câmera.\n *\n * Por que cada escolha:\n *\n * - **`click` tem duas batidas a 16 ms.** Câmera de filme faz \"tic-tac\": a trava\n *   solta e a mola devolve. Uma batida só lê como tique de plástico; duas, como\n *   mecanismo. Abaixo de 10 ms as duas fundem; acima de 30 ms viram dois cliques.\n * - **`detent` é o mais baixo e o mais curto** porque vem em rajada: girar um\n *   knob dispara dez seguidos. Q 10 dá o \"tic\" de metal fino sem corpo nenhum.\n * - **`toggle` tem corpo** (passa-banda largo em 1300 Hz): alavanca é peça\n *   maior que dente de knob e bate num batente. A primeira batida, fraca, é a\n *   alavanca saindo do lugar; é ela que diz \"alavanca\" e não \"botão\".\n * - **`drop` é o único com grave e o único que sopra**, e por isso é o único\n *   reservado pra ação forte (confirmar, publicar, soltar). O toque embaixo é\n *   ruído em passa-baixa (peso sem nota); o sopro é ruído num passa-banda\n *   largo que escorrega de 2400 pra 700 Hz (`ate`), que é o que o ouvido lê\n *   como ar saindo e não como chiado parado. Ataque de 14 ms no sopro: mais\n *   curto vira estalo, mais longo vira vento. Um grão de poeira sorteado no\n *   meio tira a cara de efeito pronto.\n * - **Ataque entre 0,4 e 3 ms.** Abaixo disso o primeiro quadro é um degrau, e\n *   degrau soa como estalo digital. Acima, a batida amolece e vira sopro.\n *\n * Os picos parecem altos perto do chimbau antigo (0.034), mas passa-banda\n * estreito joga fora quase toda a energia do ruído. Medido com `renderSfx`\n * (média de 20 toques, RMS do trecho que soa):\n *\n * | timbre    | RMS       | soa por |\n * |-----------|-----------|---------|\n * | `chimbau` | -44 dBFS  | 82 ms   |\n * | `click`   | -48 dBFS  | 28 ms   |\n * | `drop`    | -49 dBFS  | 74 ms   |\n * | `toggle`  | -52 dBFS  | 50 ms   |\n * | `detent`  | -54 dBFS  | 5 ms    |\n * | `complete`| -53 dBFS  | 60 ms   |\n * | `focus`   | -56 dBFS  | 5 ms    |\n *\n * Tudo abaixo do clique antigo, e mais curto. É o nível que se ouve com fone\n * sem notar o volume.\n *\n * **Variação leve a cada toque**: frequência ±6%, pico ±12%, intervalo entre\n * camadas ±15%, e trecho sorteado do buffer de ruído. Objeto de verdade nunca\n * soa duas vezes igual; dez estalos idênticos lêem como loop travado.\n *\n * Os dois sons antigos continuam: `hover` (chimbau quase inaudível, 8200 Hz,\n * 0.016, 28 ms) e `chimbau` (o clique da primeira versão, 6400 Hz, 0.034,\n * 62 ms), pra quem já tinha se acostumado com ele.\n *\n * ── Como pedir o timbre ────────────────────────────────────────────────────\n *\n * 1. **Por atributo**, com o `SfxProvider` na raiz: `data-sfx=\"detent\"` no\n *    elemento (ou num ancestral) escolhe o timbre; `data-sfx=\"off\"` cala. Sem\n *    atributo, o bus deduz pelo papel: `role=\"switch\"` e checkbox são\n *    `toggle`; aba, rádio, opção e botão com `aria-pressed` são `detent`\n *    (seletor girando de uma posição pra outra); o resto é `click`.\n * 2. **Por código**, em gesto contínuo: `playSfx(\"detent\")` no handler que\n *    muda o passo do knob. O portão por timbre segura a rajada.\n *\n * ── O que o motor faz sozinho, e é o que separa gostoso de irritante ───────\n *\n * 1. **Contexto criado só depois de um gesto de verdade.** Navegador nenhum\n *    toca áudio antes disso, e criar o contexto cedo (num hover) só gera aviso\n *    no console. \"Ligado por padrão\" quer dizer **armado**. Nada de banner.\n * 2. **Portão por timbre**: `click` 60 ms, `detent` 30 ms, `toggle` 90 ms,\n *    `drop` 250 ms, `focus` 45 ms, `complete` 300 ms. O hover ainda respeita\n *    60 ms contra qualquer som.\n * 3. **Debounce por alvo.** O mesmo botão não re-dispara hover sem o ponteiro sair.\n * 4. **Hover só em `(hover: hover) and (pointer: fine)`.** No toque, o hover\n *    falso do tap daria dois sons por dedo.\n * 5. **Buffer de ruído gerado uma vez.** Gerar ruído a cada toque é trabalho\n *    de CPU no exato instante em que a pessoa está mirando um botão.\n * 6. **Mudo sob `prefers-reduced-motion`**, o sinal mais próximo que o\n *    navegador dá de \"não me sacode\". A escolha explícita sempre vence.\n * 7. **Limitador no fim da cadeia.** Hover e clique no mesmo gesto somam.\n *\n * Zero dependência, zero arquivo de áudio, zero request: um CSP fechado\n * (`script-src 'self'`) aceita sem hash novo.\n */\n\nconst KEY = \"visant-sfx\";\n\n/** Alvos padrão. Card, item de acordeão e link de texto ficam de fora de\n *  propósito: som em elemento que a pessoa só atravessa é o que transforma\n *  sutil em irritante. */\nexport const SFX_DEFAULT_SELECTOR = \"button, [role='button'], a[data-sfx-target]\";\n\n/** Os timbres que o bus sabe tocar. */\nexport type SfxTimbre =\n  | \"click\"\n  | \"detent\"\n  | \"toggle\"\n  | \"drop\"\n  | \"hover\"\n  | \"chimbau\"\n  | \"focus\"\n  | \"complete\";\n\nexport const SFX_TIMBRES: readonly SfxTimbre[] = [\n  \"click\",\n  \"detent\",\n  \"toggle\",\n  \"drop\",\n  \"hover\",\n  \"chimbau\",\n  \"focus\",\n  \"complete\",\n];\n\n/** Uma batida: ruído curto passando por um filtro, com envelope próprio. */\ninterface Camada {\n  /** Instante em segundos, contado do disparo. */\n  em: number;\n  /** Faixa sorteada somada ao `em`, em segundos (os chiados do `drop`). */\n  sorteio?: number;\n  tipo: BiquadFilterType;\n  hz: number;\n  q: number;\n  pico: number;\n  /** Ataque em segundos. */\n  atk: number;\n  /** Duração total em segundos, ataque incluso. */\n  dur: number;\n  /** Passa-alta extra em série, pra camada que precisa de corte duro embaixo. */\n  corte?: number;\n  /** Frequência pra onde o filtro escorrega até o fim da camada (o sopro do `drop`). */\n  ate?: number;\n}\n\nconst TIMBRES: Record<SfxTimbre, Camada[]> = {\n  click: [\n    { em: 0, tipo: \"bandpass\", hz: 3800, q: 4, pico: 0.11, atk: 0.0006, dur: 0.009, corte: 1200 },\n    { em: 0.016, tipo: \"bandpass\", hz: 2400, q: 7, pico: 0.06, atk: 0.0008, dur: 0.014 },\n  ],\n  detent: [\n    { em: 0, tipo: \"bandpass\", hz: 2900, q: 10, pico: 0.07, atk: 0.0004, dur: 0.006, corte: 1500 },\n  ],\n  toggle: [\n    { em: 0, tipo: \"bandpass\", hz: 2100, q: 5, pico: 0.035, atk: 0.0005, dur: 0.006 },\n    { em: 0.022, tipo: \"bandpass\", hz: 1300, q: 3, pico: 0.07, atk: 0.0008, dur: 0.018 },\n    { em: 0.022, tipo: \"highpass\", hz: 4500, q: 0.7, pico: 0.012, atk: 0.0004, dur: 0.005 },\n  ],\n  // Fole de câmera fechando: o ar sai num \"pff\" que escorrega do agudo pro\n  // médio (filtro descendo de 2400 pra 700 Hz), um toque macio de couro\n  // embaixo e um grão de poeira sorteado no meio. Sem nota, sem cauda.\n  drop: [\n    { em: 0, tipo: \"lowpass\", hz: 240, q: 0.7, pico: 0.07, atk: 0.003, dur: 0.045 },\n    { em: 0.004, tipo: \"bandpass\", hz: 2400, ate: 700, q: 0.9, pico: 0.04, atk: 0.014, dur: 0.14, corte: 320 },\n    { em: 0.03, sorteio: 0.05, tipo: \"highpass\", hz: 4200, q: 0.7, pico: 0.012, atk: 0.0005, dur: 0.004 },\n  ],\n  // Os dois da primeira versão: chimbau fechado, dois passa-altas em série.\n  hover: [\n    { em: 0, tipo: \"highpass\", hz: 8200, q: 0.7, pico: 0.016, atk: 0.0015, dur: 0.028, corte: 8200 },\n  ],\n  chimbau: [\n    { em: 0, tipo: \"highpass\", hz: 6400, q: 0.7, pico: 0.034, atk: 0.006, dur: 0.062, corte: 6400 },\n  ],\n  // Anel de foco de lente: um dente de catraca miúdo e o atrito da graxa.\n  // Mais baixo que o `detent`, porque arrastar slider dispara em rajada.\n  focus: [\n    { em: 0, tipo: \"bandpass\", hz: 3400, q: 8, pico: 0.03, atk: 0.0004, dur: 0.004, corte: 1800 },\n    { em: 0, tipo: \"bandpass\", hz: 900, q: 1.5, pico: 0.012, atk: 0.002, dur: 0.018 },\n  ],\n  // Obturador armando e soltando: a alavanca engata, as lâminas caem e o\n  // corpo da câmera responde. Curto, sem cauda.\n  complete: [\n    { em: 0, tipo: \"bandpass\", hz: 1600, q: 4, pico: 0.04, atk: 0.0008, dur: 0.012 },\n    { em: 0.045, tipo: \"bandpass\", hz: 4200, q: 5, pico: 0.09, atk: 0.0005, dur: 0.008, corte: 1500 },\n    { em: 0.05, tipo: \"bandpass\", hz: 1100, q: 2, pico: 0.05, atk: 0.001, dur: 0.03 },\n  ],\n};\n\n/** Intervalo mínimo entre dois disparos do mesmo timbre, em ms. */\nconst PORTAO: Record<SfxTimbre, number> = {\n  click: 60,\n  detent: 30,\n  toggle: 90,\n  drop: 250,\n  hover: 60,\n  chimbau: 60,\n  focus: 45,\n  complete: 300,\n};\n\n/** ±metade da faixa: `variar(1000, 0.12)` cai entre 940 e 1060. */\nfunction variar(v: number, faixa: number) {\n  return v * (1 + (Math.random() - 0.5) * faixa);\n}\n\nfunction criarRuido(ctx: BaseAudioContext) {\n  const len = Math.floor(ctx.sampleRate * 0.5);\n  const buf = ctx.createBuffer(1, len, ctx.sampleRate);\n  const d = buf.getChannelData(0);\n  for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;\n  return buf;\n}\n\n/** Agenda as camadas de um timbre. Serve ao contexto vivo e ao offline. */\nfunction agendar(\n  ctx: BaseAudioContext,\n  destino: AudioNode,\n  ruido: AudioBuffer,\n  timbre: SfxTimbre,\n  t0: number,\n) {\n  // Um fator de espaçamento por toque, não por camada: o mecanismo inteiro\n  // fica um tico mais rápido ou mais lento, como dedo diferente na mesma tecla.\n  const ritmo = variar(1, 0.3);\n  for (const c of TIMBRES[timbre]) {\n    const t = t0 + c.em * ritmo + (c.sorteio ? Math.random() * c.sorteio : 0);\n    const pico = variar(c.pico, 0.24);\n\n    const src = ctx.createBufferSource();\n    src.buffer = ruido;\n    // Trecho diferente de ruído a cada vez: começar sempre na amostra 0 faz\n    // estalos idênticos, e idêntico lê como loop travado.\n    const off = Math.random() * 0.4;\n\n    const f = ctx.createBiquadFilter();\n    f.type = c.tipo;\n    const hz = variar(c.hz, 0.12);\n    f.frequency.setValueAtTime(hz, t);\n    if (c.ate) f.frequency.exponentialRampToValueAtTime(c.ate * (hz / c.hz), t + c.dur);\n    f.Q.value = c.q;\n    let fim: AudioNode = f;\n    src.connect(f);\n\n    if (c.corte) {\n      const f2 = ctx.createBiquadFilter();\n      f2.type = \"highpass\";\n      f2.frequency.value = c.corte;\n      f2.Q.value = 0.7;\n      f.connect(f2);\n      fim = f2;\n    }\n\n    const g = ctx.createGain();\n    g.gain.setValueAtTime(0.0001, t);\n    g.gain.exponentialRampToValueAtTime(pico, t + c.atk);\n    g.gain.exponentialRampToValueAtTime(0.0001, t + c.dur);\n    fim.connect(g);\n    g.connect(destino);\n\n    src.start(t, off, c.dur + 0.02);\n    src.stop(t + c.dur + 0.02);\n  }\n}\n\nfunction preferenciaSalva() {\n  try {\n    return localStorage.getItem(KEY);\n  } catch {\n    return null;\n  }\n}\n\nfunction ligadoPorPadrao() {\n  try {\n    return !window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  } catch {\n    return true;\n  }\n}\n\n/** Já houve gesto de verdade nesta página? Sem a API (Safari antigo), confia\n *  em quem chamou: o bus só é chamado de dentro de handler de gesto. */\nfunction houveGesto(visto: boolean) {\n  try {\n    const ua = (navigator as Navigator & { userActivation?: { hasBeenActive: boolean } })\n      .userActivation;\n    return ua ? ua.hasBeenActive : true;\n  } catch {\n    return visto;\n  }\n}\n\ntype Ouvinte = (on: boolean) => void;\n\nclass SfxBus {\n  private on = true;\n  private ctx: AudioContext | null = null;\n  private master: GainNode | null = null;\n  private noise: AudioBuffer | null = null;\n  private ultimo = 0;\n  private ultimoPor: Partial<Record<SfxTimbre, number>> = {};\n  private ouvintes = new Set<Ouvinte>();\n  private iniciado = false;\n  private gestoVisto = false;\n\n  /** Idempotente: dá pra chamar em todo mount sem reler o storage à toa. */\n  hidratar() {\n    if (this.iniciado || typeof window === \"undefined\") return;\n    this.iniciado = true;\n    const pref = preferenciaSalva();\n    this.on = pref === null ? ligadoPorPadrao() : pref === \"on\";\n    document.documentElement.setAttribute(\"data-sfx\", this.on ? \"on\" : \"off\");\n  }\n\n  isEnabled() {\n    return this.on;\n  }\n\n  /** Assina mudanças de estado. Devolve o cancelador. */\n  subscribe(fn: Ouvinte) {\n    this.ouvintes.add(fn);\n    return () => {\n      this.ouvintes.delete(fn);\n    };\n  }\n\n  setEnabled(v: boolean, { announce = true } = {}) {\n    this.iniciado = true;\n    this.on = v;\n    try {\n      localStorage.setItem(KEY, v ? \"on\" : \"off\");\n    } catch {\n      /* modo privado do Safari: a preferência vale pela sessão e pronto */\n    }\n    document.documentElement.setAttribute(\"data-sfx\", v ? \"on\" : \"off\");\n    this.ouvintes.forEach((fn) => fn(v));\n    // A confirmação de \"ligado\" é o próprio som, e o som de ligar é o de\n    // alavanca. Dizer com palavra o que dá pra dizer com o efeito é desperdício.\n    if (v && announce) {\n      this.gestoVisto = true;\n      this.play(\"toggle\");\n    }\n  }\n\n  /** Destrava o contexto. Qualquer gesto serve, inclusive um que não toca som.\n   *  Desligado não cria contexto: mudo é mudo até no `AudioContext`. */\n  arm() {\n    this.gestoVisto = true;\n    if (this.on) this.garantir();\n  }\n\n  /** Toca um timbre da paleta. Seguro de chamar em rajada: o portão segura. */\n  play(timbre: SfxTimbre) {\n    if (!TIMBRES[timbre] || !this.portao(timbre)) return;\n    const { ctx, master, noise } = this;\n    if (!ctx || !master || !noise) return;\n    agendar(ctx, master, noise, timbre, ctx.currentTime);\n  }\n\n  /** API da primeira versão: continua valendo. */\n  hover() {\n    this.play(\"hover\");\n  }\n\n  /** API da primeira versão. Agora toca o `click` analógico. */\n  press() {\n    this.play(\"click\");\n  }\n\n  private portao(timbre: SfxTimbre) {\n    if (!this.on) return false;\n    // Antes do primeiro gesto o navegador não deixa tocar, e criar o contexto\n    // mesmo assim só deixa aviso no console.\n    if (!this.ctx && !houveGesto(this.gestoVisto)) return false;\n    const now = Date.now();\n    if (timbre === \"hover\" && now - this.ultimo < PORTAO.hover) return false;\n    if (now - (this.ultimoPor[timbre] ?? 0) < PORTAO[timbre]) return false;\n    this.ultimo = now;\n    this.ultimoPor[timbre] = now;\n    return Boolean(this.garantir());\n  }\n\n  private garantir() {\n    if (this.ctx) {\n      if (this.ctx.state === \"suspended\") void this.ctx.resume().catch(() => {});\n      return this.ctx;\n    }\n    try {\n      const Ctor =\n        window.AudioContext ??\n        (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n      if (!Ctor) return null;\n      const ctx = new Ctor();\n      this.ctx = ctx;\n\n      const master = ctx.createGain();\n      master.gain.value = 0.9;\n\n      /* Limitador: hover e clique disparam juntos no mesmo gesto (o ponteiro\n         entra no botão e clica), e soma sem teto é clipping. */\n      const lim = ctx.createDynamicsCompressor();\n      lim.threshold.value = -8;\n      lim.knee.value = 0;\n      lim.ratio.value = 20;\n      lim.attack.value = 0.003;\n      lim.release.value = 0.2;\n      master.connect(lim);\n      lim.connect(ctx.destination);\n      this.master = master;\n      this.noise = criarRuido(ctx);\n\n      return ctx;\n    } catch {\n      this.ctx = null;\n      return null;\n    }\n  }\n}\n\n/** Singleton: um `AudioContext` por documento, não um por componente. */\nexport const sfx = new SfxBus();\n\n/**\n * Toca um timbre. É a porta pra gesto contínuo: o knob chama\n * `playSfx(\"detent\")` cada vez que cruza um passo, e o portão de 30 ms segura\n * o giro rápido sem perder o ritmo dos dentes.\n */\nexport function playSfx(timbre: SfxTimbre) {\n  sfx.hidratar();\n  sfx.play(timbre);\n}\n\n/**\n * Renderiza um timbre fora do ar, num `OfflineAudioContext`. Serve pra teste e\n * pra medir nível; nunca toca nada e não precisa de gesto.\n */\nexport function renderSfx(timbre: SfxTimbre, sampleRate = 48000): Promise<AudioBuffer> {\n  const ctx = new OfflineAudioContext(1, Math.ceil(sampleRate * 0.25), sampleRate);\n  agendar(ctx, ctx.destination, criarRuido(ctx), timbre, 0);\n  return ctx.startRendering();\n}\n\nfunction isTimbre(v: string | null): v is SfxTimbre {\n  return v !== null && (SFX_TIMBRES as readonly string[]).includes(v);\n}\n\n/**\n * Qual timbre o elemento pede. `data-sfx` explícito vence; o `<html>` fica de\n * fora porque o bus usa `data-sfx=\"on|off\"` nele pra outra coisa (o estado).\n */\nfunction timbreDe(el: Element): SfxTimbre | \"off\" {\n  const marcado = el.closest(\"[data-sfx]\");\n  if (marcado && marcado !== document.documentElement) {\n    const v = marcado.getAttribute(\"data-sfx\");\n    if (v === \"off\") return \"off\";\n    if (isTimbre(v)) return v;\n  }\n  const role = el.getAttribute(\"role\");\n  if (\n    role === \"switch\" ||\n    el.hasAttribute(\"aria-checked\") ||\n    (el instanceof HTMLInputElement && el.type === \"checkbox\")\n  )\n    return \"toggle\";\n  if (\n    role === \"tab\" ||\n    role === \"radio\" ||\n    role === \"option\" ||\n    el.hasAttribute(\"aria-pressed\") ||\n    el.hasAttribute(\"aria-selected\")\n  )\n    return \"detent\";\n  return \"click\";\n}\n\nexport interface SfxWiringOptions {\n  /** Som de hover. Ligado por padrão (compatível com a primeira versão); a\n   *  vitrine desliga, porque passar o ponteiro não é agir. */\n  hover?: boolean;\n}\n\n/**\n * Liga o bus ao documento. Um por app, no topo.\n *\n * A fiação é por **delegação no documento** e não por prop em cada botão, e é\n * essa a decisão que faz o item valer: um `<SfxProvider/>` na raiz sonoriza\n * botão que ainda nem foi escrito, inclusive de biblioteca de terceiro.\n */\nexport function useSfxWiring(\n  selector: string = SFX_DEFAULT_SELECTOR,\n  { hover = true }: SfxWiringOptions = {},\n) {\n  React.useEffect(() => {\n    sfx.hidratar();\n    // Elemento com `data-sfx` explícito também é alvo, mesmo fora do seletor.\n    const alvos = `${selector}, [data-sfx]:not(html)`;\n\n    let fine = false;\n    try {\n      fine = window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches;\n    } catch {\n      fine = false;\n    }\n\n    let ultimoAlvo: Element | null = null;\n    const over = (e: PointerEvent) => {\n      if (e.pointerType && e.pointerType !== \"mouse\") return;\n      const el = (e.target as Element | null)?.closest?.(selector) ?? null;\n      if (!el || el === ultimoAlvo) return; // re-hover no mesmo alvo não re-dispara\n      ultimoAlvo = el;\n      if (el.hasAttribute(\"data-sfx-silent\") || timbreDe(el) === \"off\") return;\n      sfx.hover();\n    };\n    const out = (e: PointerEvent) => {\n      const el = (e.target as Element | null)?.closest?.(selector) ?? null;\n      if (el && el === ultimoAlvo) ultimoAlvo = null;\n    };\n    const down = (e: PointerEvent) => {\n      sfx.arm(); // qualquer gesto destrava o contexto, mesmo sem tocar som\n      const el = (e.target as Element | null)?.closest?.(alvos) ?? null;\n      if (!el) return;\n      if (el.hasAttribute(\"data-sfx-silent\")) return; // quem toca o próprio som\n      if ((el as HTMLButtonElement).disabled || el.getAttribute(\"aria-disabled\") === \"true\")\n        return;\n      const t = timbreDe(el);\n      if (t !== \"off\") sfx.play(t);\n    };\n    const key = () => sfx.arm();\n\n    if (fine && hover) {\n      document.addEventListener(\"pointerover\", over, { passive: true });\n      document.addEventListener(\"pointerout\", out, { passive: true });\n    }\n    document.addEventListener(\"pointerdown\", down, { passive: true });\n    // Teclado destrava igual ao clique, senão quem navega por Tab percorre o\n    // app inteiro em silêncio.\n    document.addEventListener(\"keydown\", key, { passive: true, once: true });\n\n    return () => {\n      document.removeEventListener(\"pointerover\", over);\n      document.removeEventListener(\"pointerout\", out);\n      document.removeEventListener(\"pointerdown\", down);\n      document.removeEventListener(\"keydown\", key);\n    };\n  }, [selector, hover]);\n}\n\n/** Estado reativo do bus, pra desenhar o toggle. `null` até hidratar: a\n *  preferência mora no `localStorage`, que o servidor não enxerga, e chutar no\n *  HTML é um estado errado piscando na tela até a hidratação corrigir. */\nexport function useSfxEnabled() {\n  const [on, setOn] = React.useState<boolean | null>(null);\n  React.useEffect(() => {\n    sfx.hidratar();\n    setOn(sfx.isEnabled());\n    return sfx.subscribe(setOn);\n  }, []);\n  return on;\n}\n\nexport function SfxProvider({\n  selector = SFX_DEFAULT_SELECTOR,\n  hover = true,\n  children,\n}: {\n  /** Alvos que soam. Estenda quando os alvos de decisão do app tiverem outro nome. */\n  selector?: string;\n  /** Som de hover. Desligue onde passar o ponteiro não é agir. */\n  hover?: boolean;\n  children?: React.ReactNode;\n}) {\n  useSfxWiring(selector, { hover });\n  return children as React.ReactElement | null;\n}\n",
      "type": "registry:lib"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Dar som de clique analógico a botões, knobs e alavancas sem arquivo de áudio, em site com tato",
      "evite": "Produto de uso diário ou ambiente de trabalho: sem som; e nunca sem sfx-toggle pra desligar"
    }
  },
  "categories": [
    "fundacao",
    "som",
    "audio",
    "webaudio",
    "feedback",
    "tato",
    "clique",
    "analogico"
  ],
  "type": "registry:lib"
}