{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glitch-chars",
  "title": "Glitch Chars",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Caracteres que trocam sozinhos, em cinco sotaques (símbolos, blocos, braille, binário, hex), ou decodificando até virar uma palavra.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/visant/ui/glitch-chars.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Caracteres que trocam sozinhos: o \"sinal de vida\" mais barato que existe.\n *\n * Nasceu como o ruído ao lado de um texto de status (\"** Processando\"), e é a\n * primitiva por trás de `status-ticker` e `working-glyph`. Um spinner diz\n * \"esperando\"; isso diz \"a máquina está mexendo\", que é outra sensação.\n *\n * 150ms é deliberado: acima de ~200ms lê como piscada quebrada, abaixo de ~100ms\n * vira ruído que cansa. Não é decorativo, é o batimento do sistema.\n *\n * ── Variações ──────────────────────────────────────────────────────────────\n *\n * - `preset` troca o sotaque do ruído sem a pessoa montar a lista na mão:\n *   símbolos (o original), blocos de sombra, braille (o mais fino, lê como\n *   ponto andando), binário e hexadecimal. `chars` ainda manda, se vier.\n * - `text` liga o modo decodificar: o ruído vai assentando da esquerda pra\n *   direita até virar a palavra, segura um respiro (`holdMs`) e embaralha de\n *   novo. É o \"carregou\" de um painel de máquina. Com `loop={false}` assenta\n *   uma vez e fica. O leitor de tela lê a palavra, nunca o ruído.\n *\n * `prefers-reduced-motion`: congela (no ruído, num quadro fixo; no\n * decodificar, já na palavra). Fonte mono, então a largura nunca pula.\n */\n\nconst PRESETS = {\n  simbolos: \"*•□./-®\",\n  blocos: \"░▒▓█▌▐\",\n  braille: \"⠁⠃⠇⡇⣇⣧⣷⣿⠿⠟⠛⠙\",\n  binario: \"01\",\n  hex: \"0123456789ABCDEF\",\n} as const;\n\nexport type GlitchPreset = keyof typeof PRESETS;\n\nconst sortear = (pool: string) => pool[Math.floor(Math.random() * pool.length)];\n\nexport function GlitchChars({\n  chars,\n  preset = \"simbolos\",\n  length = 2,\n  intervalMs = 150,\n  text,\n  holdMs = 1600,\n  loop = true,\n  className,\n  style,\n}: {\n  /** Pool de caracteres sorteados. Passando isto, `preset` é ignorado. */\n  chars?: string;\n  /** Sotaque pronto do ruído. */\n  preset?: GlitchPreset;\n  /**\n   * Quantos caracteres por quadro. Fonte mono garante largura estável.\n   * @min 1\n   * @max 12\n   * @step 1\n   */\n  length?: number;\n  /**\n   * Milissegundos entre quadros.\n   * @min 60\n   * @max 400\n   * @step 10\n   */\n  intervalMs?: number;\n  /** Liga o modo decodificar: o ruído assenta até virar este texto. */\n  text?: string;\n  /**\n   * No modo decodificar, quanto a palavra fica parada antes de embaralhar.\n   * @min 400\n   * @max 5000\n   * @step 100\n   */\n  holdMs?: number;\n  /** No modo decodificar, recomeça depois do respiro. */\n  loop?: boolean;\n  className?: string;\n  style?: React.CSSProperties;\n}) {\n  const pool = chars || PRESETS[preset] || PRESETS.simbolos;\n  const [shown, setShown] = useState(() => text ?? pool.slice(0, length));\n\n  useEffect(() => {\n    const media =\n      typeof window !== \"undefined\" && window.matchMedia\n        ? window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n        : null;\n\n    let id: ReturnType<typeof setInterval> | null = null;\n    let espera: ReturnType<typeof setTimeout> | null = null;\n    const parar = () => {\n      if (id) clearInterval(id);\n      if (espera) clearTimeout(espera);\n      id = null;\n      espera = null;\n    };\n\n    // Decodificar: `assentados` letras da esquerda já são a palavra; o resto\n    // é ruído. Cada quadro assenta meia letra, então palavra longa demora mais,\n    // como uma leitura de verdade.\n    const decodificar = (alvo: string) => {\n      let assentados = 0;\n      id = setInterval(() => {\n        assentados = Math.min(alvo.length, assentados + 0.5);\n        const fixo = Math.floor(assentados);\n        setShown(\n          alvo.slice(0, fixo) +\n            Array.from(alvo.slice(fixo), (c) => (c === \" \" ? \" \" : sortear(pool))).join(\"\"),\n        );\n        if (fixo >= alvo.length) {\n          parar();\n          if (loop) espera = setTimeout(() => decodificar(alvo), holdMs);\n        }\n      }, intervalMs);\n    };\n\n    // A preferência é reavaliada por evento, não só na montagem: quem liga\n    // \"reduzir movimento\" no meio de uma espera longa (é justamente aí que se\n    // liga) ficava com o ruído correndo até trocar de página.\n    const aplicar = () => {\n      parar();\n      if (media?.matches) {\n        setShown(text ?? pool.slice(0, length));\n        return;\n      }\n      if (text) {\n        decodificar(text);\n        return;\n      }\n      id = setInterval(() => {\n        setShown(Array.from({ length }, () => sortear(pool)).join(\"\"));\n      }, intervalMs);\n    };\n\n    aplicar();\n    media?.addEventListener?.(\"change\", aplicar);\n    return () => {\n      parar();\n      media?.removeEventListener?.(\"change\", aplicar);\n    };\n  }, [pool, length, intervalMs, text, holdMs, loop]);\n\n  return (\n    <span\n      className={cn(\"inline-block select-none whitespace-pre font-mono tabular-nums\", className)}\n      style={style}\n    >\n      {/* O ruído é desenho; com `text`, quem lê ouve só a palavra. */}\n      <span aria-hidden=\"true\">{shown}</span>\n      {text ? <span className=\"sr-only\">{text}</span> : null}\n    </span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Textura de caracteres trocando ou palavra decodificando, em hero tech, terminal ou espera curta",
      "evite": "Título que precisa ser lido de primeira: line-reveal ou mist-text; rótulo trocando, text-morph"
    }
  },
  "categories": [
    "texto",
    "textura",
    "mono"
  ],
  "type": "registry:ui"
}