# Streaming text

Text that is still arriving. Shows a caret while `streaming`, and announces politely so screen readers hear the finished text rather than every token.

- Registry name: `streaming-text` (`@candor/streaming-text`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/streaming-text

## Install

```bash
npx candor-ui add streaming-text
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/streaming-text`.
It brings in: utils.

## Example

```tsx
"use client"

import * as React from "react"
import { Button } from "@/components/ui/button"
import { ShimmerLabel, StreamingText } from "@/components/streaming-text"

const answer = "Your order shipped this morning and should arrive on Thursday. I’ve sent the tracking link to your email."

export default function Example() {
  const [n, setN] = React.useState(0)
  React.useEffect(() => {
    if (n >= answer.length) return
    const t = setTimeout(() => setN((v) => Math.min(answer.length, v + 3)), 30)
    return () => clearTimeout(t)
  }, [n])
  return (
    <div className="flex max-w-md flex-col gap-3">
      <ShimmerLabel>Looking up your order</ShimmerLabel>
      <StreamingText text={answer.slice(0, n)} streaming={n < answer.length} />
      <Button size="sm" variant="outline" className="w-fit" onClick={() => setN(0)}>
        Replay
      </Button>
    </div>
  )
}
```

## <StreamingText>

Props:

- `text: string`: The text so far.
- `streaming?: boolean | undefined` (default `false`): Show a caret while more is arriving; announced once when it stops.
- `className?: string | undefined`

## <ShimmerLabel>

A label that shimmers while an agent is working ("Thinking", "Reading the docs").

Props:

- `children: React.ReactNode`
- `className?: string | undefined`

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
