# Thread

The scrolling message column. It follows new content while you're at the bottom and stops following the moment you scroll up to read, offering a "Jump to latest" button instead. Pass `empty` for the first-run state and the composer as `footer`.

For long histories, `virtualize` renders only the messages in or near view (each child is one item, keyed by its `key`). The column stays a `log`, the message holding focus stays rendered, and Jump to latest works the same.

It opens at the bottom, or at `anchor` (the `id` of something in it, such as a chapter divider or the card that needs the reader), and then waits for the reader instead of following.

While the reader is scrolled up, "Jump to latest" pulses once each time a new message arrives, then stays still; the jump itself scrolls smoothly unless the OS asks for reduced motion or data-motion is "none".

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

## Install

```bash
npx candor-ui add thread
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/thread`.
It brings in: locale, use-virtual-list, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Composer } from "@/components/composer"
import { Message } from "@/components/message"
import { Thread } from "@/components/thread"
import type { Message as MessageData } from "@/lib/chat"

const agent = { id: "a1", name: "Support agent", role: "assistant" }

const line = (i: number): MessageData => ({
  id: `h${i}`,
  author: i % 2 ? { role: "agent", agent } : { role: "user" },
  status: "complete",
  blocks: [{ kind: "paragraph", nodes: [{ kind: "text", text: i % 2 ? `Order ${1000 + i} shipped on Tuesday.` : `Where is order ${1001 + i}?` }] }],
})
const history = Array.from({ length: 600 }, (_, i) => line(i))

export default function Example() {
  const [value, setValue] = React.useState("")
  const [messages, setMessages] = React.useState<MessageData[]>([])
  const [long, setLong] = React.useState(false)
  const shown = long ? [...history, ...messages] : messages
  const send = (text: string) => {
    setMessages((m) => [
      ...m,
      { id: `u${m.length}`, author: { role: "user" }, status: "complete", blocks: [{ kind: "paragraph", nodes: [{ kind: "text", text }] }] },
      { id: `a${m.length}`, author: { role: "agent", agent }, status: "complete", blocks: [{ kind: "paragraph", nodes: [{ kind: "text", text: "Got it. I’ll look into that." }] }] },
    ])
    setValue("")
  }
  return (
    <div className="flex flex-col gap-2">
      <label className="flex items-center gap-1.5 text-xs text-muted-foreground">
        <input type="checkbox" checked={long} onChange={(e) => setLong(e.target.checked)} />
        A history of 600 messages, virtualised
      </label>
      <div className="h-80 overflow-hidden rounded-lg border">
        <Thread
          className="h-full"
          // virtualize renders only the messages in or near view; each child needs a key.
          virtualize={long}
          empty={<p className="text-sm text-muted-foreground">Ask about an order to start.</p>}
          footer={<Composer value={value} onChange={setValue} onSubmit={send} placeholder="Ask about an order" />}
        >
          {shown.length ? shown.map((m) => <Message key={m.id} message={m} />) : null}
        </Thread>
      </div>
    </div>
  )
}
```

## <Thread>

Props:

- `children?: React.ReactNode | undefined`
- `empty?: React.ReactNode | undefined`: Shown before the first message.
- `footer?: React.ReactNode | undefined`: Below the messages, usually the composer.
- `className?: string | undefined`
- `contentClassName?: string | undefined`: Classes for the message column.
- `virtualize?: boolean | undefined` (default `false`): Render only the messages in or near view, for histories of hundreds of messages. Give each child a `key`. Defaults to false.
- `anchor?: string | undefined`: The `id` of an element in the thread to open at, instead of the bottom: a chapter divider, or the card that needs the reader. Read once, when the messages first appear; if it isn't among them, the thread opens at the bottom. Ignored with `virtualize`, where it may not be rendered yet.
- `scrollerRef?: React.Ref<HTMLDivElement> | undefined`: The scrolling element, for your own scroll handling.
- `onDrop?: ((event: React.DragEvent<HTMLDivElement>) => void) | undefined`: Files or other data dropped on the thread. Dragging over it is allowed only when this is given.
- `fade?: boolean | undefined` (default `true`): The soft fade at the bottom of the column. Defaults to true.

## Strings (`thread`)

The fixed words, English by default. Override any of them under `thread` in `<LocaleProvider strings={{ thread: { … } }}>`; the table is exported as `threadStrings`.

- `messages`: Messages
- `jumpToLatest`: Jump to latest

## 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`.
