# Decode Text

> Text reveal that scrambles glyphs and resolves them left to right, like an AI decoding — width-stable, with the final text read to screen readers at once.

- **Slug:** `decode-text` · **Category:** effects · **Status:** beta
- **Import:** `import { DecodeText } from "@kura-ui/react/decode-text";`
- **Docs:** https://kuro.abstergo.fyi/components/decode-text
- **Registry item:** https://kuro.abstergo.fyi/r/decode-text.json
- **Tags:** text, reveal, scramble, decode, typewriter, animation, mono, hacker

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/decode-text.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add decode-text
# npm package
npm install @kura-ui/react
```

## When to use

- Revealing a short status or result the agent just produced: “Plan ready”, “Connected to runner-07”.
- Hero taglines or section headings that should arrive with a technical, machine-like flourish.
- IDs, hashes, timings and other mono data appearing after a computation.

## When not to use

- Streaming a model's answer token by token — use streaming-text.
- An in-progress label — use shimmer-text or thinking-indicator.
- Paragraphs or anything longer than a line or two: the effect becomes noise and per-glyph DOM gets heavy.
- Text that changes many times a second (live counters) — render it plainly.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `children` | `string` | — | yes | The final text (a plain string). Changing it replays the reveal. |
| `startOn` | `"mount" \| "view"` | `"mount"` | no | When the first reveal runs: immediately, or the first time ≥ 40% of it scrolls into view (a dim, still scramble shows until then). |
| `trigger` | `string \| number \| boolean` | — | no | Replay whenever this value changes — a counter, an id or a boolean. |
| `duration` | `number` | `1000` | no | Milliseconds for the reveal to sweep the whole text (ease-out). |
| `spread` | `number` | `6` | no | How many characters ahead of the resolved edge scramble actively, in the accent colour. |
| `glyphs` | `string` | — | no | Characters used for scrambling. Default "A–Z 0–9 #%&*+=/<>". Try "01" for binary or a single "·" for a quiet reveal. |
| `as` | `"span" \| "p" \| "div" \| "h1" \| "h2" \| "h3" \| "code"` | `"span"` | no | Element to render. |
| `onComplete` | `() => void` | — | no | Called when a reveal finishes (not called under reduced motion). |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **DecodeText** (`data-slot="decode-text"`) — Root element. `data-state` = pending | running | done.
- **Glyphs** (`data-slot="decode-text-glyphs"`) — The visual (aria-hidden) text; plain text once done.
- **Char** (`data-slot="decode-text-char"`) — One glyph slot while decoding. `data-state` = pending | active | done; `data-char` holds the final character.

## Accessibility

- The final text is always present in a visually hidden span, so screen readers read it immediately and once; the scrambling glyphs are aria-hidden.
- Not a live region. Wrap it in role=status if a replay (new text) should be announced.
- Each glyph reserves the width of its final character, so the line never reflows while decoding.
- Reduced motion: No scramble: the final text renders immediately.

## Notes for agents

- Pass a plain string child. For mixed formatting, wrap several DecodeTexts.
- Monospace (`font-kura-mono`) gives the cleanest look; proportional fonts work because each glyph keeps its final width.
- Use `trigger` to replay without changing the text (e.g. when a card re-opens); change `children` to decode new text.
- Keep it to one or two lines and ≤ 1.5s — it should feel like arrival, not waiting.

## Examples

### Decode on mount (minimal)

```tsx
import { DecodeText } from "@kura-ui/react/decode-text";

export function DecodeTextMinimal() {
  return <DecodeText className="font-kura-mono text-kura-fg text-sm">Agent online</DecodeText>;
}
```

### Triggers, glyph sets and fonts (full)

Replay via `trigger`, text changes, `startOn="view"`, a binary glyph set and a proportional font.

```tsx
"use client";

import { RotateCcw } from "lucide-react";
import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { DecodeText } from "@kura-ui/react/decode-text";

const lines = [
  "Plan ready: 3 steps, 2 files",
  "Migration applied to staging",
  "All 214 tests passing",
];

export function DecodeTextFull() {
  const [i, setI] = React.useState(0);
  const [runs, setRuns] = React.useState(0);
  return (
    <div className="flex w-full max-w-md flex-col items-start gap-5">
      <DecodeText
        as="h3"
        trigger={runs}
        duration={1400}
        spread={8}
        className="font-kura-mono font-medium text-kura-fg text-xl tracking-tight"
      >
        {lines[i % lines.length]!}
      </DecodeText>
      <DecodeText
        as="p"
        startOn="view"
        duration={900}
        glyphs="01"
        className="font-kura-mono text-kura-fg-muted text-xs"
      >
        sha 9f3c2e1 · 412 ms · 1,284 tokens
      </DecodeText>
      <DecodeText as="p" duration={1200} className="text-kura-fg-muted text-sm">
        Proportional fonts keep their width while decoding.
      </DecodeText>
      <div className="flex gap-1.5">
        <Button size="sm" variant="secondary" onClick={() => setI((v) => v + 1)}>
          Next message
        </Button>
        <Button size="sm" variant="ghost" onClick={() => setRuns((r) => r + 1)}>
          <RotateCcw /> Replay
        </Button>
      </div>
    </div>
  );
}
```

### Agent connection status (composition)

A status card that decodes its new state inside a polite live region.

```tsx
"use client";

import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { DecodeText } from "@kura-ui/react/decode-text";

export function DecodeTextAgentHandshake() {
  const [connected, setConnected] = React.useState(false);
  return (
    <div className="flex w-full max-w-sm flex-col gap-3 rounded-kura-xl bg-kura-surface p-4 shadow-kura-sm">
      <div className="flex items-center justify-between">
        <span className="text-kura-fg-subtle text-xs uppercase tracking-wide">Remote agent</span>
        <span
          className="size-2 rounded-full bg-kura-accent data-[on=false]:bg-kura-gray-8"
          data-on={connected}
          aria-hidden
        />
      </div>
      <div role="status" aria-live="polite" className="font-kura-mono text-kura-fg text-sm">
        <DecodeText trigger={connected} duration={1100}>
          {connected ? "Connected to build-runner-07" : "Not connected"}
        </DecodeText>
      </div>
      <Button
        size="sm"
        variant={connected ? "secondary" : "primary"}
        onClick={() => setConnected((c) => !c)}
      >
        {connected ? "Disconnect" : "Connect"}
      </Button>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kura-merge-refs`, `use-reduced-motion`, `utils`
- Related: `streaming-text`, `shimmer-text`, `thinking-indicator`
