# Introduction



Nexus UI is an open-source component library for building AI-powered interfaces. It gives you composable, copy-paste primitives designed for chat, streaming, and multimodal experiences — built on React, Tailwind CSS v4, and Radix UI.

Think of it as **shadcn/ui, purpose-built for AI apps**.

Why Nexus UI? [#why-nexus-ui]

<Cards>
  <Card title="AI-native" icon="AiMagicIcon">
    Built for streaming, tool calls, and multimodal from day one.
  </Card>

  <Card title="Design-first" icon="PaintBoardIcon">
    Every component looks polished out of the box with refined animations and micro-interactions.
  </Card>

  <Card title="Composable" icon="DashboardSquare01Icon">
    Small, single-purpose primitives that compose into full experiences.
  </Card>

  <Card title="AI SDK compatible" icon="TriangleIcon">
    Works with `useChat` and the Vercel AI SDK.
  </Card>

  <Card title="Copy-paste ownership" icon="Copy02Icon">
    You own the code, not a dependency.
  </Card>

  <Card title="Themeable" icon="DarkModeIcon">
    Compatible with shadcn/ui design tokens and themes.
  </Card>
</Cards>

How it works [#how-it-works]

Nexus UI is built on the [shadcn registry](https://ui.shadcn.com/docs/registry). Components are not installed as a package — you copy them into your project and own them entirely.

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn dlx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>
</Tabs>

This copies the component source into your `components/nexus-ui` directory. You can then customize it however you want.

Components [#components]

| Component                                             | Description                                                                                                           |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| [Prompt Input](/docs/components/prompt-input)         | Composable chat input with auto-resizing textarea and action slots                                                    |
| [Suggestions](/docs/components/suggestions)           | Prompt suggestion chips for guiding user input                                                                        |
| [Model Selector](/docs/components/model-selector)     | Dropdown for selecting AI models with radio groups, sub-menus, and custom items                                       |
| [Attachments](/docs/components/attachments)           | Composable file attachments for chat inputs and messages with preview, variants, and upload wiring                    |
| [Message](/docs/components/message)                   | Chat message layout with markdown, optional avatar, actions, and attachments                                          |
| [Thread](/docs/components/thread)                     | Scrollable chat thread with stick-to-bottom scrolling and jump-to-bottom control                                      |
| [Citation](/docs/components/citation)                 | Inline source references with hover preview and multi-source carousel                                                 |
| [Reasoning](/docs/components/reasoning)               | Collapsible model reasoning trace with streaming-aware labels and markdown body                                       |
| [Text Shimmer](/docs/components/text-shimmer)         | Animated shimmer text for loading, tool runs, and other in-progress UI                                                |
| [Image](/docs/components/image)                       | Image renderer for URLs, base64, and byte payloads with preview, loader, lightbox, and action slots                   |
| [Feedback Bar](/docs/components/feedback-bar)         | Feedback prompt bar for per-message or thread ratings with action and close slots                                     |
| [Toaster](/docs/components/toaster)                   | Headless toast notifications powered by Sonner, with variant-aware styling and custom action/cancel controls          |
| [Chain of Thought](/docs/components/chain-of-thought) | Structured multi-step thought timeline with step status, optional expandable output, and auto-close when steps finish |
| [Tool](/docs/components/tool)                         | Status-aware tool call UI with JSON input/output codeblocks                                                           |
| [Questions](/docs/components/questions)               | Follow-up clarification questions with single or multiple choice, carousel navigation, and batch submission           |

More components are being actively developed. See the [roadmap](/docs/roadmap) or follow the repo for updates.

Credits [#credits]

Built by [Victor Williams](https://www.victorwilliams.me). Designed by [Michael Odunsi](https://www.odunsi.xyz).


# Installation



Nexus UI components are designed for React projects using Tailwind CSS v4 and TypeScript. Follow the steps below to get set up.

Prerequisites [#prerequisites]

Make sure your project has the following:

* [React 19](https://react.dev) or later
* [Tailwind CSS v4](https://tailwindcss.com)
* [TypeScript](https://typescriptlang.org)

Nexus UI also depends on shared utilities from [shadcn/ui](https://ui.shadcn.com). If you don't have shadcn set up yet, initialize it first:

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx shadcn@latest init
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest init
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn dlx shadcn@latest init
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx shadcn@latest init
    ```
  </Tab>
</Tabs>

Add registry [#add-registry]

Add the Nexus UI registry to your `components.json` (skip if using Option 1):

```json title="components.json"
{
  "registries": {
    "@nexus-ui": "https://nexus-ui.dev/r/{name}.json"
  }
}
```

Add components [#add-components]

Option 1: shadcn CLI (recommended) [#option-1-shadcn-cli-recommended]

Nexus UI is in the [shadcn directory](https://ui.shadcn.com/registry). No config needed—add components directly:

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn dlx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx shadcn@latest add @nexus-ui/prompt-input
    ```
  </Tab>
</Tabs>

Option 2: Direct URL [#option-2-direct-url]

Add components by passing the registry URL directly:

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx shadcn@latest add https://nexus-ui.dev/r/prompt-input.json
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx shadcn@latest add https://nexus-ui.dev/r/prompt-input.json
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn dlx shadcn@latest add https://nexus-ui.dev/r/prompt-input.json
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx shadcn@latest add https://nexus-ui.dev/r/prompt-input.json
    ```
  </Tab>
</Tabs>

Option 3: Nexus UI CLI [#option-3-nexus-ui-cli]

Install components using the Nexus UI CLI:

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx nexus-ui-cli@latest add prompt-input
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx nexus-ui-cli@latest add prompt-input
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn dlx nexus-ui-cli@latest add prompt-input
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx nexus-ui-cli@latest add prompt-input
    ```
  </Tab>
</Tabs>

Install all components: `npx nexus-ui-cli@latest`

Manual installation [#manual-installation]

If you prefer to install manually, copy the component source from the docs page directly into your project.

Each component page includes the full source code. Copy it into your `components/nexus-ui` directory and install the required dependencies:

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npm install @radix-ui/react-slot
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm add @radix-ui/react-slot
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn add @radix-ui/react-slot
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bun add @radix-ui/react-slot
    ```
  </Tab>
</Tabs>

Project structure [#project-structure]

After adding components, your project will look something like this:

```
components/
├── nexus-ui/
│   └── prompt-input.tsx
├── ui/
│   ├── button.tsx
│   ├── textarea.tsx
│   └── scroll-area.tsx
└── ...
```

Components in `nexus-ui/` are Nexus UI primitives. Components in `ui/` are shared shadcn/ui primitives that Nexus UI components may depend on.

Usage [#usage]

Import and compose components directly:

```tsx noCollapse
import {
  PromptInput,
  PromptInputTextarea,
  PromptInputActions,
} from '@/components/nexus-ui/prompt-input'

export function ChatInput() {
  return (
    <PromptInput>
      <PromptInputTextarea />
      <PromptInputActions>
        <PromptInputActionGroup>
          <PromptInputAction asChild>
            <Button>
              <Paperclip />
            </Button>
          </PromptInputAction>
        </PromptInputActionGroup>
        <PromptInputActionGroup>
          <PromptInputAction asChild>
            <Button>
              <ArrowUp />
            </Button>
          </PromptInputAction>
        </PromptInputActionGroup>
      </PromptInputActions>
    </PromptInput>
  )
}
```

See individual component pages for detailed usage and examples.


# Roadmap



A look at what's shipped and what's ahead. Components are released as they reach production quality.

Released [#released]

* **Prompt Input** — Composable chat input with auto-resizing textarea and action slots
* **Suggestions** — Prompt suggestion chips for guiding user input
* **Model Selector** — Dropdown for selecting AI models with radio groups, sub-menus, and custom items
* **Attachments** — Composable file attachments for chat inputs and messages with preview, variants, and upload wiring
* **Message** — Chat message layout with markdown, optional avatar, actions, and attachments
* **Thread** — Scrollable chat thread with stick-to-bottom scrolling and jump-to-bottom control
* **Citation** — Inline source references with hover preview and multi-source carousel
* **Reasoning** — Collapsible model reasoning trace with streaming-aware labels and markdown body
* **Text Shimmer** — Animated shimmer text for loading, tool runs, and other in-progress UI
* **Image** — Image renderer for URLs, base64, and byte payloads with preview, loader, lightbox, and action slots
* **Feedback Bar** — Feedback prompt bar for per-message or thread ratings with action and close slots
* **Toaster** — Headless toast notifications powered by Sonner, with variant-aware styling and custom action/cancel controls
* **Chain of Thought** — Structured multi-step thought timeline with step status, optional expandable output, and auto-close when steps finish
* **Tool** — Status-aware tool call UI with JSON input/output codeblocks
* **Questions** — Follow-up clarification questions with single or multiple choice, carousel navigation, and batch submission

Planned [#planned]

* **Audio Player** — Playback controls for text-to-speech and audio responses
* **Mic Selector** — Dropdown for choosing input microphone device
* **Queue** — Queue manager for tracking and managing background tasks

Future [#future]

* **Orb** — Animated audio visualization orb for active voice sessions
* **Scrub Bar** — Seekable progress bar for audio playback
* **Voice Selector** — Dropdown for choosing TTS voice and language
* **Voice Input** — Push-to-talk and continuous voice capture with waveform
* **Transcription** — Real-time speech-to-text display with interim and final results

Suggest a component [#suggest-a-component]

Have an idea for a component? [Open an issue](https://github.com/victorcodess/nexus-ui/issues/new) on GitHub.


# Skills



Skills give AI coding agents like Cursor, Claude Code, and Windsurf project-aware context about Nexus UI. When installed, your assistant knows how to find, install, compose, and customize components using the correct APIs and patterns.

For example, you can ask your assistant to:

* *"Add a chat input with send and attach buttons."*
* *"Build a streaming chat interface with messages and a prompt input."*
* *"Show me all available Nexus UI components."*

The skill provides your assistant with component APIs, composition patterns, installation commands, and AI SDK integration — so it generates correct code on the first try.

Install [#install]

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
  <Tab value="npm">
    ```bash
    npx skills add victorcodess/nexus-ui
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm dlx skills add victorcodess/nexus-ui
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    npx skills add victorcodess/nexus-ui
    ```
  </Tab>

  <Tab value="bun">
    ```bash
    bunx skills add victorcodess/nexus-ui
    ```
  </Tab>
</Tabs>

Once installed, your AI assistant automatically loads it when working with Nexus UI components. Learn more at [skills.sh](https://skills.sh).

What's included [#whats-included]

The skill provides your AI assistant with:

Component APIs [#component-apis]

Full reference for all Nexus UI components — sub-components, props, import paths, and composition patterns. The assistant knows the correct way to combine primitives such as `PromptInput` / `PromptInputTextarea`, `Message` / `MessageMarkdown`, `Thread` / `ThreadContent`, and `Citation` / `CitationTrigger`.

Installation & CLI [#installation--cli]

How to add the `@nexus-ui` registry to `components.json`, install components via the shadcn CLI, and manage dependencies.

AI SDK integration [#ai-sdk-integration]

Patterns for using Nexus UI with the [Vercel AI SDK](https://sdk.vercel.ai) — connecting `useChat` to `PromptInputTextarea`, handling form submission, and wiring up streaming.

Styling & customization [#styling--customization]

How to override styles via `className`, use Tailwind CSS v4 design tokens, and customize components after installation.

MCP Server [#mcp-server]

The [Model Context Protocol](https://modelcontextprotocol.io) lets AI tools access Nexus UI components directly from your editor.

Cursor [#cursor]

Create a `.cursor/mcp.json` file in your project root:

```json title=".cursor/mcp.json"
{
  "mcpServers": {
    "nexus-ui": {
      "description": "Nexus UI registry",
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "shadcn@canary", "mcp"],
      "env": {
        "REGISTRY_URL": "https://nexus-ui.dev/api/registry/registry.json"
      }
    }
  }
}
```

Restart Cursor after adding the configuration.

Other AI tools [#other-ai-tools]

For other tools that support MCP (Claude Code, Windsurf, etc.), use the same server configuration above and follow your tool's MCP setup docs.

Usage [#usage]

Once configured, you can interact with the registry directly in chat:

```
Show me all available Nexus UI components
```

```
Add the PromptInput component from Nexus UI
```

LLM resources [#llm-resources]

Machine-readable documentation for LLMs:

| Resource      | URL                                                              |
| ------------- | ---------------------------------------------------------------- |
| llms.txt      | [nexus-ui.dev/llms.txt](https://nexus-ui.dev/llms.txt)           |
| llms-full.txt | [nexus-ui.dev/llms-full.txt](https://nexus-ui.dev/llms-full.txt) |


# Attachments



import AttachmentsDefault from "@/components/nexus-ui/examples/attachments/default";
import AttachmentsVariantDetailed from "@/components/nexus-ui/examples/attachments/variant-detailed";
import AttachmentsVariantInline from "@/components/nexus-ui/examples/attachments/variant-inline";
import AttachmentsVariantPasted from "@/components/nexus-ui/examples/attachments/variant-pasted";
import AttachmentsWithPromptInput from "@/components/nexus-ui/examples/attachments/with-prompt-input";
import AttachmentsWithProgress from "@/components/nexus-ui/examples/attachments/with-progress";
import { Callout } from "@/components/callout";

Composable attachment UI for chat and messaging: thumbnails, type icons, or a **pasted-text** excerpt depending on **`Attachment`** **`variant`**, plus remove actions and optional upload progress. A controlled **`Attachments`** root owns the hidden file input and **`AttachmentTrigger`**; opt in to page drop (**`windowDrop`**, **`AttachmentsDropOverlay`**) and paste or custom flows via **`useAttachments`** and **`appendFiles`**.

<DemoWithCode src="components/nexus-ui/examples/attachments/default.tsx">
  <AttachmentsDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/attachments
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/attachments
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/attachments
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/attachments
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npm install @radix-ui/react-slot class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm add @radix-ui/react-slot class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn add @radix-ui/react-slot class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bun add @radix-ui/react-slot class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/attachments.tsx" title="components/nexus-ui/attachments.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Attachments,
  AttachmentsDropOverlay,
  AttachmentTrigger,
  AttachmentList,
  Attachment,
  useAttachments,
  type AttachmentsContextValue,
} from "@/components/nexus-ui/attachments";
```

```tsx keepBackground noCollapse
<Attachments
  attachments={attachments}
  onAttachmentsChange={setAttachments}
  accept="image/*"
  multiple
>
  <AttachmentTrigger asChild>
    <button type="button">Add files</button>
  </AttachmentTrigger>
  <AttachmentList>
    {attachments.map((a) => (
      <Attachment
        key={`${a.name}-${a.size}`}
        variant="inline"
        attachment={a}
        onRemove={() =>
          setAttachments((prev) => prev.filter((x) => x !== a))
        }
      />
    ))}
  </AttachmentList>
</Attachments>
```

<Callout type="warning">
  `AttachmentTrigger` and any component that opens the file picker must be rendered inside `Attachments`. If mounted outside, the picker will not open because it cannot access shared context.
</Callout>

Examples [#examples]

Detailed variant [#detailed-variant]

Wider tile with thumbnail, file name, and a second line: formatted **size** when **`attachment.size`** is set, otherwise a **kind** label from the file extension (uppercased, e.g. **PDF**, **XLSX**).

<DemoWithCode src="components/nexus-ui/examples/attachments/variant-detailed.tsx">
  <AttachmentsVariantDetailed />
</DemoWithCode>

Inline variant [#inline-variant]

Compact horizontal chip with thumbnail and file name. A hover fade sits over the trailing edge so long names can share space with the remove control.

<DemoWithCode src="components/nexus-ui/examples/attachments/variant-inline.tsx">
  <AttachmentsVariantInline />
</DemoWithCode>

Pasted text variant [#pasted-text-variant]

For **large pasted plain text** (e.g. from **`appendFiles([file], { paste: true })`** after pasting into your prompt), use **`variant="pasted"`** with **`AttachmentMeta.source === "paste"`**. The tile shows a short excerpt and a **Pasted** footer with remove.

<DemoWithCode src="components/nexus-ui/examples/attachments/variant-pasted.tsx">
  <AttachmentsVariantPasted />
</DemoWithCode>

Upload progress [#upload-progress]

Pass **`progress`** (`0`–`100`) on **`Attachment`** to show a thin bar along the bottom of the tile for **`compact`**, **`inline`**, and **`detailed`**. The **`pasted`** variant does not render that bar (omit **`progress`** or it is ignored). Omit **`progress`** when the upload finishes.

<DemoWithCode src="components/nexus-ui/examples/attachments/with-progress.tsx">
  <AttachmentsWithProgress />
</DemoWithCode>

With Prompt Input [#with-prompt-input]

Wrap **[Prompt Input](/docs/components/prompt-input) with `Attachments`** (not the other way around) so **`AttachmentTrigger`**, the list, and the textarea stay in one **`Attachments`** tree. Enable **`windowDrop`** for page-level drag-and-drop (same validation as the picker: **`accept`**, **`maxFiles`**, **`maxSize`**, **`onFilesRejected`**). This example turns that on and adds **`AttachmentsDropOverlay`** (default **`fullscreen`**) for drag feedback—use **`variant="contained"`** inside a **`relative`** shell for in-box chrome only. Pasting **images** uses **`filesFromDataTransfer`**. Long **text** over a threshold becomes a **`text/plain`** file with **`appendFiles([file], { paste: true })`** so **`source: "paste"`** is set and the **`pasted`** tile is used (see **`with-prompt-input.tsx`**).

<DemoWithCode src="components/nexus-ui/examples/attachments/with-prompt-input.tsx">
  <AttachmentsWithPromptInput />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use **Attachments** with the [Vercel AI SDK](https://sdk.vercel.ai) and [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat): `sendMessage` accepts a **`files`** argument (**`FileList`** or an array of [`FileUIPart`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message) objects). The SDK turns them into user message parts for multimodal models.

See [Prompt Input](/docs/components/prompt-input#vercel-ai-sdk-integration) for a minimal chat API route. The same route works when messages include **file** parts—**`convertToModelMessages`** includes those parts in the model request.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    Use the route from the Prompt Input docs, or ensure your handler calls **`streamText`** (or **`generateText`**) with **`messages: await convertToModelMessages(messages)`** so **file** parts are forwarded to the provider.
  </Step>

  <Step>
    <h3>
      Wire Prompt Input, Attachments, and 

      `sendMessage`
    </h3>

    Build **`FileUIPart`** values from your **`AttachmentMeta`** list and pass them as **`files`** (URLs can be data URLs, HTTPS URLs, or blob URLs your app can read; for production, prefer stable URLs after upload). Put **`Attachments`** around **`PromptInput`** so **`AttachmentTrigger`** stays in context and you can later wrap the same **`Attachments`** subtree with a chat-wide drop zone if needed.

    ```tsx
    "use client";

    import { useCallback, useState } from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, type FileUIPart } from "ai";
    import { Button } from "@/components/ui/button";
    import {
      PromptInput,
      PromptInputAction,
      PromptInputActionGroup,
      PromptInputActions,
      PromptInputTextarea,
    } from "@/components/nexus-ui/prompt-input";
    import {
      Attachments,
      Attachment,
      AttachmentList,
      AttachmentTrigger,
      type AttachmentMeta,
    } from "@/components/nexus-ui/attachments";
    import { ArrowUp02Icon, PlusSignIcon, SquareIcon } from "@hugeicons/core-free-icons";
    import { HugeiconsIcon } from "@hugeicons/react";

    function attachmentKey(a: AttachmentMeta) {
      return `${a.name ?? ""}-${a.size ?? ""}-${a.mimeType ?? ""}-${a.source ?? ""}-${a.url ?? ""}`;
    }

    function toFileParts(items: AttachmentMeta[]): FileUIPart[] {
      return items
        .filter((a) => a.url)
        .map((a) => ({
          type: "file" as const,
          url: a.url!,
          mediaType: a.mimeType ?? "application/octet-stream",
          filename: a.name,
        }));
    }

    export default function ChatWithAttachments() {
      const { sendMessage, status } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });
      const [input, setInput] = useState("");
      const [attachments, setAttachments] = useState<AttachmentMeta[]>([]);
      const isLoading = status !== "ready";

      const handleSubmit = useCallback(
        (value?: string) => {
          const trimmed = (value ?? input).trim();
          const files = toFileParts(attachments);
          if (!trimmed && files.length === 0) return;
          sendMessage({
            text: trimmed,
            ...(files.length ? { files } : {}),
          });
          setInput("");
          setAttachments([]);
        },
        [attachments, input, sendMessage],
      );

      return (
        <form
          onSubmit={(e) => {
            e.preventDefault();
            handleSubmit();
          }}
          className="w-full"
        >
          <Attachments
            attachments={attachments}
            onAttachmentsChange={setAttachments}
            accept="image/*"
            multiple
            disabled={isLoading}
          >
            <PromptInput onSubmit={handleSubmit}>
              {attachments.length > 0 ? (
                <AttachmentList className="px-3 pt-3">
                  {attachments.map((item) => (
                    <Attachment
                      key={attachmentKey(item)}
                      variant="inline"
                      attachment={item}
                      onRemove={() =>
                        setAttachments((prev) =>
                          prev.filter((x) => attachmentKey(x) !== attachmentKey(item)),
                        )
                      }
                    />
                  ))}
                </AttachmentList>
              ) : null}
              <PromptInputTextarea
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Message with attachments…"
                disabled={isLoading}
              />
              <PromptInputActions>
                <PromptInputActionGroup>
                  <PromptInputAction>
                    <AttachmentTrigger asChild>
                      <Button
                        type="button"
                        variant="ghost"
                        size="icon-sm"
                        className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
                        disabled={isLoading}
                      >
                        <HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
                      </Button>
                    </AttachmentTrigger>
                  </PromptInputAction>
                </PromptInputActionGroup>
                <PromptInputActionGroup>
                  <PromptInputAction asChild>
                    <Button
                      type="submit"
                      size="icon-sm"
                      className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
                      disabled={isLoading || !input.trim()}
                    >
                      {isLoading ? (
                        <HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
                      ) : (
                        <HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
                      )}
                    </Button>
                  </PromptInputAction>
                </PromptInputActionGroup>
              </PromptInputActions>
            </PromptInput>
          </Attachments>
        </form>
      );
    }
    ```

    The AI SDK [attachments guide](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot#attachments) also covers **`FileList`** and automatic conversion for **`image/*`** and **`text/*`** when you pass a native file input.
  </Step>
</Steps>

API Reference [#api-reference]

Attachments [#attachments]

Controlled root: holds **`AttachmentMeta[]`**, wires **`onAttachmentsChange`**, renders a screen-reader-only **`input type="file"`**, optionally registers **`document`** drag-and-drop when **`windowDrop`** is true (opt-in), and exposes context for **`AttachmentTrigger`**, **`appendFiles`**, and **`isDraggingFile`**. Renders the input first, then **`children`**. Must wrap every **`AttachmentTrigger`** that opens its picker.

Object URLs created by this picker (**`URL.createObjectURL`** for every **`File`** chosen) are tracked and **`URL.revokeObjectURL`** when an attachment leaves the list or when **`Attachments`** unmounts. Blob URLs you attach yourself (outside this flow) are not revoked by the component. Only image and video tiles use **`url`** for built-in previews; other types keep the file icon.

<TypeTable
  type={{
  attachments: {
    type: "AttachmentMeta[]",
    description:
      "Controlled list of attachment metadata shown in the UI and updated when the user picks files.",
  },
  onAttachmentsChange: {
    type: "(attachments: AttachmentMeta[]) => void",
    description:
      "Called with the next list when files are chosen. New items are appended after `maxSize` and slot limits are applied.",
  },
  accept: {
    type: "string",
    description:
      "Passed to the file input `accept` attribute. Also used to filter dropped files when using drag-and-drop.",
  },
  multiple: {
    type: "boolean",
    default: "true",
    description: "Allow multiple files per dialog open.",
  },
  maxFiles: {
    type: "number",
    description: "Maximum total attachments; additional picks are truncated.",
  },
  maxSize: {
    type: "number",
    description: "Maximum size per file in bytes; larger files are skipped.",
  },
  disabled: {
    type: "boolean",
    default: "false",
    description:
      "Disables the file input and prevents `AttachmentTrigger` from opening the dialog.",
  },
  onFileInputChange: {
    type: "React.ChangeEventHandler<HTMLInputElement>",
    description:
      "Optional. Fires after the internal change handler; the event still reflects selected files until the input value is cleared.",
  },
  onFilesRejected: {
    type: "(detail: AttachmentsRejectedFiles) => void",
    description:
      "Optional. When files are not all appended: outside `accept` (`notAccepted`), oversize (`tooLarge`), over `maxFiles` (`overMaxFiles`), or extra when `multiple` is false (`truncatedByMultiple`).",
  },
  windowDrop: {
    type: "boolean",
    default: "false",
    description:
      "When true, registers `dragover` / `drop` on `document` so files can be dropped anywhere in the page.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Triggers, lists, and surrounding layout (for example prompt chrome).",
  },
}}
/>

```ts
export type AttachmentsRejectedFiles = {
  notAccepted: File[];
  tooLarge: File[];
  overMaxFiles: File[];
  truncatedByMultiple: File[];
};
```

AttachmentsDropOverlay [#attachmentsdropoverlay]

Optional visual layer when **`isDraggingFile`** is true (set when **`windowDrop`** is enabled and a file drag is over the document). **`variant="fullscreen"`** (default) portals to **`document.body`** and covers the viewport; **`variant="contained"`** uses **`absolute inset-0`** — place inside a **`relative`** wrapper (e.g. prompt shell). **`pointer-events-none`** so drops still reach **`document`**. Default content is short copy; override with **`children`**. Must be rendered **inside** **`Attachments`**.

<TypeTable
  type={{
  variant: {
    type: '"fullscreen" | "contained"',
    default: '"fullscreen"',
    description:
      "`fullscreen`: portal to `document.body`, fixed full viewport. `contained`: absolute fill of the positioned parent.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Overlay content; default is a short “Drop files to attach” line.",
  },
  className: {
    type: "string",
    description: "Merged with the overlay shell classes.",
  },
}}
/>

Also extends **`React.HTMLAttributes<HTMLDivElement>`** (for example **`style`**, **`id`**) except **`children`** is typed explicitly above.

AttachmentTrigger [#attachmenttrigger]

Button (or slotted child) that opens the **`Attachments`** file dialog. Extends standard **`button`** props; supports **`asChild`** for composing with **`Button`**.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "Merge behavior onto the child element instead of rendering a `button`.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the default `button`.",
  },
  disabled: {
    type: "boolean",
    description:
      "Native disabled attribute; combine with root `disabled` for full picker lockout.",
  },
  type: {
    type: '"button" | "submit" | "reset"',
    default: '"button"',
    description: "Button type when not using `asChild`.",
  },
}}
/>

AttachmentList [#attachmentlist]

Horizontal, scrollable row for attachment tiles. Sets **`role="list"`** by default. Extends **`React.HTMLAttributes<HTMLDivElement>`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description:
      "Additional CSS classes (for example padding inside a prompt).",
  },
  role: {
    type: "string",
    default: '"list"',
    description: "ARIA role for the container.",
  },
}}
/>

Attachment [#attachment]

One attachment tile. Chooses a default layout from **`variant`** unless **`children`** is provided. Renders **`AttachmentProgress`** when **`progress`** is a finite number **except** for **`variant="pasted"`**, which keeps the tile progress-free.

<TypeTable
  type={{
  variant: {
    type: '"compact" | "inline" | "detailed" | "pasted"',
    default: '"compact"',
    description:
      "`compact`: square preview; `inline`: horizontal chip with optional fade for long names; `detailed`: row with metadata; `pasted`: excerpt + Pasted footer for clipboard long-text (`AttachmentMeta.source: 'paste'`).",
  },
  attachment: {
    type: "AttachmentMeta",
    description: "Metadata driving preview, labels, and remove behavior.",
  },
  progress: {
    type: "number",
    description:
      "0–100; bottom progress bar when set and finite. Not shown for `pasted`.",
  },
  onRemove: {
    type: "() => void",
    description: "Called when the default remove control is activated.",
  },
  detailedSubtitle: {
    type: '"size" | "kind"',
    description:
      "`detailed` only. Second line of text; when omitted, inferred from whether `attachment.size` is a positive number.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Replaces the default layout when provided (custom composition).",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the tile root.",
  },
  pastedExcerptMaxChars: {
    type: "number",
    default: "220",
    description: "`pasted` only: excerpt length before an ellipsis.",
  },
}}
/>

AttachmentPreview [#attachmentpreview]

Preview region inside an **`Attachment`**: raster image when **`thumbnailUrl`** or an image **`url`** exists, otherwise the type icon for **`attachment.type`**. For **`variant="pasted"`**, the preview is a multi-line **text excerpt** (from blob / **`data`** / **`url`** content) instead of an icon. Reads **`variant`** and **`attachment`** from context.

<TypeTable
  type={{
  variant: {
    type: '"compact" | "inline" | "detailed" | "pasted"',
    description:
      "Optional override for preview framing; defaults to the parent `Attachment` variant.",
  },
  pastedExcerptMaxChars: {
    type: "number",
    default: "220",
    description: "`pasted` only: excerpt length before an ellipsis.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the preview container.",
  },
}}
/>

AttachmentRemove [#attachmentremove]

Remove control for the current attachment. Default **`aria-label`** uses **`attachment.name`**. Merges **`onClick`** with **`onRemove`** from context. Extends **`button`** props; supports **`asChild`**.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "Merge onto the child element instead of rendering a `button`.",
  },
  position: {
    type: '"corner" | "center-end" | "inline"',
    description:
      "Icon position; defaults to `corner` for `compact` and `detailed`, and `center-end` for `inline`. Use `inline` for footer rows (e.g. `variant: 'pasted'`).",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the remove control.",
  },
  "aria-label": {
    type: "string",
    description:
      "Accessible label; falls back to `Remove ${attachment.name}`.",
  },
}}
/>

AttachmentInfo [#attachmentinfo]

Column wrapper for title and subtitle text in the **detailed** layout. Pure layout; extends **`React.HTMLAttributes<HTMLDivElement>`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes on the info column.",
  },
}}
/>

AttachmentProperty [#attachmentproperty]

Renders a single line of text from **`attachment`**: file **name**, formatted **size**, or a **kind** label from the filename extension (uppercased). The **`as`** prop selects which field to show.

<TypeTable
  type={{
  as: {
    type: '"name" | "size" | "kind"',
    description:
      "`name`: file name; `size`: formatted `attachment.size`; `kind`: uppercased extension when the name has one.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the rendered paragraph.",
  },
}}
/>

AttachmentProgress [#attachmentprogress]

Thin horizontal progress bar along the bottom edge of a tile for **`compact`**, **`inline`**, and **`detailed`**. Usually passed via **`Attachment`** **`progress`**. Not used in the default **`pasted`** layout. **`value`** is clamped to **0–100**.

<TypeTable
  type={{
  value: {
    type: "number",
    description: "Progress from 0 to 100; width of the filled segment.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the track container.",
  },
}}
/>

AttachmentMeta [#attachmentmeta]

Metadata object for one attachment (not a React component). Used with **`Attachments`**, **`Attachment`**, and when mapping to AI SDK **`FileUIPart`** values.

```ts
export interface AttachmentMeta {
  type: "image" | "file" | "video" | "audio";
  name?: string;
  url?: string;
  /** Raster preview URL (e.g. PDF first page). When unset, preview uses the icon for `type`. */
  thumbnailUrl?: string;
  mimeType?: string;
  size?: number;
  width?: number;
  height?: number;
  data?: Blob | ArrayBuffer;
  source?: "paste";
}
```

useAttachments [#useattachments]

Returns the full **`Attachments`** context (same source as internal primitives). Use **`isDraggingFile`** for custom drag chrome when **`windowDrop`** is on, **`appendFiles`** for custom drop targets (or your own **`onDrop`** handlers), **`openPicker`** / **`inputRef`** for advanced wiring. Throws if used outside **`Attachments`**.

```ts noCollapse
export type AppendFilesOptions = {
  paste?: boolean;
};

export type AttachmentsContextValue = {
  inputRef: React.RefObject<HTMLInputElement | null>;
  inputId: string;
  openPicker: () => void;
  appendFiles: (files: File[], options?: AppendFilesOptions) => void;
  isDraggingFile: boolean;
  attachments: AttachmentMeta[];
  onAttachmentsChange: (next: AttachmentMeta[]) => void;
  accept?: string;
  multiple: boolean;
  maxFiles?: number;
  maxSize?: number;
  disabled: boolean;
};

export function useAttachments(): AttachmentsContextValue;
```


# Chain of Thought



import ChainOfThoughtDefault from "@/components/nexus-ui/examples/chain-of-thought/default";
import ChainOfThoughtBasic from "@/components/nexus-ui/examples/chain-of-thought/basic";
import ChainOfThoughtWithContent from "@/components/nexus-ui/examples/chain-of-thought/with-content";
import ChainOfThoughtError from "@/components/nexus-ui/examples/chain-of-thought/error";
import ChainOfThoughtWithoutHeader from "@/components/nexus-ui/examples/chain-of-thought/without-header";

Structured timeline for assistant/tool execution traces. Use it to display sequential steps (web search, code search, file reads, tool calls) with per-step status, optional expandable output, and an auto-closing root when all steps complete.

<DemoWithCode src="components/nexus-ui/examples/chain-of-thought/default.tsx" previewClassName="max-h-none! h-auto min-h-[480px] flex flex-col items-start py-40! justify-start">
  <ChainOfThoughtDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/chain-of-thought
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/chain-of-thought
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/chain-of-thought
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/chain-of-thought
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add collapsible && npm install @hugeicons/react @hugeicons/core-free-icons tw-shimmer
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add collapsible && pnpm add @hugeicons/react @hugeicons/core-free-icons tw-shimmer
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add collapsible && yarn add @hugeicons/react @hugeicons/core-free-icons tw-shimmer
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add collapsible && bun add @hugeicons/react @hugeicons/core-free-icons tw-shimmer
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/chain-of-thought.tsx" title="components/nexus-ui/chain-of-thought.tsx" />

        <ComponentSource src="lib/use-on-change.ts" title="lib/use-on-change.ts" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  ChainOfThought,
  ChainOfThoughtTrigger,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtStepContent,
  ChainOfThoughtComplete,
} from "@/components/nexus-ui/chain-of-thought";
```

```tsx keepBackground noCollapse
<ChainOfThought>
  <ChainOfThoughtTrigger>
    Thinking...
  </ChainOfThoughtTrigger>
  <ChainOfThoughtContent>
    <ChainOfThoughtStep status="active" hasContent>
      <ChainOfThoughtStepTitle icon={<SearchIcon />}>
        Searching the web...
      </ChainOfThoughtStepTitle>
      <ChainOfThoughtStepContent>
        {/* step output */}
      </ChainOfThoughtStepContent>
    </ChainOfThoughtStep>

    <ChainOfThoughtStep status="pending">
      <ChainOfThoughtStepTitle icon={<CodeIcon />}>
        Search codebase for API handlers
      </ChainOfThoughtStepTitle>
    </ChainOfThoughtStep>

    <ChainOfThoughtComplete label="Task complete" />
  </ChainOfThoughtContent>
</ChainOfThought>
```

Typical status progression per step is `pending -> active -> completed` (or `error`).

Examples [#examples]

Basic [#basic]

Simple timeline labels that mirror how coding agents report progress in tools like Cursor. Steps without icons hide connectors by default.

<DemoWithCode src="components/nexus-ui/examples/chain-of-thought/basic.tsx" previewClassName="max-h-none! h-auto min-h-[480px] flex flex-col items-start py-40! justify-start">
  <ChainOfThoughtBasic />
</DemoWithCode>

With Content [#with-content]

Expandable step content for rich outputs like search queries, links, file matches, and analysis payloads.

<DemoWithCode src="components/nexus-ui/examples/chain-of-thought/with-content.tsx" previewClassName="max-h-none! h-auto min-h-[480px] flex flex-col items-start py-40! justify-start">
  <ChainOfThoughtWithContent />
</DemoWithCode>

Error [#error]

Show a failed step with `status="error"` and include contextual details in step content.

<DemoWithCode src="components/nexus-ui/examples/chain-of-thought/error.tsx" previewClassName="max-h-none! h-auto min-h-[480px] flex flex-col items-start py-40! justify-start">
  <ChainOfThoughtError />
</DemoWithCode>

Without Header [#without-header]

Render only the step timeline by omitting `ChainOfThoughtTrigger`. Root stays open and `autoCloseOnAllComplete` is disabled.

<DemoWithCode src="components/nexus-ui/examples/chain-of-thought/without-header.tsx" previewClassName="max-h-none! h-auto min-h-[480px] flex flex-col items-start py-40! justify-start">
  <ChainOfThoughtWithoutHeader />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

`ChainOfThought` works well with `useChat` by mapping assistant tool parts into timeline steps.

Use it with [Vercel AI SDK](https://sdk.vercel.ai) by:

* deriving step status (`pending`/`active`/`completed`/`error`) from tool part state
* rendering tool results inside `ChainOfThoughtStepContent` when available
* letting root auto-close after all steps are completed

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react
    ```
  </Step>

  <Step>
    <h3>
      Create a chat API route that streams tool and text parts
    </h3>

    You can use any `streamText(...).toUIMessageStreamResponse()` route shape used in your app. See [Prompt Input docs](/docs/components/prompt-input#vercel-ai-sdk-integration) for a minimal route scaffold.
  </Step>

  <Step>
    <h3>
      Map assistant parts to Chain of Thought steps
    </h3>

    ```tsx
    "use client";

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, type UIMessage } from "ai";
    import {
      ChainOfThought,
      ChainOfThoughtTrigger,
      ChainOfThoughtContent,
      ChainOfThoughtStep,
      ChainOfThoughtStepTitle,
      ChainOfThoughtStepContent,
      ChainOfThoughtComplete,
      type ChainOfThoughtStepStatus,
    } from "@/components/nexus-ui/chain-of-thought";

    type StepVM = {
      id: string;
      label: string;
      status: ChainOfThoughtStepStatus;
      hasContent?: boolean;
      content?: React.ReactNode;
    };

    function stepsFromAssistant(message: UIMessage): StepVM[] {
      return message.parts.flatMap((part, index) => {
        // AI SDK UI emits tool-specific part types: tool-<toolName>
        if (part.type === "tool-displayWeather") {
          switch (part.state) {
            case "input-available":
              return [
                {
                  id: `weather-${index}`,
                  label: "Running weather tool...",
                  status: "active" as const,
                  hasContent: true,
                  content: <div className="text-xs text-muted-foreground">Fetching forecast...</div>,
                },
              ];
            case "output-available":
              return [
                {
                  id: `weather-${index}`,
                  label: "Weather tool",
                  status: "completed" as const,
                  hasContent: true,
                  content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
                },
              ];
            case "output-error":
              return [
                {
                  id: `weather-${index}`,
                  label: "Weather tool failed",
                  status: "error" as const,
                  hasContent: true,
                  content: <div className="text-xs text-destructive">{part.errorText}</div>,
                },
              ];
          }
        }

        if (part.type === "tool-getStockPrice") {
          switch (part.state) {
            case "input-available":
              return [
                {
                  id: `stock-${index}`,
                  label: "Running stock tool...",
                  status: "active" as const,
                },
              ];
            case "output-available":
              return [
                {
                  id: `stock-${index}`,
                  label: "Stock price tool",
                  status: "completed" as const,
                  hasContent: true,
                  content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
                },
              ];
            case "output-error":
              return [
                {
                  id: `stock-${index}`,
                  label: "Stock price tool failed",
                  status: "error" as const,
                  hasContent: true,
                  content: <div className="text-xs text-destructive">{part.errorText}</div>,
                },
              ];
          }
        }

        return [];
      });
    }

    export default function ChainOfThoughtWithUseChat() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      const steps = stepsFromAssistant(assistant);
      if (steps.length === 0) return null;

      return (
        <ChainOfThought autoCloseOnAllComplete>
          <ChainOfThoughtTrigger>Thinking...</ChainOfThoughtTrigger>
          <ChainOfThoughtContent>
            {steps.map((step) => (
              <ChainOfThoughtStep
                key={step.id}
                status={step.status}
                hasContent={step.hasContent}
              >
                <ChainOfThoughtStepTitle collapsible={Boolean(step.hasContent)}>
                  {step.label}
                </ChainOfThoughtStepTitle>
                {step.hasContent ? (
                  <ChainOfThoughtStepContent>{step.content}</ChainOfThoughtStepContent>
                ) : null}
              </ChainOfThoughtStep>
            ))}
            <ChainOfThoughtComplete label="Task complete" />
          </ChainOfThoughtContent>
        </ChainOfThought>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

ChainOfThought [#chainofthought]

Root container for the thought timeline. Tracks child step statuses to drive trigger activity and optional auto-close behavior. Wraps [Collapsible Root](https://www.radix-ui.com/primitives/docs/components/collapsible#root).

<TypeTable
  type={{
  open: {
    type: "boolean",
    description: "Controlled open state for the root collapsible container.",
  },
  defaultOpen: {
    type: "boolean",
    default: "true",
    description: "Initial open state in uncontrolled mode.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description:
      "Called when root open state changes from user interaction or auto-close.",
  },
  autoCloseOnAllComplete: {
    type: "boolean",
    default: "true",
    description:
      "Automatically closes root once all registered steps reach completed.",
  },
  className: {
    type: "string",
    description: "Additional classes merged with the root wrapper.",
  },
}}
/>

ChainOfThoughtTrigger [#chainofthoughttrigger]

Top trigger row for the thought timeline. Label shimmer is active while the flow is in progress and no step is in `error`. Wraps [Collapsible Trigger](https://www.radix-ui.com/primitives/docs/components/collapsible#trigger).

<TypeTable
  type={{
  icon: {
    type: "React.ReactNode",
    description: "Optional icon rendered before the trigger label.",
  },
  label: {
    type: "React.ReactNode",
    description: "Fallback label when children is not provided.",
  },
  children: {
    type: "React.ReactNode",
    description: "Optional label content. Overrides label when present.",
  },
  className: {
    type: "string",
    description: "Additional trigger classes.",
  },
}}
/>

ChainOfThoughtContent [#chainofthoughtcontent]

Content container for step rows. Wraps [Collapsible Content](https://www.radix-ui.com/primitives/docs/components/collapsible#content).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for spacing and animation wrapper.",
  },
  children: {
    type: "React.ReactNode",
    description: "ChainOfThoughtStep rows and optional completion row.",
  },
}}
/>

ChainOfThoughtStep [#chainofthoughtstep]

A single timeline step with status-aware open behavior and optional expandable content. Wraps [Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible).

<TypeTable
  type={{
  status: {
    type: '"pending" | "active" | "completed" | "error"',
    default: '"pending"',
    description:
      "Step lifecycle state. Active/error can auto-open content; completed can auto-close.",
  },
  hasContent: {
    type: "boolean",
    default: "false",
    description:
      "Marks the step as expandable and enables step content behavior.",
  },
  showConnector: {
    type: "boolean",
    description:
      "Overrides connector visibility. By default it follows whether step title has an icon.",
  },
  open: {
    type: "boolean",
    description: "Controlled open state for this step.",
  },
  defaultOpen: {
    type: "boolean",
    default: "false",
    description:
      "Initial open state for uncontrolled step mode.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description:
      "Called when step open state changes or status-driven auto-open/close occurs.",
  },
  autoCloseOnComplete: {
    type: "boolean",
    default: "true",
    description:
      "When true, closes step content when status transitions to completed.",
  },
}}
/>

ChainOfThoughtStepTitle [#chainofthoughtsteptitle]

Step label row. Can render as static text row or collapsible trigger row. Wraps [Collapsible Trigger](https://www.radix-ui.com/primitives/docs/components/collapsible#trigger).

<TypeTable
  type={{
  icon: {
    type: "React.ReactNode",
    description: "Optional leading icon for the step row.",
  },
  label: {
    type: "React.ReactNode",
    description: "Fallback title when children is not provided.",
  },
  children: {
    type: "React.ReactNode",
    description: "Optional step title content. Overrides label when present.",
  },
  collapsible: {
    type: "boolean",
    description:
      "Forces trigger behavior. By default it follows step hasContent.",
  },
  className: {
    type: "string",
    description: "Additional classes for the title row.",
  },
}}
/>

ChainOfThoughtStepContent [#chainofthoughtstepcontent]

Expandable area for tool output/results. Keep result rendering consumer-defined. Wraps [Collapsible Content](https://www.radix-ui.com/primitives/docs/components/collapsible#content).

<TypeTable
  type={{
  className: {
    type: "string",
    description:
      "Additional classes for content panel and animation wrapper.",
  },
  children: {
    type: "React.ReactNode",
    description: "Any custom step payload (lists, tables, cards, etc.).",
  },
}}
/>

ChainOfThoughtComplete [#chainofthoughtcomplete]

Terminal row for completion state (`Task complete`, `Done`, etc.).

<TypeTable
  type={{
  label: {
    type: "React.ReactNode",
    description: "Completion label text/content.",
  },
  icon: {
    type: "React.ReactNode",
    description: "Optional completion icon.",
  },
  className: {
    type: "string",
    description: "Additional classes for completion row styling.",
  },
}}
/>


# Citation



import CitationDefault from "@/components/nexus-ui/examples/citation/default";
import CitationMultipleSources from "@/components/nexus-ui/examples/citation/multiple-sources";
import CitationTriggerVariants from "@/components/nexus-ui/examples/citation/trigger-variants";
import CitationInlineWithText from "@/components/nexus-ui/examples/citation/inline-with-text";
import { Callout } from "@/components/callout";

Inline chip for showing a **source reference** with **hover preview** (favicon, label, and card copy for title, description, and link). Pass **`citations`** as an array of **`{ url, title?, description? }`**—**one entry** gives a single citation; **several entries** pair with **`CitationCarousel`** and **`CitationCarouselItem`** so users can move between sources in the same preview.

<DemoWithCode src="components/nexus-ui/examples/citation/default.tsx">
  <CitationDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/citation
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add carousel hover-card && npm install radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add carousel hover-card && pnpm add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add carousel hover-card && yarn add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add carousel hover-card && bun add radix-ui @hugeicons/react @hugeicons/core-free-icons tldts
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/citation.tsx" title="components/nexus-ui/citation.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noCollapse
import {
  Citation,
  CitationContent,
  CitationItem,
  CitationTrigger,
} from "@/components/nexus-ui/citation";
```

```tsx keepBackground noCollapse
<Citation citations={[{ url: "https://example.com", title: "Example", description: "…" }]}>
  <CitationTrigger />
  <CitationContent>
    <CitationItem />
  </CitationContent>
</Citation>
```

`CitationItem` renders a default **`h4`** title, **`p`** description, and **`CitationSource`** footer. Toggle blocks with **`showTitle`**, **`showDescription`**, and **`showSource`**, or pass **`children`** to replace the default layout entirely (e.g. custom order or **`CitationSource`** only).

Examples [#examples]

Multiple sources [#multiple-sources]

With more than one **`citations`** entry, the default chip shows **`+N`** after the first source. **`CitationCarousel`** wraps a shadcn **`Carousel`** component inside the hover card so each source is one slide—users move between them with prev/next (or swipe) without closing the preview.

<DemoWithCode src="components/nexus-ui/examples/citation/multiple-sources.tsx">
  <CitationMultipleSources />
</DemoWithCode>

Trigger label and favicon [#trigger-label-and-favicon]

The built-in **`CitationTrigger`** chip can be adjusted with **`showFavicon`** and **`showSiteName`**. Use **`label`** to replace the auto-derived site name with any string—numbers, hostname, or text from **`resolveCitationSource`** / **`parseCitationUrl`** when you want it derived in code.

<DemoWithCode src="components/nexus-ui/examples/citation/trigger-variants.tsx">
  <CitationTriggerVariants />
</DemoWithCode>

Inline with text [#inline-with-text]

The trigger can sit **inline** with surrounding copy—place **`Citation`** where a phrase or sentence needs a source chip.

<DemoWithCode src="components/nexus-ui/examples/citation/inline-with-text.tsx">
  <CitationInlineWithText />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use **Citation** with [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) when your model returns **sources** as [`UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message) **`parts`** (for example **`type: "source-url"`**). The AI SDK does not standardize citation markup across providers, and [Streamdown](https://streamdown.ai/) does not resolve footnotes for you—so you collect URLs from message parts, match them to inline markers like **`[1]`** in the assistant text, and swap those markers for **`Citation`** inside markdown by overriding the anchor renderer.

See [Prompt Input](/docs/components/prompt-input#vercel-ai-sdk-integration) for the same **`POST /api/chat`** shape; below extends it with **`sendSources: true`** and a client-side inline-citation pipeline.

<Callout type="info">
  Citation UI depends on providers and models that emit source parts. If your model returns text only, there are no citations to render.
</Callout>

<Steps>
  <Step>
    <h3>
      Install the AI SDK and a provider that emits sources
    </h3>

    Add **`ai`**, **`@ai-sdk/react`**, and a provider package whose models surface search or citation URLs in the UI message stream (for example **`@ai-sdk/perplexity`** for Sonar). Many chat models stream text only—without **`source-url`** (or equivalent) parts, you have nothing to map **`[1]`** to.

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/perplexity
    ```
  </Step>

  <Step>
    <h3>
      Create a chat API route that streams sources
    </h3>

    Call **`streamText`**, then return **`toUIMessageStreamResponse({ sendSources: true })`** so the client receives source metadata alongside text. Without **`sendSources`**, assistant messages may omit **`source-url`** parts even when the underlying model found URLs. Pick a model your provider documents as search- or citation-capable.

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, type UIMessage } from "ai";
    import { perplexity } from "@ai-sdk/perplexity";

    export async function POST(req: Request) {
      const { messages }: { messages: UIMessage[] } = await req.json();

      const result = streamText({
        model: perplexity("sonar"),
        messages: await convertToModelMessages(messages),
      });

      return result.toUIMessageStreamResponse({
        sendSources: true,
      });
    }
    ```
  </Step>

  <Step>
    <h3>
      Add helpers that turn markers into citation UI
    </h3>

    Assistant prose often contains bracket references (**`[1]`**, **`[1][2][4]`**). Treat the ordered list of **`source-url`** parts as the bibliography: **`[1]`** is the first URL, **`[2]`** the second, and so on. **`withInlineCitationLinks`** rewrites each run of adjacent markers into a single markdown link whose **`href`** uses a dedicated **`https://…`** prefix (custom schemes are often stripped by sanitizers). **`createInlineCitationComponents`** passes a custom **`a`** component to Streamdown: real links stay as **`<a>`**; marker links parse the id list, resolve rows from your sources array, and render **`Citation`**, **`CitationTrigger`**, and **`CitationContent`**.

    ```tsx title="lib/inline-citations.tsx"
    "use client";

    import * as React from "react";
    import {
      Citation,
      CitationCarousel,
      CitationCarouselContent,
      CitationCarouselHeader,
      CitationCarouselIndex,
      CitationCarouselItem,
      CitationCarouselNext,
      CitationCarouselPagination,
      CitationCarouselPrev,
      CitationContent,
      CitationItem,
      CitationSourcesBadge,
      CitationTrigger,
      type CitationSourceInput,
    } from "@/components/nexus-ui/citation";

    const GROUP_RE = /((?:\[\d+\])+)/g;
    const ID_RE = /\[(\d+)\]/g;
    const PREFIX = "https://citations.local/";

    export function withInlineCitationLinks(text: string) {
      return text.replace(GROUP_RE, (match) => {
        const ids = [...match.matchAll(ID_RE)]
          .map(([, id]) => Number(id))
          .filter((id) => Number.isInteger(id) && id > 0);
        if (ids.length === 0) return match;
        const label = ids.map((id) => `[${id}]`).join("");
        return `[${label}](${PREFIX}${ids.join(",")})`;
      });
    }

    function parseIdsFromHref(href: string) {
      if (!href.startsWith(PREFIX)) return [];
      return href
        .slice(PREFIX.length)
        .split(",")
        .map((id) => Number(id))
        .filter((id) => Number.isInteger(id) && id > 0);
    }

    function citationsFromIds(ids: number[], sources: CitationSourceInput[]) {
      return Array.from(new Set(ids))
        .map((id) => sources[id - 1])
        .filter(Boolean) as CitationSourceInput[];
    }

    export function createInlineCitationComponents(sources: CitationSourceInput[]) {
      return {
        a: ({ href, children, ...props }: any) => {
          if (typeof href !== "string") {
            return React.createElement("a", { ...props, href }, children);
          }

          const ids = parseIdsFromHref(href);
          if (ids.length === 0) {
            return React.createElement("a", { ...props, href }, children);
          }

          const citations = citationsFromIds(ids, sources);
          if (citations.length === 0) return <>{children}</>;

          return (
            <>
              {" "}
              <Citation citations={citations}>
                <CitationTrigger />
                <CitationContent>
                  {citations.length > 1 ? (
                    <CitationCarousel>
                      <CitationCarouselHeader>
                        <CitationSourcesBadge />

                        <CitationCarouselPagination>
                          <CitationCarouselPrev />
                          <CitationCarouselIndex />
                          <CitationCarouselNext />
                        </CitationCarouselPagination>
                      </CitationCarouselHeader>

                      <CitationCarouselContent>
                        {citations.map((citation, index) => (
                          <CitationCarouselItem key={citation.url} index={index}>
                            <CitationItem />
                          </CitationCarouselItem>
                        ))}
                      </CitationCarouselContent>
                    </CitationCarousel>
                  ) : (
                    <CitationItem />
                  )}
                </CitationContent>
              </Citation>
            </>
          );
        },
      };
    }
    ```
  </Step>

  <Step>
    <h3>
      Render assistant markdown with 

      `useChat`

       and MessageMarkdown
    </h3>

    For each assistant message, join **`text`** parts (for example with **`isTextUIPart`**), collect **`source-url`** parts into **`{ url, title }`** objects for **`Citation`**, then pass **`createInlineCitationComponents(sources)`** into **`MessageMarkdown`** via **`components`**. Feed **`withInlineCitationLinks(text)`** as children so markers become links before Streamdown renders. The example below shows one assistant turn; in a full chat, map **`messages`** the same way as in [Message](/docs/components/message#vercel-ai-sdk-integration).

    ```tsx title="app/citations-chat/page.tsx"
    "use client";

    import * as React from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
    import { MessageMarkdown } from "@/components/nexus-ui/message";
    import {
      createInlineCitationComponents,
      withInlineCitationLinks,
    } from "@/lib/inline-citations";

    function textFromMessage(message: UIMessage) {
      return message.parts.filter(isTextUIPart).map((p) => p.text).join("");
    }

    function sourceUrlPartsFromMessage(message: UIMessage) {
      return message.parts.filter(
        (p): p is Extract<UIMessage["parts"][number], { type: "source-url" }> =>
          p.type === "source-url",
      );
    }

    export default function CitationsChatPage() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      const text = textFromMessage(assistant);
      const sources = sourceUrlPartsFromMessage(assistant).map((s) => ({
        url: s.url,
        title: s.title?.trim() || s.url,
      }));

      return (
        <MessageMarkdown components={createInlineCitationComponents(sources)}>
          {withInlineCitationLinks(text)}
        </MessageMarkdown>
      );
    }
    ```
  </Step>
</Steps>

Provider limits and enriching previews [#provider-limits-and-enriching-previews]

Only some providers and models return sources through the AI SDK (for example Perplexity Sonar, some Grok variants). Others stream plain text with no **`source-url`** parts. When sources exist, their shape can differ by provider.

Source parts usually give you at least a **URL**. **`Citation`** accepts optional **`title`** and **`description`** for the hover card; the SDK does not fetch page metadata for you. To show real titles or snippets, call a link-preview or metadata service (or your own scraper) and merge the results into the objects you pass to **`citations`** before rendering.

API Reference [#api-reference]

Citation [#citation]

Root component. **Normalizes** **`citations`**, stores **citation source data** and **carousel** state in context for descendants, and wraps **[Hover Card](https://www.radix-ui.com/primitives/docs/components/hover-card#root)**.

<TypeTable
  type={{
  defaultOpen: {
    type: "boolean",
    description: "Uncontrolled initial open state.",
  },
  open: {
    type: "boolean",
    description: "Controlled open state.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description: "Called when the open state changes.",
  },
  openDelay: {
    type: "number",
    description:
      "The duration from when the mouse enters the trigger or content until the hover card opens.",
    default: "50ms",
  },
  closeDelay: {
    type: "number",
    description:
      "The duration from when the mouse leaves the trigger or content until the hover card closes.",
    default: "50ms",
  },
  citations: {
    type: "CitationSourceInput[]",
    description:
      "Source(s) to show. Each item requires url; title and description are optional for the preview.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically CitationTrigger and CitationContent (and carousel or item primitives inside the content).",
  },
}}
/>

CitationTrigger [#citationtrigger]

The **source chip** users see in the UI. Wraps **[Hover Card Trigger](https://www.radix-ui.com/primitives/docs/components/hover-card#trigger)** so hovering it opens the **preview** (`CitationContent`). Adjust the chip with **`label`**, **`showFavicon`**, and **`showSiteName`**.

<TypeTable
  type={{
  label: {
    type: "React.ReactNode",
    description:
      "Replaces the auto-derived site-name text on the default chip.",
  },
  showFavicon: {
    type: "boolean",
    default: "true",
    description: "Show the first source’s favicon on the default chip.",
  },
  showSiteName: {
    type: "boolean",
    default: "true",
    description:
      "Show site-name (or label) text. When false with favicon on, yields a favicon-only chip.",
  },
  className: {
    type: "string",
    description: "Merged with default chip styles.",
  },
}}
/>

CitationContent [#citationcontent]

Preview card that appears when the trigger is hovered and shows the source details; Wraps **[Hover Card Content](https://www.radix-ui.com/primitives/docs/components/hover-card#content)**.

<TypeTable
  type={{
  align: {
    type: '"start" | "center" | "end"',
    default: '"center"',
    description: "Alignment relative to the trigger.",
  },
  alignOffset: {
    type: "number",
    default: "0",
    description: "Pixel offset along the alignment axis.",
  },
  side: {
    type: '"top" | "right" | "bottom" | "left"',
    default: '"bottom"',
    description: "Preferred side of the trigger for the popover.",
  },
  sideOffset: {
    type: "number",
    default: "4",
    description: "Distance from the trigger in px.",
  },
  className: {
    type: "string",
    description: "Merged with default card shell styles.",
  },
}}
/>

CitationCarousel [#citationcarousel]

[**Carousel**](https://ui.shadcn.com/docs/components/radix/carousel) root for multi-source slides. **`setApi`** registers the API on **`Citation`** (for nav/index) and invokes any **`setApi`** you pass.

<TypeTable
  type={{
  orientation: {
    type: '"horizontal" | "vertical"',
    default: '"horizontal"',
    description:
      "Carousel axis; forwarded to Embla via the shared Carousel primitive.",
  },
  opts: {
    type: "object",
    description: "Optional Embla carousel options.",
  },
  plugins: {
    type: "EmblaPlugin | EmblaPlugin[]",
    description: "Optional Embla plugins array.",
  },
  setApi: {
    type: "(api: CarouselApi | undefined) => void",
    description:
      "Optional; invoked when the Embla instance is ready or cleared.",
  },
  className: {
    type: "string",
    description: "Classes on the carousel region wrapper.",
  },
}}
/>

CitationCarouselHeader [#citationcarouselheader]

Header row above the slide viewport; Usually contains a summary **`CitationSource`** and **`CitationCarouselPagination`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default includes horizontal padding and top padding.",
  },
}}
/>

CitationCarouselContent [#citationcarouselcontent]

Wraps **[Carousel Content](https://ui.shadcn.com/docs/components/radix/carousel#content)** and syncs viewport **height** to the **active** slide.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Classes on the flex track inside the overflow viewport.",
  },
}}
/>

CitationCarouselItem [#citationcarouselitem]

One carousel slide; **`index`** selects **`citations[index]`** and sets **`CitationItemContext`**. Wraps **[Carousel Item](https://ui.shadcn.com/docs/components/radix/carousel#item)**.

<TypeTable
  type={{
  index: {
    type: "number",
    description: "Zero-based index into Citation citations.",
  },
  className: {
    type: "string",
    description: "Merged with slide layout (includes self-start).",
  },
}}
/>

CitationCarouselPagination [#citationcarouselpagination]

Flex row for nav controls.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default flex row with gap for prev / index / next.",
  },
}}
/>

CitationCarouselPrev / CitationCarouselNext [#citationcarouselprev--citationcarouselnext]

Icon buttons for prev/next navigation; Standard **`button`** HTML attributes.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default Hugeicons arrows.",
  },
  className: {
    type: "string",
    description: "Merged with default circular button styles.",
  },
}}
/>

CitationCarouselIndex [#citationcarouselindex]

**`current / count`** from context (**1-based** current). **`span`** HTML attributes.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Default tabular-nums muted text.",
  },
}}
/>

CitationItem [#citationitem]

Preview row as an **`<a>`**, containing the source details; **`href`** defaults to the citation URL. Default stack: **`h4`** title, **`p`** description, **`CitationSource`** footer.

<TypeTable
  type={{
  showTitle: {
    type: "boolean",
    default: "true",
    description: "Show the title block.",
  },
  showDescription: {
    type: "boolean",
    default: "true",
    description: "Show the description block.",
  },
  showSource: {
    type: "boolean",
    default: "true",
    description: "Show the footer source row.",
  },
  href: {
    type: "string",
    description: "Active or item-scoped citation url.",
  },
  className: {
    type: "string",
    description: "Default column layout with padding inside the hover card.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Replaces the default title, description, and source stack when set.",
  },
}}
/>

CitationSource [#citationsource]

Citation source chip with the favicon and site name.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default flex row with gap.",
  },
}}
/>

CitationFavicon [#citationfavicon]

Favicon of the citation site. **`src`** overrides **`faviconSrc`** from context.

<TypeTable
  type={{
  src: {
    type: "string",
    description: "Optional favicon URL; defaults from citation context.",
  },
  className: {
    type: "string",
    description: "Wrapper div classes (size defaults to icon scale).",
  },
}}
/>

CitationSiteName [#citationsitename]

Site name of the citation site.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default truncate text-primary.",
  },
}}
/>

CitationSourcesBadge [#citationsourcesbadge]

Chip with an optional overlapping favicon stack plus a **`{n} source(s)`** label. Use in **`CitationCarouselHeader`**, message action rows, or anywhere inside **`Citation`**.

<TypeTable
  type={{
  showFavicons: {
    type: "boolean",
    default: "true",
    description: "Show the stacked favicons before the label.",
  },
  label: {
    type: "React.ReactNode",
    description: "Optional; defaults to “N source(s)” from citation count.",
  },
  className: {
    type: "string",
    description: "Secondary rounded-full row; tight padding for the chip.",
  },
}}
/>

parseCitationUrl [#parsecitationurl]

Tolerant parse to a **`URL`**: trims the string and, when no **`http`/`https`** scheme is present, prefixes **`https://`** so bare hostnames resolve. Use for hostname extraction, favicon lookups, or custom **`CitationTrigger`** labels outside the component tree.

```ts
export function parseCitationUrl(urlStr: string): URL;
```

resolveCitationSource [#resolvecitationsource]

Maps one **`CitationSourceInput`** to **`ResolvedCitation`**—normalized **`url`**, optional **`title`** / **`description`**, derived **`siteName`**, and a **`faviconSrc`** URL. **`resolveCitationSources`** maps an array; **`Citation`** uses this pipeline when resolving the **`citations`** prop.

```ts noCollapse
export type CitationSourceInput = {
  url: string;
  title?: React.ReactNode;
  description?: React.ReactNode;
};

export type ResolvedCitation = {
  url: string;
  title: React.ReactNode | null;
  description: React.ReactNode | null;
  siteName: string;
  faviconSrc: string;
};

export function resolveCitationSource(
  input: CitationSourceInput,
): ResolvedCitation;

export function resolveCitationSources(
  inputs: CitationSourceInput[],
): ResolvedCitation[];
```


# Feedback Bar



import FeedbackBarDefault from "@/components/nexus-ui/examples/feedback-bar/default";
import FeedbackBarCompactMinimal from "@/components/nexus-ui/examples/feedback-bar/compact-minimal";

Composable feedback bar for both individual AI responses and full conversation rating flows, with flexible action and close slots for thumbs, quick sentiment, or custom controls.

<DemoWithCode src="components/nexus-ui/examples/feedback-bar/default.tsx">
  <FeedbackBarDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/feedback-bar
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/feedback-bar
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/feedback-bar
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/feedback-bar
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add tooltip kbd && npm install @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add tooltip kbd && pnpm add @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add tooltip kbd && yarn add @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add tooltip kbd && bun add @radix-ui/react-slot
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/feedback-bar.tsx" title="components/nexus-ui/feedback-bar.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  FeedbackBar,
  FeedbackBarContent,
  FeedbackBarPrompt,
  FeedbackBarLabel,
  FeedbackBarActions,
  FeedbackBarAction,
  FeedbackBarClose,
} from "@/components/nexus-ui/feedback-bar";
```

```tsx keepBackground
<FeedbackBar>
  <FeedbackBarContent>
    <FeedbackBarPrompt>
      <InfoIcon />
      <FeedbackBarLabel>Is this helpful?</FeedbackBarLabel>
    </FeedbackBarPrompt>
    <FeedbackBarActions>
      <FeedbackBarAction asChild>
        <Button type="button">Like</Button>
      </FeedbackBarAction>
    </FeedbackBarActions>
  </FeedbackBarContent>
  <FeedbackBarClose>
    <Button type="button">Close</Button>
  </FeedbackBarClose>
</FeedbackBar>
```

Examples [#examples]

Icon-only Variant [#icon-only-variant]

Use icon-only actions with a close control for tight layouts and quick response feedback.

<DemoWithCode src="components/nexus-ui/examples/feedback-bar/compact-minimal.tsx">
  <FeedbackBarCompactMinimal />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

`FeedbackBar` is intentionally neutral, so you can place it at two different feedback scopes:

* **Response-based feedback:** capture sentiment for one assistant message.
* **Conversation-based feedback:** capture sentiment for the entire chat.

Response-based feedback [#response-based-feedback]

Attach feedback to a specific assistant message id.

```tsx
"use client";

import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { HugeiconsIcon } from "@hugeicons/react";
import { ThumbsUpIcon, ThumbsDownIcon } from "@hugeicons/core-free-icons";
import { Button } from "@/components/ui/button";
import {
  FeedbackBar,
  FeedbackBarAction,
  FeedbackBarActions,
  FeedbackBarContent,
  FeedbackBarLabel,
  FeedbackBarPrompt,
} from "@/components/nexus-ui/feedback-bar";

type Vote = "up" | "down";

export default function MessageFeedback() {
  const { messages } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });
  const [votes, setVotes] = useState<Record<string, Vote>>({});

  async function submitMessageFeedback(messageId: string, vote: Vote) {
    setVotes((prev) => ({ ...prev, [messageId]: vote }));

    await fetch("/api/feedback/message", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messageId, vote }),
    });
  }

  return (
    <div className="flex flex-col gap-4">
      {messages
        .filter((m) => m.role === "assistant")
        .map((m) => (
          <FeedbackBar key={m.id}>
            <FeedbackBarContent>
              <FeedbackBarPrompt>
                <FeedbackBarLabel>
                  {votes[m.id] ? "Thanks for your feedback." : "Was this response helpful?"}
                </FeedbackBarLabel>
              </FeedbackBarPrompt>
              <FeedbackBarActions>
                <FeedbackBarAction asChild>
                  <Button
                    type="button"
                    variant={votes[m.id] === "up" ? "secondary" : "ghost"}
                    size="icon-sm"
                    onClick={() => submitMessageFeedback(m.id, "up")}
                    aria-label="Helpful"
                  >
                    <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2.0} className="size-4" />
                  </Button>
                </FeedbackBarAction>
                <FeedbackBarAction asChild>
                  <Button
                    type="button"
                    variant={votes[m.id] === "down" ? "secondary" : "ghost"}
                    size="icon-sm"
                    onClick={() => submitMessageFeedback(m.id, "down")}
                    aria-label="Not helpful"
                  >
                    <HugeiconsIcon icon={ThumbsDownIcon} strokeWidth={2.0} className="size-4" />
                  </Button>
                </FeedbackBarAction>
              </FeedbackBarActions>
            </FeedbackBarContent>
          </FeedbackBar>
        ))}
    </div>
  );
}
```

Conversation-based feedback [#conversation-based-feedback]

Show one feedback bar for the whole conversation (for example at the end of the thread).

```tsx
"use client";

import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { HugeiconsIcon } from "@hugeicons/react";
import { ThumbsUpIcon, ThumbsDownIcon } from "@hugeicons/core-free-icons";
import { Button } from "@/components/ui/button";
import {
  FeedbackBar,
  FeedbackBarAction,
  FeedbackBarActions,
  FeedbackBarContent,
  FeedbackBarLabel,
  FeedbackBarPrompt,
} from "@/components/nexus-ui/feedback-bar";

type Vote = "up" | "down";

export default function ConversationFeedback() {
  const { messages } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });
  const conversationId = "replace-with-your-session-id";
  const [vote, setVote] = useState<Vote | null>(null);

  const hasAssistantReply = messages.some((m) => m.role === "assistant");
  if (!hasAssistantReply) return null;

  async function submitConversationFeedback(nextVote: Vote) {
    setVote(nextVote);

    await fetch("/api/feedback/conversation", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ conversationId, vote: nextVote }),
    });
  }

  return (
    <FeedbackBar>
      <FeedbackBarContent>
        <FeedbackBarPrompt>
          <FeedbackBarLabel>
            {vote ? "Thanks for rating this conversation." : "How was this conversation?"}
          </FeedbackBarLabel>
        </FeedbackBarPrompt>
        <FeedbackBarActions>
          <FeedbackBarAction asChild>
            <Button
              type="button"
              variant={vote === "up" ? "secondary" : "ghost"}
              size="icon-sm"
              onClick={() => submitConversationFeedback("up")}
              aria-label="Good conversation"
            >
              <HugeiconsIcon icon={ThumbsUpIcon} strokeWidth={2.0} className="size-4" />
            </Button>
          </FeedbackBarAction>
          <FeedbackBarAction asChild>
            <Button
              type="button"
              variant={vote === "down" ? "secondary" : "ghost"}
              size="icon-sm"
              onClick={() => submitConversationFeedback("down")}
              aria-label="Poor conversation"
            >
              <HugeiconsIcon icon={ThumbsDownIcon} strokeWidth={2.0} className="size-4" />
            </Button>
          </FeedbackBarAction>
        </FeedbackBarActions>
      </FeedbackBarContent>
    </FeedbackBar>
  );
}
```

API Reference [#api-reference]

FeedbackBar [#feedbackbar]

Root container for the feedback row.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the root container.",
  },
}}
/>

FeedbackBarContent [#feedbackbarcontent]

Main area that holds info and actions.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the main layout row.",
  },
}}
/>

FeedbackBarPrompt [#feedbackbarprompt]

Left-side info section for icon and question copy.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the info section.",
  },
}}
/>

FeedbackBarLabel [#feedbackbarlabel]

Text label for the feedback prompt.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the label text.",
  },
}}
/>

FeedbackBarActions [#feedbackbaractions]

Container for action controls.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the actions row.",
  },
}}
/>

FeedbackBarAction [#feedbackbaraction]

Wrapper for a single action item. Use `asChild` to render your own interactive control directly (for example, a `Button`). Supports built-in tooltips via `tooltip` as either a string or object (`content`, optional `side`, optional `shortcut`).

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description: "Render as the child element instead of a div.",
  },
  className: {
    type: "string",
    description: "Additional classes for the action wrapper.",
  },
  tooltip: {
    type: 'string | { content?: string; side?: "top" | "right" | "bottom" | "left"; shortcut?: string }',
    description:
      "Tooltip config. String form maps to tooltip content. Object form supports content, side, and keyboard shortcut. If content is omitted, no tooltip is rendered.",
  },
}}
/>

FeedbackBarClose [#feedbackbarclose]

Right-side close section with a left divider.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for the close section wrapper.",
  },
  tooltip: {
    type: 'string | { content?: string; side?: "top" | "right" | "bottom" | "left"; shortcut?: string }',
    description:
      "Tooltip config for the close action. String form maps to tooltip content. Object form supports content, side, and keyboard shortcut. If content is omitted, no tooltip is rendered.",
  },
}}
/>


# Image



import ImageWithActions from "@/components/nexus-ui/examples/image/with-actions";
import ImageDefault from "@/components/nexus-ui/examples/image/default";
import ImageNoSourcePlaceholder from "@/components/nexus-ui/examples/image/no-source-placeholder";
import ImageExternalSrc from "@/components/nexus-ui/examples/image/external-src";
import ImageLightboxExample from "@/components/nexus-ui/examples/image/lightbox";

Composable image primitives for multimodal UIs. `Image` accepts AI-generated payloads (`base64` or `uint8Array`) and exposes `ImagePreview`, `ImageLoader`, and optional action slots.

<DemoWithCode src="components/nexus-ui/examples/image/default.tsx">
  <ImageDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/image
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/image
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/image
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/image
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add tooltip kbd && npm install @radix-ui/react-slot @radix-ui/react-dialog
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add tooltip kbd && pnpm add @radix-ui/react-slot @radix-ui/react-dialog
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add tooltip kbd && yarn add @radix-ui/react-slot @radix-ui/react-dialog
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add tooltip kbd && bun add @radix-ui/react-slot @radix-ui/react-dialog
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/image.tsx" title="components/nexus-ui/image.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Image,
  ImagePreview,
  ImageLightbox,
  ImageLightboxOverlay,
  ImageLightboxPreview,
  ImageLightboxClose,
  ImageLoader,
  ImageActions,
  ImageActionGroup,
  ImageAction,
} from "@/components/nexus-ui/image";
```

```tsx keepBackground
<Image
  base64={base64Image}
  mediaType="image/png"
  uint8Array={new Uint8Array([])}
  alt="Generated image"
/>
```

Examples [#examples]

With Overlay Actions [#with-overlay-actions]

Compose `ImagePreview` with `ImageActions`, `ImageActionGroup`, and `ImageAction` for top/bottom/side controls. `ImageAction` supports built-in tooltips via `tooltip` as a string or object (`content`, optional `side`, optional `shortcut`).

<DemoWithCode src="components/nexus-ui/examples/image/with-actions.tsx" previewClassName="h-auto sm:h-[412px]">
  <ImageWithActions />
</DemoWithCode>

External URL [#external-url]

Use `src` for regular image URLs when you want `Image` to behave like a standard `<img>` source renderer.

<DemoWithCode src="components/nexus-ui/examples/image/external-src.tsx">
  <ImageExternalSrc />
</DemoWithCode>

No Source Placeholder [#no-source-placeholder]

Render `Image` without `base64` or `uint8Array` source to show the built-in placeholder loader.

<DemoWithCode src="components/nexus-ui/examples/image/no-source-placeholder.tsx">
  <ImageNoSourcePlaceholder />
</DemoWithCode>

Lightbox Preview [#lightbox-preview]

`ImagePreview` acts as the trigger, while `ImageLightbox` renders portal primitives (`ImageLightboxOverlay`, `ImageLightboxPreview`) and `ImageLightboxClose` can be placed externally in overlay actions.

<DemoWithCode src="components/nexus-ui/examples/image/lightbox.tsx">
  <ImageLightboxExample />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use `Image` with OpenAI image generation by returning `base64` and `mediaType` from your API route, then passing them directly to `Image`.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat route
    </h3>

    ```ts title="app/api/chat/route.ts"
    import { openai } from "@ai-sdk/openai";
    import { generateImage } from "ai";

    export async function POST(req: Request) {
      const { prompt }: { prompt: string } = await req.json();

      const { image } = await generateImage({
        model: openai.image("dall-e-3"),
        prompt: prompt,
        size: "1024x1024",
      });

      return Response.json({
        base64: image.base64,
        uint8Array: image.uint8Array ? Array.from(image.uint8Array) : undefined,
        mediaType: image.mediaType,
      });
    }
    ```
  </Step>

  <Step>
    <h3>
      Call the route and render the generated image
    </h3>

    ```tsx
    "use client";

    import { useState } from "react";
    import { Image } from "@/components/nexus-ui/image";

    type GeneratedImage = {
      base64?: string;
      uint8Array?: number[];
      mediaType?: string;
    };

    export function ChatWithImages() {
      const [prompt, setPrompt] = useState("A futuristic city skyline at sunset");
      const [generatedImage, setGeneratedImage] = useState<GeneratedImage | null>(null);
      const binaryImage = generatedImage?.uint8Array
        ? new Uint8Array(generatedImage.uint8Array)
        : undefined;

      async function onGenerate() {
        const response = await fetch("/api/chat", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ prompt }),
        });
        const data = (await response.json()) as GeneratedImage;
        setGeneratedImage(data);
      }

      return (
        <div className="space-y-3">
          <input
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            className="w-full rounded-md border px-3 py-2"
          />
          <button className="rounded-md border px-3 py-2" onClick={onGenerate}>
            Generate
          </button>

          {generatedImage?.base64 || binaryImage ? (
            <Image
              base64={generatedImage.base64}
              uint8Array={binaryImage}
              mediaType={generatedImage.mediaType ?? "image/png"}
              alt="Generated image"
              className="h-64 w-full"
            />
          ) : null}
        </div>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Image [#image]

Root container that resolves image source from `base64` or `uint8Array`, manages shared context, and wraps [Dialog Root](https://www.radix-ui.com/primitives/docs/components/dialog#root) for lightbox state.

<TypeTable
  type={{
  src: {
    type: "string",
    description:
      "Standard image URL source (e.g. external or local path). Used when `base64` and `uint8Array` are not provided.",
  },
  base64: {
    type: "string",
    description:
      "Raw base64 payload or a full data URL (`data:image/...;base64,...`).",
  },
  uint8Array: {
    type: "Uint8Array",
    description: "Binary image payload used when `base64` is not provided.",
  },
  mediaType: {
    type: "string",
    default: '"image/png"',
    description:
      "Preferred MIME type when creating the data URL/blob. Ignored when `base64` is a full `data:` URL.",
  },
  open: {
    type: "boolean",
    description: "Controlled open state for the internal dialog root.",
  },
  defaultOpen: {
    type: "boolean",
    description: "Initial uncontrolled open state for the internal dialog root.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description: "Callback fired when dialog open state changes.",
  },
  modal: {
    type: "boolean",
    default: "true",
    description: "Whether the dialog should behave as a modal.",
  },
  alt: {
    type: "string",
    default: '""',
    description: "Default alt text used by `ImagePreview`.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the image root container.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Optional custom composition. When omitted, `<ImagePreview />` is rendered.",
  },
}}
/>

ImagePreview [#imagepreview]

Renders the `<img>` element using source/alt from `Image` context and wraps [Dialog Trigger](https://www.radix-ui.com/primitives/docs/components/dialog#trigger). If there is no resolved source, it renders the loader placeholder.

<TypeTable
  type={{
  src: {
    type: "string",
    description:
      "Optional `img` source override for this preview instance. Falls back to `Image` root source when omitted.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the preview image/wrapper.",
  },
  alt: {
    type: "string",
    description:
      "Optional alt override. Falls back to `Image` root `alt` when omitted.",
  },
  onLoad: {
    type: "React.ReactEventHandler<HTMLImageElement>",
    description: "Called when the image successfully loads.",
  },
  onError: {
    type: "React.ReactEventHandler<HTMLImageElement>",
    description: "Called when image loading fails.",
  },
}}
/>

Also extends standard `img` props.

ImageLightbox [#imagelightbox]

Portal root for lightbox content. Wraps [Dialog Portal](https://www.radix-ui.com/primitives/docs/components/dialog#portal).

<TypeTable
  type={{
  container: {
    type: "HTMLElement",
    description: "Optional custom portal container element.",
  },
  forceMount: {
    type: "boolean",
    default: "false",
    description: "Forces portal subtree to stay mounted.",
  },
}}
/>

ImageLightboxOverlay [#imagelightboxoverlay]

Backdrop layer for the lightbox. Wraps [Dialog Overlay](https://www.radix-ui.com/primitives/docs/components/dialog#overlay).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes for the overlay.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLDivElement>",
    description: "Optional click handler for the overlay element.",
  },
}}
/>

ImageLightboxPreview [#imagelightboxpreview]

Centered lightbox content surface that renders the image and optional children. Wraps [Dialog Content](https://www.radix-ui.com/primitives/docs/components/dialog#content).

<TypeTable
  type={{
  src: {
    type: "string",
    description:
      "Optional source override for the lightbox image. Falls back to `Image` context source.",
  },
  alt: {
    type: "string",
    description:
      "Optional alt override for the lightbox image. Falls back to `Image` context alt.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes for the content surface.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Optional overlay content rendered inside the preview content (e.g. actions).",
  },
  onInteractOutside: {
    type: "(event) => void",
    description:
      "Called when interacting outside content. Built-in handling keeps `ImageActions` interactions from dismissing.",
  },
}}
/>

ImageLightboxClose [#imagelightboxclose]

Close control primitive for the lightbox. Wraps [Dialog Close](https://www.radix-ui.com/primitives/docs/components/dialog#close).

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "When true, composes close behavior onto the child element instead of rendering a button.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the close trigger.",
  },
}}
/>

ImageLoader [#imageloader]

Pulsing loader surface used by `ImagePreview` (both as background while image paints and as placeholder when no source exists).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes on the loader element.",
  },
}}
/>

ImageActions [#imageactions]

Absolute-positioned actions container for overlay controls.

<TypeTable
  type={{
  align: {
    type: '"inline-start" | "inline-end" | "block-start" | "block-end"',
    default: '"block-end"',
    description: "Placement of the action bar around the image frame.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the actions container.",
  },
}}
/>

ImageActionGroup [#imageactiongroup]

Horizontal grouping wrapper for related overlay actions.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes on the group.",
  },
}}
/>

ImageAction [#imageaction]

Wrapper for one action item. Supports polymorphism via `asChild` and optional built-in tooltip rendering.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "When true, merges props onto the child element instead of rendering a div.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes on the action wrapper.",
  },
  tooltip: {
    type: 'string | { content?: string; side?: "top" | "right" | "bottom" | "left"; shortcut?: string }',
    description:
      "Tooltip config. String form maps to tooltip content. Object form supports content, side, and keyboard shortcut. If content is omitted, no tooltip is rendered.",
  },
}}
/>


# Message



import MessageDefault from "@/components/nexus-ui/examples/message/default";
import MessageWithActions from "@/components/nexus-ui/examples/message/with-actions";
import MessageWithAvatar from "@/components/nexus-ui/examples/message/with-avatar";
import MessageWithAttachments from "@/components/nexus-ui/examples/message/with-attachments";
import MessageRichText from "@/components/nexus-ui/examples/message/rich-text";
import { Callout } from "@/components/callout";

Composable **user** and **assistant** chat turns via **`Message`**, **`MessageContent`**, **`MessageMarkdown`** ([Streamdown](https://github.com/vercel/streamdown)), **`MessageAvatar`**, **`MessageActions`**, and **[Attachments](/docs/components/attachments)**.

<DemoWithCode src="components/nexus-ui/examples/message/default.tsx">
  <MessageDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/message
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/message
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/message
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/message
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add button avatar tooltip kbd && npm install streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaid radix-ui @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons hast
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add button avatar tooltip kbd && pnpm add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaid radix-ui @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons hast
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add button avatar tooltip kbd && yarn add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaid radix-ui @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons hast
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add button avatar tooltip kbd && bun add streamdown @streamdown/cjk @streamdown/code @streamdown/math @streamdown/mermaid radix-ui @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons hast
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/message.tsx" title="components/nexus-ui/message.tsx" />

        <ComponentSource src="components/nexus-ui/codeblock.tsx" title="components/nexus-ui/codeblock.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Message,
  MessageStack,
  MessageContent,
  MessageMarkdown,
  MessageActions,
  MessageActionGroup,
  MessageAction,
  MessageAvatar,
} from "@/components/nexus-ui/message";
```

```tsx keepBackground
<Message from="user">
  <MessageStack>
    <MessageContent>
      <MessageMarkdown>Hello</MessageMarkdown>
    </MessageContent>
  </MessageStack>
  <MessageAvatar src="/avatar.png" alt="You" fallback="Y" />
</Message>
```

Examples [#examples]

With Actions [#with-actions]

Use **`MessageActions`** and **`MessageActionGroup`** for a row of controls. **`MessageAction`** supports built-in tooltips via `tooltip` as either a string or object (`content`, optional `side`, optional `shortcut`).

<DemoWithCode src="components/nexus-ui/examples/message/with-actions.tsx">
  <MessageWithActions />
</DemoWithCode>

With Avatar [#with-avatar]

**`MessageAvatar`** composes shadcn **Avatar** / **AvatarImage** / **AvatarFallback** — pass **`src`**, **`alt`**, and optional **`fallback`** (and **`delayMs`** if needed). Place the avatar **after** **`MessageStack`** for **`from="user"`**, and **before** **`MessageStack`** for **`from="assistant"`**.

<DemoWithCode src="components/nexus-ui/examples/message/with-avatar.tsx">
  <MessageWithAvatar />
</DemoWithCode>

With Attachments [#with-attachments]

Render **`AttachmentList`** / **`Attachment`** inside **`MessageStack`** above **`MessageContent`**. Use **`readOnly`** on **`Attachment`** when showing files that were already sent (no remove control or progress). See **[Attachments](/docs/components/attachments)** for **`AttachmentMeta`** and variants.

<DemoWithCode src="components/nexus-ui/examples/message/with-attachments.tsx">
  <MessageWithAttachments />
</DemoWithCode>

Rich Text (Markdown) [#rich-text-markdown]

**`MessageMarkdown`** renders markdown with **Streamdown** and shared prose-style classes. Pass a string as **`children`** (or a template literal) for headings, lists, code blocks, and links.

<DemoWithCode previewClassName="h-[600px] justify-start overflow-y-auto" src="components/nexus-ui/examples/message/rich-text.tsx">
  <MessageRichText />
</DemoWithCode>

Message and Streamdown [#message-and-streamdown]

How MessageMarkdown uses Streamdown [#how-messagemarkdown-uses-streamdown]

It is a thin wrapper around **[Streamdown](https://github.com/vercel/streamdown)** with the same component props, so you can pass **`children`**, **`className`**, and other **`Streamdown`** options as needed.

Nexus applies shared **prose-style** **`className`** tokens for headings, body text, links, lists, and inline code. **Shiki** highlights fenced code with **`github-light`** and **`github-dark`**.

**Plugins** from **`@streamdown/code`**, **`@streamdown/math`**, **`@streamdown/mermaid`**, and **`@streamdown/cjk`** enable code blocks, math, diagrams, and CJK-friendly typography. A few **MDX components** are overridden—especially **tables** and **codeblocks**.

Streamdown parses markdown into HTML that relies on utility classes shipped inside **`streamdown`** and those plugin packages. That model works well for **streaming** assistant output so you do not hand-build the markup.

Fenced Code [#fenced-code]

**`MessageMarkdown`** uses **`CodeBlock`** for **`components.code`** (`@/components/nexus-ui/codeblock`). **`@streamdown/code`** still handles highlighting—you are only swapping the **UI**.

Use **`CodeBlock`** **`showTitleRow`** to show or hide the fenced-block title row (pass it on your **`components.code`** renderer). Turn line gutters on or off with Streamdown **`lineNumbers`**, which **`MessageMarkdown`** forwards like any other Streamdown prop.

**`@nexus-ui/message`** ships **`message.tsx`** and **`codeblock.tsx`** in one install—there is no separate codeblock package.

Tailwind @source after install [#tailwind-source-after-install]

Installing **Message** with the **shadcn CLI** should merge **Tailwind `@source`** lines into the CSS file from **`components.json`** so those classes are **not purged**. With a **manual** install, that merge does not run by default.

<Callout type="warning">
  After either path, **open your global CSS** and **confirm** the **`@source`** entries for **`streamdown`** and **`@streamdown/*`** are present, and that **`../node_modules`** still resolves to the project root from that file’s folder.

  If `@source` entries for `streamdown` and `@streamdown/*` are missing, markdown may render unstyled and code or diagram blocks can break.
</Callout>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Render [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) messages with **Message** by reading each **[`UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message)** **`parts`** array. Join **`text`** parts for **MessageMarkdown** (streaming updates apply as the SDK appends or grows **`TextUIPart`** content).

See [Prompt Input](/docs/components/prompt-input#vercel-ai-sdk-integration) for a minimal **`POST /api/chat`** route with **`streamText`** and **`toUIMessageStreamResponse`**. For user turns that include uploads, map **`file`** parts to **[Attachments](/docs/components/attachments)** (or your own preview) in addition to text—**[Attachments](/docs/components/attachments#vercel-ai-sdk-integration)** covers **`sendMessage`** with **`files`**.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    Use the same handler as in the Prompt Input docs: **`messages: await convertToModelMessages(messages)`** and **`return result.toUIMessageStreamResponse()`**.
  </Step>

  <Step>
    <h3>
      Map 

      `messages`

       to Message
    </h3>

    Use **`isTextUIPart`** from **`ai`** so you only aggregate **`type: "text"`** segments. Skip **`system`** turns unless you surface them deliberately. Assistant messages can also include **reasoning**, **tool**, **source**, and other part types—extend this loop when you need those in the UI.

    ```tsx
    "use client";

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
    import {
      Message,
      MessageStack,
      MessageContent,
      MessageMarkdown,
    } from "@/components/nexus-ui/message";

    function textFromMessage(message: UIMessage) {
      return message.parts.filter(isTextUIPart).map((p) => p.text).join("");
    }

    export default function ChatThread() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      return (
        <div className="flex flex-col gap-4">
          {messages
            .filter((m) => m.role !== "system")
            .map((m) => (
              <Message key={m.id} from={m.role === "user" ? "user" : "assistant"}>
                <MessageStack>
                  <MessageContent>
                    <MessageMarkdown>{textFromMessage(m)}</MessageMarkdown>
                  </MessageContent>
                </MessageStack>
              </Message>
            ))}
        </div>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Message [#message]

Root of one chat turn: row for stack, avatar, and siblings; **`from`** sets alignment and is provided in context to **`MessageStack`**, **`MessageContent`**, and **`MessageActions`**.

<TypeTable
  type={{
  from: {
    type: '"user" | "assistant"',
    description:
      "User vs assistant; alignment on Message and nested layout/styles via context.",
  },
  "aria-label": {
    type: "string",
    description:
      "Accessible name for the turn. Defaults to User message / Assistant message unless aria-labelledby is set.",
  },
  "aria-labelledby": {
    type: "string",
    description:
      "Optional id(s) of visible labels; when set, a default aria-label is not applied.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the message row.",
  },
}}
/>

MessageStack [#messagestack]

Stacks bubble, attachments, and actions in a column; cross-axis alignment follows **`from`** on **`Message`** (user vs assistant).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the stack container.",
  },
}}
/>

MessageContent [#messagecontent]

Wraps the message body (e.g. markdown inside). **User** turns get a filled bubble; **assistant** turns stay visually light on the thread—both follow **`from`** on **`Message`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the bubble.",
  },
}}
/>

MessageMarkdown [#messagemarkdown]

Renders **markdown** with **[Streamdown](https://github.com/vercel/streamdown)**. Props are forwarded to **`Streamdown`**; values you pass replace the same keys on the underlying component (e.g. a new **`plugins`** object replaces the default bundle).

Commonly used options:

<TypeTable
  type={{
  children: {
    type: "string",
    description: "Markdown source to render.",
  },
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the Streamdown root element.",
  },
  mode: {
    type: '"streaming" | "static"',
    default: '"streaming"',
    description:
      "`streaming` for live token flow; `static` for finished content.",
  },
  isAnimating: {
    type: "boolean",
    default: "false",
    description:
      "Whether content is actively streaming (e.g. disables some controls).",
  },
  parseIncompleteMarkdown: {
    type: "boolean",
    default: "true",
    description:
      "Use the remend preprocessor for unfinished markdown while streaming.",
  },
  plugins: {
    type: "PluginConfig",
    description:
      "Math, Mermaid, Shiki, and CJK plugins. Replaces MessageMarkdown defaults when set.",
  },
  controls: {
    type: "ControlsConfig | boolean",
    description:
      "Show or hide table/code/Mermaid control buttons. Replaces MessageMarkdown defaults when set.",
  },
  components: {
    type: "Components",
    description:
      "Map markdown nodes to custom React elements. Replaces MessageMarkdown defaults when set.",
  },
  shikiTheme: {
    type: "[ThemeInput, ThemeInput]",
    default: "['github-light', 'github-dark']",
    description:
      "Light and dark syntax-highlighting themes for fenced code blocks.",
  },
}}
/>

For **`remend`**, **`remarkPlugins`**, **`rehypePlugins`**, **`linkSafety`**, **`animated`**, **`caret`**, and the full prop list, see the [Streamdown configuration docs](https://streamdown.ai/docs/configuration).

MessageActions [#messageactions]

A flex container for action buttons. Default **`justify-end`** (user) or **`justify-start`** (assistant); override with **`className`** (e.g. **`justify-between`**) for multiple groups.

<TypeTable
  type={{
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the actions container.",
  },
}}
/>

MessageActionGroup [#messageactiongroup]

Groups related action buttons together with a horizontal layout.

<TypeTable
  type={{
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the group container.",
  },
}}
/>

MessageAction [#messageaction]

A wrapper for individual action buttons. Supports polymorphism via `asChild` and optional built-in tooltip rendering.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description: "Render as the child element instead of a div.",
  },
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the action.",
  },
  tooltip: {
    type: 'string | { content?: string; side?: "top" | "right" | "bottom" | "left"; shortcut?: string }',
    description:
      "Tooltip config. String form maps to tooltip content. Object form supports content, side, and keyboard shortcut. If content is omitted, no tooltip is rendered.",
  },
}}
/>

MessageAvatar [#messageavatar]

**shadcn** [**Avatar**](https://ui.shadcn.com/docs/components/avatar) with **`src`** / **`alt`** / optional **`fallback`**. Sibling to **`MessageStack`** in **`Message`**: after the stack for **user**, before for **assistant**.

<TypeTable
  type={{
  src: {
    type: "string",
    description: "Image URL passed to AvatarImage.",
  },
  alt: {
    type: "string",
    default: '""',
    description: "AvatarImage alt text.",
  },
  fallback: {
    type: "React.ReactNode",
    description:
      "Optional. Shown inside AvatarFallback while loading or if the image errors.",
  },
  delayMs: {
    type: "number",
    description:
      "Optional. Passed to AvatarFallback (Radix delay before showing fallback).",
  },
  size: {
    type: '"default" | "sm" | "lg"',
    description: "Optional. shadcn Avatar size.",
  },
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the Avatar root.",
  },
}}
/>


# Model Selector



import ModelSelectorDefault from "@/components/nexus-ui/examples/model-selector/default";
import ModelSelectorBasic from "@/components/nexus-ui/examples/model-selector/basic";
import ModelSelectorWithSub from "@/components/nexus-ui/examples/model-selector/with-sub";
import ModelSelectorWithItems from "@/components/nexus-ui/examples/model-selector/with-items";
import ModelSelectorWithCheckbox from "@/components/nexus-ui/examples/model-selector/with-checkbox";
import ModelSelectorTriggerVariants from "@/components/nexus-ui/examples/model-selector/trigger-variants";
import ModelSelectorWithPromptInput from "@/components/nexus-ui/examples/model-selector/with-prompt-input";
import ModelSelectorCustomTrigger from "@/components/nexus-ui/examples/model-selector/custom-trigger";
import ModelSelectorWithSearch from "@/components/nexus-ui/examples/model-selector/with-search";

A composable dropdown for selecting AI models. Built on [Radix UI Dropdown Menu](https://www.radix-ui.com/primitives/docs/components/dropdown-menu),
it supports radio groups for single selection, sub-menus for nested options, plain items for toggles or actions, and separators for grouping.
The trigger displays the selected model's icon and title when you pass an `items` array.

<DemoWithCode src="components/nexus-ui/examples/model-selector/default.tsx">
  <ModelSelectorDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/model-selector
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/model-selector
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/model-selector
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/model-selector
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npm install radix-ui class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm add radix-ui class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn add radix-ui class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bun add radix-ui class-variance-authority @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/model-selector.tsx" title="components/nexus-ui/model-selector.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  ModelSelector,
  ModelSelectorTrigger,
  ModelSelectorContent,
  ModelSelectorGroup,
  ModelSelectorLabel,
  ModelSelectorRadioGroup,
  ModelSelectorRadioItem,
} from "@/components/nexus-ui/model-selector";
```

```tsx keepBackground noCollapse
<ModelSelector value={model} onValueChange={setModel} items={models}>
  <ModelSelectorTrigger />
  <ModelSelectorContent>
    <ModelSelectorGroup>
      <ModelSelectorLabel>Select model</ModelSelectorLabel>
      <ModelSelectorRadioGroup value={model} onValueChange={setModel}>
        {models.map((m) => (
          <ModelSelectorRadioItem
            key={m.value}
            value={m.value}
            title={m.title}
          />
        ))}
      </ModelSelectorRadioGroup>
    </ModelSelectorGroup>
  </ModelSelectorContent>
</ModelSelector>
```

Examples [#examples]

Basic [#basic]

A minimal model selector with radio items. No icons, descriptions, or labels.

<DemoWithCode src="components/nexus-ui/examples/model-selector/basic.tsx">
  <ModelSelectorBasic />
</DemoWithCode>

Trigger Variants [#trigger-variants]

The trigger supports three variants: `filled` (default), `outline`, and `ghost`. Use `outline` or `ghost` when placing
the model selector inside a [Prompt Input](/docs/components/prompt-input) or other dense UI.

<DemoWithCode src="components/nexus-ui/examples/model-selector/trigger-variants.tsx">
  <ModelSelectorTriggerVariants />
</DemoWithCode>

Custom Trigger [#custom-trigger]

Pass custom `children` to `ModelSelectorTrigger` to override the default icon-and-title layout.
You control the content entirely—use your own copy, icons, and layout.

<DemoWithCode src="components/nexus-ui/examples/model-selector/custom-trigger.tsx">
  <ModelSelectorCustomTrigger />
</DemoWithCode>

With Sub-menus [#with-sub-menus]

Use `ModelSelectorSub`, `ModelSelectorSubTrigger`, `ModelSelectorPortal`, and `ModelSelectorSubContent` for nested
options like provider-specific model groups.

<DemoWithCode src="components/nexus-ui/examples/model-selector/with-sub.tsx">
  <ModelSelectorWithSub />
</DemoWithCode>

With Checkbox [#with-checkbox]

Use `ModelSelectorCheckboxItem` alone for multi-select. Each item is a checkbox—toggle models on or off. Use a custom
trigger to show the selection (e.g. "Select models", "GPT-4", or "3 models").

<DemoWithCode src="components/nexus-ui/examples/model-selector/with-checkbox.tsx">
  <ModelSelectorWithCheckbox />
</DemoWithCode>

With Prompt Input [#with-prompt-input]

Place the model selector in a `PromptInputActionGroup` alongside attach, image, mic, and send buttons.

<DemoWithCode src="components/nexus-ui/examples/model-selector/with-prompt-input.tsx">
  <ModelSelectorWithPromptInput />
</DemoWithCode>

With Search [#with-search]

Place `ModelSelectorSearch` first in `ModelSelectorContent`; the root owns the query and clears it when the menu closes (`onOpenChange` still runs if you pass it). Radio items filter on `value`, `title`, and `description`; checkbox items on `title` and `description` only; plain `ModelSelectorItem` rows are not filtered. Optional search `onChange` runs after internal updates. `ModelSelectorEmpty` needs root `items` to show no-match copy (default: No models found).

<DemoWithCode src="components/nexus-ui/examples/model-selector/with-search.tsx">
  <ModelSelectorWithSearch />
</DemoWithCode>

With Items and Separators [#with-items-and-separators]

Use `ModelSelectorItem` for non-selection actions (e.g. extended thinking toggle) and `ModelSelectorSeparator`
to divide sections.

<DemoWithCode src="components/nexus-ui/examples/model-selector/with-items.tsx">
  <ModelSelectorWithItems />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use the Model Selector with the [Vercel AI SDK](https://sdk.vercel.ai) to let users pick which model powers the chat. Pass the selected model to your API via `prepareSendMessagesRequest`.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    Create a route that reads `model` from the request body:

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, UIMessage } from "ai";
    import { openai } from "@ai-sdk/openai";

    export async function POST(req: Request) {
      const { messages, model = "gpt-4o-mini" }: { messages: UIMessage[]; model?: string } =
        await req.json();

      const result = streamText({
        model: openai(model),
        system: "You are a helpful assistant.",
        messages: await convertToModelMessages(messages),
      });

      return result.toUIMessageStreamResponse();
    }
    ```
  </Step>

  <Step>
    <h3>
      Wire Model Selector + Prompt Input to 

      `useChat`
    </h3>

    ```tsx
    "use client";

    import { useState } from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport } from "ai";
    import { Button } from "@/components/ui/button";
    import {
      PromptInput,
      PromptInputActions,
      PromptInputAction,
      PromptInputActionGroup,
      PromptInputTextarea,
    } from "@/components/nexus-ui/prompt-input";
    import {
      ModelSelector,
      ModelSelectorContent,
      ModelSelectorGroup,
      ModelSelectorLabel,
      ModelSelectorRadioGroup,
      ModelSelectorRadioItem,
      ModelSelectorTrigger,
    } from "@/components/nexus-ui/model-selector";
    import ChatgptIcon from "@/components/svgs/chatgpt";
    import {
      ArrowUp02Icon,
      PlusSignIcon,
      SquareIcon,
    } from "@hugeicons/core-free-icons";
    import { HugeiconsIcon } from "@hugeicons/react";

    const models = [
      { value: "gpt-4", icon: ChatgptIcon, title: "GPT-4", description: "Most capable" },
      { value: "gpt-4o-mini", icon: ChatgptIcon, title: "GPT-4o Mini", description: "Fast" },
      { value: "gpt-4o", icon: ChatgptIcon, title: "GPT-4o", description: "Multimodal" },
    ];

    export default function ChatWithModelSelector() {
      const [model, setModel] = useState("gpt-4o-mini");
      const { sendMessage, status } = useChat({
        transport: new DefaultChatTransport({
          api: "/api/chat",
          prepareSendMessagesRequest: ({ body }) => ({
            body: { ...body, model },
          }),
        }),
      });
      const [input, setInput] = useState("");
      const isLoading = status !== "ready";

      const handleSubmit = (value?: string) => {
        const text = (value ?? input).trim();
        if (text) {
          sendMessage({ text });
          setInput("");
        }
      };

      return (
        <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }} className="w-full">
          <PromptInput onSubmit={handleSubmit}>
            <PromptInputTextarea
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Ask anything..."
              disabled={isLoading}
            />
            <PromptInputActions>
              <PromptInputActionGroup>
                <ModelSelector value={model} onValueChange={setModel} items={models}>
                  <ModelSelectorTrigger variant="outline" />
                  <ModelSelectorContent className="w-[264px]" align="start">
                    <ModelSelectorGroup>
                      <ModelSelectorLabel>Select model</ModelSelectorLabel>
                      <ModelSelectorRadioGroup value={model} onValueChange={setModel}>
                        {models.map((m) => (
                          <ModelSelectorRadioItem
                            key={m.value}
                            value={m.value}
                            icon={m.icon}
                            title={m.title}
                            description={m.description}
                          />
                        ))}
                      </ModelSelectorRadioGroup>
                    </ModelSelectorGroup>
                  </ModelSelectorContent>
                </ModelSelector>
                <PromptInputAction asChild>
                  <Button
                    type="button"
                    variant="ghost"
                    size="icon-sm"
                    className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
                  >
                    <HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
                  </Button>
                </PromptInputAction>
              </PromptInputActionGroup>
              <PromptInputActionGroup>
                <PromptInputAction asChild>
                  <Button
                    type="submit"
                    size="icon-sm"
                    className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
                    disabled={isLoading || !input.trim()}
                  >
                    {isLoading ? (
                      <HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
                    ) : (
                      <HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
                    )}
                  </Button>
                </PromptInputAction>
              </PromptInputActionGroup>
            </PromptInputActions>
          </PromptInput>
        </form>
      );
    }
    ```
  </Step>
</Steps>

For multi-provider support (OpenAI, Anthropic, etc.), add `@ai-sdk/anthropic` and similar, then map the model value to the correct provider in your API route.

API Reference [#api-reference]

The Model Selector primitives extend [Radix UI Dropdown Menu](https://www.radix-ui.com/primitives/docs/components/dropdown-menu).
For props like `open`, `onOpenChange`, `modal`, `dir`, etc., refer to the Radix docs. Below are the props specific to
the Model Selector.

ModelSelector [#modelselector]

Root component. Provides `value`, `onValueChange`, and `items` to children via context.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the root.",
  },
  value: {
    type: "string",
    description: "The currently selected model value.",
  },
  onValueChange: {
    type: "(value: string) => void",
    description: "Called when the selection changes.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description:
      "Forwarded from the Radix menu root. The root also clears the internal search query when the menu closes.",
  },
  items: {
    type: "Array<{ value: string; icon?: React.ComponentType<{ className?: string }>; title: string; description?: string }>",
    description:
      "Optional. Used by ModelSelectorTrigger to display the selected model's icon and title. Omit for custom trigger content.",
  },
}}
/>

ModelSelectorTrigger [#modelselectortrigger]

The button that opens the dropdown. Shows selected model (icon + title) when `items` is provided, or custom `children`.
Supports `asChild` to merge into a child element (e.g. Button).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the trigger.",
  },
  variant: {
    type: '"filled" | "outline" | "ghost"',
    default: '"filled"',
    description:
      "Visual style. Use outline or ghost when placing inside a prompt input or dense UI.",
  },
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "When true, merges props onto the child element instead of rendering a button.",
  },
}}
/>

ModelSelectorSearch [#modelselectorsearch]

A search input bound to the root filter query; radio and checkbox items hide when they do not match. Place at the top of `ModelSelectorContent`. Accepts standard `<input>` props.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes for the input element.",
  },
}}
/>

ModelSelectorEmpty [#modelselectorempty]

Shown when the query is non-empty, `items` is set on the root, and none of those entries match the filter. Otherwise hidden. Default copy: No models found. Accepts standard `<div>` props.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes for the message.",
  },
  children: {
    type: "React.ReactNode",
    description: "Optional. Replaces the default No models found copy.",
  },
}}
/>

ModelSelectorItem [#modelselectoritem]

A plain menu item (no selection state). Use for toggles, links, or actions.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the item.",
  },
  inset: {
    type: "boolean",
    default: "false",
    description:
      "Adds left padding for alignment with items that have icons.",
  },
  variant: {
    type: '"default" | "destructive"',
    default: '"default"',
    description:
      "Visual variant. Use destructive for delete or dangerous actions.",
  },
}}
/>

ModelSelectorItemTitle [#modelselectoritemtitle]

A paragraph for the item title. Used internally by CheckboxItem and RadioItem when `title` is provided. Accepts standard `p` element props.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the title.",
  },
}}
/>

ModelSelectorItemDescription [#modelselectoritemdescription]

A paragraph for the item description. Used internally by CheckboxItem and RadioItem when `description` is provided. Accepts standard `p` element props.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the description.",
  },
}}
/>

ModelSelectorItemIcon [#modelselectoritemicon]

A span wrapper for the item icon. Used internally by CheckboxItem and RadioItem when `icon` is provided. Accepts standard `span` element props.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the icon wrapper.",
  },
}}
/>

ModelSelectorItemIndicator [#modelselectoritemindicator]

A span that wraps selection indicators. Renders children inside Radix `ItemIndicator` by default. Use `wrapWithItemIndicator={false}` for always-visible content (e.g. LockIcon when disabled).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the indicator wrapper.",
  },
  wrapWithItemIndicator: {
    type: "boolean",
    default: "true",
    description:
      "When false, children render directly without ItemIndicator. Use for always-visible content like LockIcon when disabled.",
  },
}}
/>

ModelSelectorCheckboxItem [#modelselectorcheckboxitem]

A checkbox-style item for multi-select. Shows a check indicator when `checked` is true, or a lock icon when `disabled`.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the item.",
  },
  checked: {
    type: "boolean",
    description: "Whether the item is checked.",
  },
  disabled: {
    type: "boolean",
    default: "false",
    description: "When true, the item is disabled and shows a lock icon.",
  },
  icon: {
    type: "React.ComponentType<{ className?: string }>",
    description: "Optional icon component for the item.",
  },
  indicator: {
    type: "React.ReactNode",
    default: "CheckIcon",
    description:
      "Custom indicator to show when selected. Renders inside ItemIndicator.",
  },
  title: {
    type: "string",
    description: "Optional title. Override with children for full control.",
  },
  description: {
    type: "string",
    description: "Optional description shown below the title.",
  },
}}
/>

ModelSelectorRadioItem [#modelselectorradioitem]

A radio-style item for single selection. Shows a check when selected, or a lock icon when `disabled`.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the item.",
  },
  value: {
    type: "string",
    description:
      "The value for this option. Must be unique within the RadioGroup.",
  },
  disabled: {
    type: "boolean",
    default: "false",
    description: "When true, the item is disabled and shows a lock icon.",
  },
  icon: {
    type: "React.ComponentType<{ className?: string }>",
    description: "Optional icon component for the item.",
  },
  indicator: {
    type: "React.ReactNode",
    default: "CheckIcon",
    description:
      "Custom indicator to show when selected. Renders inside ItemIndicator.",
  },
  title: {
    type: "string",
    description: "Optional title. Override with children for full control.",
  },
  description: {
    type: "string",
    description: "Optional description shown below the title.",
  },
}}
/>

Other primitives [#other-primitives]

`ModelSelectorContent`, `ModelSelectorPortal`, `ModelSelectorGroup`, `ModelSelectorLabel`, `ModelSelectorRadioGroup`,
`ModelSelectorSeparator`, `ModelSelectorSub`, `ModelSelectorSubTrigger`, and `ModelSelectorSubContent` accept the
same props as their Radix Dropdown Menu counterparts. See the
[Radix Dropdown Menu documentation](https://www.radix-ui.com/primitives/docs/components/dropdown-menu) for details.

`ModelSelectorItemTitle`, `ModelSelectorItemDescription`, `ModelSelectorItemIcon`, and `ModelSelectorItemIndicator`
are low-level building blocks used by CheckboxItem and RadioItem. Use them when composing custom item content with `children`.


# Prompt Input



import PromptInputDefault from "@/components/nexus-ui/examples/prompt-input/default";
import PromptInputBasic from "@/components/nexus-ui/examples/prompt-input/basic";
import GeminiInput from "@/components/nexus-ui/examples/prompt-input/gemini-input";
import PromptInputMultipleActions from "@/components/nexus-ui/examples/prompt-input/multiple-actions";

A flexible, composable input component for building chat interfaces. Includes an auto-resizing textarea with scroll support and customizable action slots for buttons like send, attach, and more.

<DemoWithCode src="components/nexus-ui/examples/prompt-input/default.tsx">
  <PromptInputDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/prompt-input
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/prompt-input
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/prompt-input
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/prompt-input
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add textarea scroll-area tooltip kbd && npm install @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add textarea scroll-area tooltip kbd && pnpm add @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add textarea scroll-area tooltip kbd && yarn add @radix-ui/react-slot
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add textarea scroll-area tooltip kbd && bun add @radix-ui/react-slot
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/prompt-input.tsx" title="components/nexus-ui/prompt-input.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  PromptInput,
  PromptInputTextarea,
  PromptInputActions,
  PromptInputActionGroup,
  PromptInputAction,
} from "@/components/nexus-ui/prompt-input";
```

```tsx keepBackground
<PromptInput>
  <PromptInputTextarea placeholder="Ask anything..." />
  <PromptInputActions>
    <PromptInputActionGroup>
      {/* Left-aligned actions */}
    </PromptInputActionGroup>
    <PromptInputActionGroup>
      {/* Right-aligned actions */}
    </PromptInputActionGroup>
  </PromptInputActions>
</PromptInput>
```

Examples [#examples]

Basic [#basic]

A minimal prompt input with just a textarea and send button.

<DemoWithCode src="components/nexus-ui/examples/prompt-input/basic.tsx">
  <PromptInputBasic />
</DemoWithCode>

With Multiple Actions [#with-multiple-actions]

Combine multiple action buttons in a single group and add built-in tooltips using `PromptInputAction`'s `tooltip` prop (string or object with `content`, optional `side`, and optional `shortcut`).

<DemoWithCode src="components/nexus-ui/examples/prompt-input/multiple-actions.tsx">
  <PromptInputMultipleActions />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Connect Prompt Input to the [Vercel AI SDK](https://sdk.vercel.ai) for streaming chat interfaces.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, UIMessage } from "ai";
    import { openai } from "@ai-sdk/openai";

    export async function POST(req: Request) {
      const { messages }: { messages: UIMessage[] } = await req.json();

      const result = streamText({
        model: openai("gpt-4o-mini"),
        system: "You are a helpful assistant.",
        messages: await convertToModelMessages(messages),
      });

      return result.toUIMessageStreamResponse();
    }
    ```
  </Step>

  <Step>
    <h3>
      Wire Prompt Input to 

      `useChat`
    </h3>

    Use `onSubmit` for Enter-to-submit. Shift+Enter inserts a new line.

    ```tsx
    "use client";

    import { useState, useCallback } from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport } from "ai";
    import { Button } from "@/components/ui/button";
    import {
      PromptInput,
      PromptInputActions,
      PromptInputAction,
      PromptInputActionGroup,
      PromptInputTextarea,
    } from "@/components/nexus-ui/prompt-input";
    import { ArrowUp02Icon, SquareIcon } from "@hugeicons/core-free-icons";
    import { HugeiconsIcon } from "@hugeicons/react";

    export default function ChatWithPromptInput() {
      const { sendMessage, status } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });
      const [input, setInput] = useState("");
      const isLoading = status !== "ready";

      const handleSubmit = useCallback(
        (value?: string) => {
          const trimmed = (value ?? input).trim();
          if (trimmed) {
            sendMessage({ text: trimmed });
            setInput("");
          }
        },
        [input, sendMessage],
      );

      return (
        <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }} className="w-full">
          <PromptInput onSubmit={handleSubmit}>
            <PromptInputTextarea
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="Ask anything..."
              disabled={isLoading}
            />
            <PromptInputActions>
              <PromptInputActionGroup />
              <PromptInputActionGroup>
                <PromptInputAction asChild>
                  <Button
                    type="submit"
                    size="icon-sm"
                    className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
                    disabled={isLoading || !input.trim()}
                  >
                    {isLoading ? (
                      <HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
                    ) : (
                      <HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
                    )}
                  </Button>
                </PromptInputAction>
              </PromptInputActionGroup>
            </PromptInputActions>
          </PromptInput>
        </form>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

PromptInput [#promptinput]

The root container that wraps the textarea and action bar.

<TypeTable
  type={{
  onSubmit: {
    type: "(value: string) => void (optional)",
    description:
      "Called when Enter is pressed in the textarea (without Shift). Receives the current textarea value. Use with value/onChange on PromptInputTextarea for controlled mode. Shift+Enter inserts a new line.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the container.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLDivElement>",
    description:
      "Called after the internal click handler. The internal handler focuses the textarea when clicking non-interactive areas.",
  },
}}
/>

PromptInputTextarea [#promptinputtextarea]

An auto-resizing textarea wrapped in a scroll area. Accepts all standard `textarea` props including `disabled` and `onKeyDown`. Use `onSubmit` on PromptInput for Enter-to-submit; Shift+Enter inserts a new line.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the textarea.",
  },
  disabled: {
    type: "boolean",
    default: "false",
    description: "Disables the textarea (e.g. while the AI is responding).",
  },
  onChange: {
    type: "React.ChangeEventHandler<HTMLTextAreaElement>",
    description: "Called when the value changes. Use with value for controlled mode.",
  },
  onKeyDown: {
    type: "React.KeyboardEventHandler<HTMLTextAreaElement>",
    description: "Called when a key is pressed. Fired after the internal Enter/Shift+Enter handler.",
  },
  placeholder: {
    type: "string",
    default: '"How can I help you today?"',
    description: "Placeholder text displayed when the textarea is empty.",
  },
  ref: {
    type: "React.Ref<HTMLTextAreaElement>",
    description:
      "Forwarded ref to the textarea element. Merged with the internal ref used for click-to-focus.",
  },
  value: {
    type: "string",
    description: "Controlled value. Use with onChange for controlled mode.",
  },
}}
/>

PromptInputActions [#promptinputactions]

A flex container for action buttons. Uses `justify-between` to position child groups at opposite ends.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the actions container.",
  },
}}
/>

PromptInputActionGroup [#promptinputactiongroup]

Groups related action buttons together with a horizontal layout.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the group.",
  },
}}
/>

PromptInputAction [#promptinputaction]

A wrapper for individual action buttons. Supports polymorphism via `asChild` and optional built-in tooltip rendering.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "When true, merges props onto the child element instead of rendering a wrapper div. Uses Radix UI Slot.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the action.",
  },
  tooltip: {
    type: 'string | { content?: string; side?: "top" | "right" | "bottom" | "left"; shortcut?: string }',
    description:
      "Tooltip config. A string maps to tooltip content. Object form supports content, side, and keyboard shortcut. If content is not provided, no tooltip is rendered.",
  },
}}
/>


# Questions



import QuestionsDefault from "@/components/nexus-ui/examples/questions/default";
import QuestionsMultipleChoice from "@/components/nexus-ui/examples/questions/multiple-choice";
import QuestionsMultipleQuestions from "@/components/nexus-ui/examples/questions/multiple-questions";
import QuestionsRequired from "@/components/nexus-ui/examples/questions/required";
import QuestionsFixedOptions from "@/components/nexus-ui/examples/questions/fixed-options";
import { Callout } from "@/components/callout";

Composable card for **follow-up clarification questions** in chat. Pass **`items`** for metadata, compose with **`Question`**, **`QuestionOption`**, and **`QuestionOther`**, then handle **`onSubmit`**.

Single choice **auto-advances**; **Skip** passes optional questions. Unanswered optional questions submit as **`skipped`** with **`[No Preference]`**. **Submit** stays disabled until required questions are answered—in multi-question flows, keep it hidden or disabled until the last slide.

<DemoWithCode src="components/nexus-ui/examples/questions/default.tsx">
  <QuestionsDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/questions
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add carousel button card checkbox && npm install @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add carousel button card checkbox && pnpm add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add carousel button card checkbox && yarn add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add carousel button card checkbox && bun add @hugeicons/react @hugeicons/core-free-icons
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/questions.tsx" title="components/nexus-ui/questions.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noCollapse
import {
  Question,
  QuestionOption,
  QuestionOptions,
  QuestionOther,
  Questions,
  QuestionsFooter,
  QuestionsHeader,
  QuestionsSubmit,
  QuestionsTitle,
  type QuestionInput,
} from "@/components/nexus-ui/questions";
```

```tsx keepBackground noCollapse
const question: QuestionInput = {
  id: "depth",
  type: "single",
  prompt: "How detailed should the explanation be?",
  options: [
    { value: "brief", label: "Brief overview" },
    { value: "standard", label: "Standard depth" },
  ],
};

<Questions
  items={[question]}
  onSubmit={(submission) => console.log(submission)}
>
  <QuestionsHeader>
    <QuestionsTitle />
  </QuestionsHeader>

  <Question id={question.id}>
    <QuestionOptions>
      {question.options.map((option) => (
        <QuestionOption key={option.value} value={option.value}>
          {option.label}
        </QuestionOption>
      ))}
      <QuestionOther />
    </QuestionOptions>
  </Question>

  <QuestionsFooter>
    <QuestionsSubmit />
  </QuestionsFooter>
</Questions>
```

Pass **`items`** so **`QuestionsTitle`** and carousel pagination have question metadata on the first paint. For a single question, compose **`Question`** directly without **`QuestionsCarousel`**; use the carousel primitives when there are multiple slides.

Examples [#examples]

Multiple choice [#multiple-choice]

Set **`type: "multiple"`** in **`items`** to render checkbox rows. Users select one or more options before submitting—multiple choice does not auto-advance.

<DemoWithCode src="components/nexus-ui/examples/questions/multiple-choice.tsx">
  <QuestionsMultipleChoice />
</DemoWithCode>

Multiple questions [#multiple-questions]

For two or more prompts, wrap slides in **`QuestionsCarousel`**. Prev/next controls and pagination stay in sync with the active index; required questions block forward navigation until answered.

<DemoWithCode src="components/nexus-ui/examples/questions/multiple-questions.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <QuestionsMultipleQuestions />
</DemoWithCode>

Required questions [#required-questions]

Set **`required: true`** on every question in **`items`** to block forward navigation until each slide is answered. Omit **`QuestionsSkip`** when nothing is optional—skip only applies to unanswered optional questions. Omit **`QuestionsDismiss`** (and **`onDismiss`**) when you want a forced flow.

<DemoWithCode src="components/nexus-ui/examples/questions/required.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <QuestionsRequired />
</DemoWithCode>

Fixed options [#fixed-options]

Omit **`QuestionOther`** when the question should only accept predefined options.

<DemoWithCode src="components/nexus-ui/examples/questions/fixed-options.tsx">
  <QuestionsFixedOptions />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Use **Questions** when an agent needs structured clarification before continuing—for example a **`askClarifyingQuestions`** tool that streams arguments to the client and waits for the user's answers.

The usual pattern:

1. Define a tool **without** **`execute`** so the model stops after emitting question definitions.
2. Render **`Questions`** when the tool part reaches **`input-available`**.
3. Call **`addToolOutput`** (or **`addToolResult`**) in **`onSubmit`** with the submission payload.
4. Optionally use **`sendAutomaticallyWhen`** so the chat resumes after the tool output is added.

<Callout type="info">
  This flow is client-side tool handling. The model proposes questions; your UI collects answers and returns them as the tool result so the agent can continue.
</Callout>

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react zod
    ```
  </Step>

  <Step>
    <h3>
      Expose a clarification tool on your chat route
    </h3>

    Define a tool with a Zod schema that matches **`QuestionInput`**. Omit **`execute`** so the client must supply the result.

    ```ts title="app/api/chat/route.ts"
    import { convertToModelMessages, streamText, tool, type UIMessage } from "ai";
    import { z } from "zod";

    const questionSchema = z.object({
      id: z.string(),
      type: z.enum(["single", "multiple"]),
      prompt: z.string(),
      required: z.boolean().optional(),
      options: z.array(
        z.object({
          value: z.string(),
          label: z.string(),
        }),
      ),
    });

    export async function POST(req: Request) {
      const { messages }: { messages: UIMessage[] } = await req.json();

      const result = streamText({
        model: "anthropic/claude-sonnet-4.5",
        system:
          "When the user's request is ambiguous, call askClarifyingQuestions before answering.",
        messages: await convertToModelMessages(messages),
        tools: {
          askClarifyingQuestions: tool({
            description:
              "Ask the user one or more clarifying questions before continuing.",
            inputSchema: z.object({
              questions: z.array(questionSchema).min(1),
            }),
          }),
        },
      });

      return result.toUIMessageStreamResponse();
    }
    ```
  </Step>

  <Step>
    <h3>
      Render tool input as Questions and return answers with 

      <code className="tracking-0!">addToolOutput</code>
    </h3>

    Find the latest assistant message with an **`askClarifyingQuestions`** tool part in **`input-available`** state, map **`part.input.questions`** to **`items`**, and wire **`onSubmit`** to **`addToolOutput`**. Use **`sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls`** (or your own predicate) so the model continues after answers are submitted.

    ```tsx title="app/clarification-chat/page.tsx"
    "use client";

    import { useChat } from "@ai-sdk/react";
    import {
      DefaultChatTransport,
      lastAssistantMessageIsCompleteWithToolCalls,
      type UIMessage,
    } from "ai";
    import {
      Question,
      QuestionOption,
      QuestionOptions,
      QuestionOther,
      Questions,
      QuestionsCarousel,
      QuestionsCarouselContent,
      QuestionsCarouselIndex,
      QuestionsCarouselItem,
      QuestionsCarouselNext,
      QuestionsCarouselPagination,
      QuestionsCarouselPrev,
      QuestionsFooter,
      QuestionsHeader,
      QuestionsSkip,
      QuestionsSubmit,
      QuestionsTitle,
      type QuestionInput,
      type QuestionsSubmission,
    } from "@/components/nexus-ui/questions";

    type ClarifyingQuestionsInput = {
      questions: QuestionInput[];
    };

    type ClarifyingQuestionsToolPart = {
      type: "tool-askClarifyingQuestions";
      toolCallId: string;
      state: "input-available" | "output-available" | "output-error";
      input?: ClarifyingQuestionsInput;
    };

    function findPendingClarification(messages: UIMessage[]) {
      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      return assistant.parts.find(
        (part): part is ClarifyingQuestionsToolPart =>
          part.type === "tool-askClarifyingQuestions" &&
          part.state === "input-available" &&
          Boolean((part as ClarifyingQuestionsToolPart).input?.questions?.length),
      );
    }

    export default function ClarificationChatPage() {
      const { messages, addToolOutput, status } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
        sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
      });

      const pending = findPendingClarification(messages);
      if (!pending?.input?.questions) return null;

      const items = pending.input.questions;

      function handleSubmit(submission: QuestionsSubmission) {
        void addToolOutput({
          tool: "askClarifyingQuestions",
          toolCallId: pending.toolCallId,
          output: { answers: submission },
        });
      }

      return (
        <Questions items={items} onSubmit={handleSubmit}>
          <QuestionsCarousel>
            <QuestionsHeader>
              <QuestionsTitle />
              {items.length > 1 ? (
                <QuestionsCarouselPagination>
                  <QuestionsCarouselPrev />
                  <QuestionsCarouselIndex format="of" />
                  <QuestionsCarouselNext />
                </QuestionsCarouselPagination>
              ) : null}
            </QuestionsHeader>

            <QuestionsCarouselContent className="mx-0">
              {items.map((question) => (
                <QuestionsCarouselItem key={question.id}>
                  <Question id={question.id}>
                    <QuestionOptions>
                      {question.options.map((option) => (
                        <QuestionOption key={option.value} value={option.value}>
                          {option.label}
                        </QuestionOption>
                      ))}
                      <QuestionOther />
                    </QuestionOptions>
                  </Question>
                </QuestionsCarouselItem>
              ))}
            </QuestionsCarouselContent>
          </QuestionsCarousel>

          <QuestionsFooter>
            {items.length > 1 ? <QuestionsSkip /> : null}
            <QuestionsSubmit showOnLastQuestion disabled={status !== "ready"} />
          </QuestionsFooter>
        </Questions>
      );
    }
    ```
  </Step>

  <Step>
    <h3>
      Alternative: send answers as a user message
    </h3>

    If you are not using tools, format **`QuestionsSubmission`** and call **`sendMessage`** directly—useful for lightweight clarification UIs that do not participate in the tool loop.

    ```tsx
    import type { QuestionsSubmission } from "@/components/nexus-ui/questions";

    function formatAnswers(submission: QuestionsSubmission) {
      return submission
        .map((entry) => {
          if (entry.type === "single") {
            return `- ${entry.prompt}: ${entry.answer.label}`;
          }
          const labels = entry.answer.map((item) => item.label).join(", ");
          return `- ${entry.prompt}: ${labels}`;
        })
        .join("\n");
    }

    // onSubmit={(submission) => sendMessage({ text: formatAnswers(submission) })}
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Questions [#questions]

Root card and state container. Manages answer state, active index, carousel sync, skip/submit rules, and typed submission payloads. Extends shadcn **[Card](https://ui.shadcn.com/docs/components/radix/card)**.

<TypeTable
  type={{
  items: {
    type: "QuestionInput[]",
    description:
      "Required question metadata and options. Drives title, pagination, navigation, and submission labels. Map the same data into Question slides for options UI.",
  },
  autoAdvance: {
    type: "boolean",
    default: "true",
    description:
      "When true, single-choice option clicks advance to the next slide. Other input never auto-advances. No-op on the last question.",
  },
  onSubmit: {
    type: "(submission: QuestionsSubmission) => void",
    description:
      "Fired when Submit is clicked and every required question is answered. Skipped or unanswered optional questions use answer label \"[No Preference]\". Clears selections and resets to the first question.",
  },
  onSkip: {
    type: "(questionId: string) => void",
    description: "Called when Skip advances past the current question.",
  },
  onDismiss: {
    type: "() => void",
    description: "Called when QuestionsDismiss is clicked.",
  },
  className: {
    type: "string",
    description: "Merged with the default card shell styles.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Composition tree: header, carousel or question slides, and footer.",
  },
}}
/>

QuestionsHeader [#questionsheader]

Top bar layout shell for title, pagination, and dismiss controls. Extends shadcn **`CardHeader`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default header row layout.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically QuestionsTitle, QuestionsCarouselPagination, and QuestionsDismiss.",
  },
}}
/>

QuestionsTitle [#questionstitle]

Renders the active question prompt from **`items[root.index]`** in **`CardTitle`**, unless **`children`** override it. Extends shadcn **`CardTitle`**.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description:
      "Optional override. Defaults to the prompt from items at the active index.",
  },
  className: {
    type: "string",
    description: "Merged with default title styles.",
  },
}}
/>

QuestionsDismiss [#questionsdismiss]

Icon dismiss control. Invokes **`onDismiss`** on the root. Extends shadcn **`Button`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default dismiss button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before onDismiss runs.",
  },
}}
/>

QuestionsCarousel [#questionscarousel]

[**Carousel**](https://ui.shadcn.com/docs/components/radix/carousel) root for multi-question slides. Registers the Embla API on **`Questions`** for index sync and animated viewport height.

<TypeTable
  type={{
  setApi: {
    type: "(api: CarouselApi | undefined) => void",
    description:
      "Optional; invoked when the Embla instance is ready or cleared.",
  },
  className: {
    type: "string",
    description: "Classes on the carousel region wrapper.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionsHeader and QuestionsCarouselContent.",
  },
}}
/>

QuestionsCarouselPagination [#questionscarouselpagination]

Flex row for prev / index / next controls.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default pagination row layout.",
  },
  children: {
    type: "React.ReactNode",
    description:
      "Typically QuestionsCarouselPrev, QuestionsCarouselIndex, and QuestionsCarouselNext.",
  },
}}
/>

QuestionsCarouselPrev [#questionscarouselprev]

Previous-slide button. Disabled on the first question.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default arrow icon.",
  },
  className: {
    type: "string",
    description: "Merged with default circular nav button styles.",
  },
}}
/>

QuestionsCarouselIndex [#questionscarouselindex]

Active slide indicator (**1-based** current). Renders a **`span`**.

<TypeTable
  type={{
  format: {
    type: '"of" | "slash"',
    default: '"of"',
    description: 'Display as "1 of 3" or "1/3".',
  },
  className: {
    type: "string",
    description: "Merged with default tabular-nums text styles.",
  },
}}
/>

QuestionsCarouselNext [#questionscarouselnext]

Next-slide button. Disabled on the last question or when the current required question is unanswered.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description: "Replace the default arrow icon.",
  },
  className: {
    type: "string",
    description: "Merged with default circular nav button styles.",
  },
}}
/>

QuestionsCarouselContent [#questionscarouselcontent]

Carousel track wrapper. Animates viewport height to the active slide. Extends shadcn **`CarouselContent`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Classes on the flex track inside the overflow viewport.",
  },
  children: {
    type: "React.ReactNode",
    description: "QuestionsCarouselItem slides.",
  },
}}
/>

QuestionsCarouselItem [#questionscarouselitem]

One carousel slide. Wrap **`Question`** inside.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default slide layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically a Question with QuestionOptions.",
  },
}}
/>

Question [#question]

One question scope for option components. Wraps children in **`CardContent`**. **`id`** must match an entry in **`Questions`** **`items`**.

<TypeTable
  type={{
  id: {
    type: "string",
    description: "Stable question id; keys the answer in context.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionOptions with QuestionOption and optional QuestionOther.",
  },
}}
/>

QuestionOptions [#questionoptions]

Vertical list wrapper for **`QuestionOption`** and **`QuestionOther`**. Injects **`optionIndex`** into **`QuestionOption`** children for numbered single-choice badges.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with the default options list layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "QuestionOption and QuestionOther rows.",
  },
}}
/>

QuestionOption [#questionoption]

Selectable row. Single choice renders a numbered button; clicks advance to the next slide when **`Questions`** **`autoAdvance`** is enabled (default). Multiple choice renders a checkbox label.

<TypeTable
  type={{
  value: {
    type: "string",
    description: "Answer value stored in submission.",
  },
  optionIndex: {
    type: "number",
    description: "Optional; set automatically by QuestionOptions.",
  },
  children: {
    type: "React.ReactNode",
    description: "Option label shown in the row.",
  },
  className: {
    type: "string",
    description: "Merged with default row styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description:
      "Optional click handler for single choice. Called before selection.",
  },
}}
/>

QuestionOther [#questionother]

Free-text **Other** row. Compose only when free-text answers are allowed. Uses **`QUESTION_OTHER_VALUE`** (`"__other__"`) as the stored value when selected. Typing selects **Other** (single and multiple); multiple choice also pairs with a checkbox.

<TypeTable
  type={{
  placeholder: {
    type: "string",
    default: '"Other..."',
    description: "Input placeholder text.",
  },
  className: {
    type: "string",
    description: "Merged with default row styles.",
  },
  onKeyDown: {
    type: "React.KeyboardEventHandler<HTMLInputElement>",
    description: "Forwarded to the text input.",
  },
}}
/>

QuestionsFooter [#questionsfooter]

Bottom bar layout shell for skip and submit actions. Extends shadcn **`CardFooter`**.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Merged with default footer layout.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically QuestionsSkip and QuestionsSubmit.",
  },
}}
/>

QuestionsSkip [#questionsskip]

Ghost **Skip** button with a fixed **Skip** label. Disabled on the last question, when the current question is required and unanswered, or when there is no current question. Extends shadcn **`Button`**.

<TypeTable
  type={{
  disabled: {
    type: "boolean",
    description:
      "Optional override. Defaults to disabled when skip is not allowed.",
  },
  className: {
    type: "string",
    description: "Merged with default skip button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before skip runs.",
  },
}}
/>

QuestionsSubmit [#questionssubmit]

Primary **Submit** button. Disabled while any **required** question is unanswered; optional unanswered questions are allowed. Extends shadcn **`Button`**.

<TypeTable
  type={{
  showOnLastQuestion: {
    type: "boolean",
    default: "false",
    description:
      "When true, renders nothing until the active slide is the last question.",
  },
  disableUntilLastQuestion: {
    type: "boolean",
    default: "false",
    description:
      "When true, keeps submit visible but disabled until the active slide is the last question. Still respects required-question validation via canSubmit.",
  },
  disabled: {
    type: "boolean",
    description:
      "Optional override. Defaults to disabled while a required question is unanswered.",
  },
  className: {
    type: "string",
    description: "Merged with default submit button styles.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before submit runs.",
  },
  children: {
    type: "React.ReactNode",
    default: '"Submit"',
    description: "Button label.",
  },
}}
/>

Types [#types]

```ts noCollapse
export type QuestionOptionInput = {
  value: string;
  label: React.ReactNode;
};

export type QuestionInput = {
  id: string;
  type: "single" | "multiple";
  prompt: React.ReactNode;
  options: QuestionOptionInput[];
  required?: boolean;
};

export type QuestionSubmissionAnswer = {
  value: string;
  label: React.ReactNode;
};

export type QuestionsSubmission = Array<
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "single";
      status: "answered";
      answer: QuestionSubmissionAnswer;
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "single";
      status: "skipped";
      answer: QuestionSubmissionAnswer;
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "multiple";
      status: "answered";
      answer: QuestionSubmissionAnswer[];
    }
  | {
      questionId: string;
      prompt: React.ReactNode;
      type: "multiple";
      status: "skipped";
      answer: QuestionSubmissionAnswer[];
    }
>;

export const QUESTION_OTHER_VALUE = "__other__";
export const QUESTION_NO_PREFERENCE_VALUE = "__no_preference__";
export const QUESTION_NO_PREFERENCE_LABEL = "[No Preference]";
```


# Reasoning



import ReasoningDefault from "@/components/nexus-ui/examples/reasoning/default";

Collapsible reasoning UI for assistant "thinking" traces. The root tracks streaming state to update the trigger label (`Thinking...` -> `Thought for N seconds`) and auto-opens/closes content while reasoning streams.

<DemoWithCode src="components/nexus-ui/examples/reasoning/default.tsx">
  <ReasoningDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/reasoning
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/reasoning
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/reasoning
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/reasoning
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add collapsible && npm install @hugeicons/react @hugeicons/core-free-icons streamdown tw-shimmer
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add collapsible && pnpm add @hugeicons/react @hugeicons/core-free-icons streamdown tw-shimmer
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add collapsible && yarn add @hugeicons/react @hugeicons/core-free-icons streamdown tw-shimmer
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add collapsible && bun add @hugeicons/react @hugeicons/core-free-icons streamdown tw-shimmer
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/reasoning.tsx" title="components/nexus-ui/reasoning.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Reasoning,
  ReasoningTrigger,
  ReasoningContent,
} from "@/components/nexus-ui/reasoning";
```

```tsx keepBackground noCollapse
<Reasoning isStreaming={isReasoningStreaming}>
  <ReasoningTrigger />
  <ReasoningContent>{reasoningMarkdown}</ReasoningContent>
</Reasoning>
```

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Some models emit structured reasoning parts in the AI SDK response stream. In practice, models like **DeepSeek R1** and **Claude** can return reasoning content that you can render with `Reasoning`.

Use `Reasoning` with [Vercel AI SDK](https://sdk.vercel.ai) by mapping assistant `reasoning` parts to:

* `isStreaming`: true while reasoning parts are still streaming
* `ReasoningContent` children: merged reasoning text

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    This mirrors the docs demo route: use Claude via the AI Gateway id and enable Anthropic thinking so reasoning parts are returned.

    ```ts title="app/api/chat/route.ts"
    import { streamText, smoothStream, UIMessage, convertToModelMessages } from "ai";

    export async function POST(req: Request) {
      const {
        messages,
        model,
      }: { messages: UIMessage[]; model?: string } = await req.json();

      const result = streamText({
        model: "anthropic/claude-sonnet-4.5",
        messages: await convertToModelMessages(messages),
        experimental_transform: smoothStream({
          chunking: "word",
          delayInMs: 18,
        }),
        providerOptions: {
          anthropic: {
            thinking: { type: "enabled", budgetTokens: 1024 },
          },
        },
      });

      return result.toUIMessageStreamResponse({
        sendReasoning: true,
      });
    }
    ```
  </Step>

  <Step>
    <h3>
      Render assistant reasoning parts with 

      <code className="tracking-0!">Reasoning</code>
    </h3>

    ```tsx
    "use client";

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, isReasoningUIPart, type UIMessage } from "ai";
    import {
      Reasoning,
      ReasoningContent,
      ReasoningTrigger,
    } from "@/components/nexus-ui/reasoning";

    function reasoningPartsFromMessage(message: UIMessage) {
      return message.parts.filter(isReasoningUIPart);
    }

    function reasoningTextFromMessage(message: UIMessage) {
      return reasoningPartsFromMessage(message).map((p) => p.text).join("");
    }

    function reasoningStreamingFromMessage(message: UIMessage) {
      const parts = reasoningPartsFromMessage(message);
      return parts.some((p) => p.state === "streaming");
    }

    export default function ReasoningWithUseChat() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      const reasoningText = reasoningTextFromMessage(assistant);
      const isReasoningStreaming = reasoningStreamingFromMessage(assistant);

      if (!reasoningText.trim()) return null;

      return (
        <Reasoning isStreaming={isReasoningStreaming}>
          <ReasoningTrigger />
          <ReasoningContent>{reasoningText}</ReasoningContent>
        </Reasoning>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Reasoning [#reasoning]

Root component. Manages streaming timing, open state behavior, and label context for children. Wraps [Collapsible](https://www.radix-ui.com/primitives/docs/components/collapsible#root).

<TypeTable
  type={{
  isStreaming: {
    type: "boolean",
    default: "false",
    description:
      "Controls reasoning stream lifecycle. Drives timing, auto-open/close behavior, and trigger label updates.",
  },
  open: {
    type: "boolean",
    description:
      "Controlled open state for the collapsible content. Works with onOpenChange.",
  },
  defaultOpen: {
    type: "boolean",
    default: "false",
    description: "Initial open state in uncontrolled mode.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description:
      "Called when open state changes (manual toggle or internal stream-driven open/close).",
  },
  className: {
    type: "string",
    description: "Additional classes for the root collapsible wrapper.",
  },
}}
/>

ReasoningTrigger [#reasoningtrigger]

Trigger row with built-in brain icon, label text, and rotating chevron. If `children` is provided, it replaces the default label text. Wraps [Collapsible Trigger](https://www.radix-ui.com/primitives/docs/components/collapsible#trigger). The shimmer label effect is powered by `tw-shimmer`; see [tw-shimmer docs](https://www.assistant-ui.com/tw-shimmer) for customization options.

<TypeTable
  type={{
  children: {
    type: "React.ReactNode",
    description:
      "Optional custom trigger label. When omitted, uses context label derived from streaming state.",
  },
  className: {
    type: "string",
    description:
      "Additional classes merged with the default trigger styling.",
  },
}}
/>

ReasoningContent [#reasoningcontent]

Collapsible content wrapper with enter/exit animation and markdown rendering via [Streamdown](https://streamdown.ai/). Wraps [Collapsible Content](https://www.radix-ui.com/primitives/docs/components/collapsible#content).

<TypeTable
  type={{
  children: {
    type: "string",
    description:
      "Markdown text to render. This component expects a string and passes it to Streamdown.",
  },
  className: {
    type: "string",
    description:
      "Additional classes merged with the default content container styles.",
  },
}}
/>


# Suggestions



import SuggestionDefault from "@/components/nexus-ui/examples/suggestion/default";
import SuggestionVertical from "@/components/nexus-ui/examples/suggestion/vertical";
import SuggestionCustomValue from "@/components/nexus-ui/examples/suggestion/custom-value";
import SuggestionWithIcons from "@/components/nexus-ui/examples/suggestion/with-icons";
import SuggestionWithPromptInput from "@/components/nexus-ui/examples/suggestion/with-prompt-input";
import SuggestionVariants from "@/components/nexus-ui/examples/suggestion/variants";
import SuggestionWithPanel from "@/components/nexus-ui/examples/suggestion/with-panel";

Clickable prompt suggestion chips that guide users toward common queries. Built on shadcn's `Button` with a composable compound pattern.

<DemoWithCode src="components/nexus-ui/examples/suggestion/default.tsx">
  <SuggestionDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/suggestions
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/suggestions
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/suggestions
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/suggestions
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add button && npm install @radix-ui/react-presence @radix-ui/react-slot class-variance-authority
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add button && pnpm add @radix-ui/react-presence @radix-ui/react-slot class-variance-authority
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add button && yarn add @radix-ui/react-presence @radix-ui/react-slot class-variance-authority
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add button && bun add @radix-ui/react-presence @radix-ui/react-slot class-variance-authority
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/suggestions.tsx" title="components/nexus-ui/suggestions.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Suggestions,
  SuggestionList,
  Suggestion,
} from "@/components/nexus-ui/suggestions";
```

```tsx keepBackground
<Suggestions onSelect={(value) => handleSuggestion(value)}>
  <SuggestionList>
    <Suggestion>Tell me a joke</Suggestion>
    <Suggestion>Explain quantum computing</Suggestion>
  </SuggestionList>
</Suggestions>
```

Examples [#examples]

Variants [#variants]

The `Suggestion` component supports three variants: `filled` (filled background), `outline` (bordered), and `ghost` (transparent until hovered).

<DemoWithCode src="components/nexus-ui/examples/suggestion/variants.tsx">
  <SuggestionVariants />
</DemoWithCode>

Vertical Layout [#vertical-layout]

Use `orientation="vertical"` on `SuggestionList` to stack suggestions in a column.

<DemoWithCode src="components/nexus-ui/examples/suggestion/vertical.tsx">
  <SuggestionVertical />
</DemoWithCode>

With Custom Value [#with-custom-value]

Use the `value` prop when the display text differs from the value passed to `onSelect`.

<DemoWithCode src="components/nexus-ui/examples/suggestion/custom-value.tsx">
  <SuggestionCustomValue />
</DemoWithCode>

With Icons [#with-icons]

Since `Suggestion` renders a shadcn `Button`, you can add icons alongside text.

<DemoWithCode src="components/nexus-ui/examples/suggestion/with-icons.tsx">
  <SuggestionWithIcons />
</DemoWithCode>

With Prompt Input [#with-prompt-input]

Clicking a suggestion populates the `PromptInput` textarea, combining both components.

<DemoWithCode src="components/nexus-ui/examples/suggestion/with-prompt-input.tsx">
  <SuggestionWithPromptInput />
</DemoWithCode>

With Panel [#with-panel]

Category chips that open a panel with related suggestions. Uses the `highlight` prop to style matching terms.

<DemoWithCode src="components/nexus-ui/examples/suggestion/with-panel.tsx" previewClassName="h-[600px]">
  <SuggestionWithPanel />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Combine Suggestions with [Prompt Input](/docs/components/prompt-input) and the [Vercel AI SDK](https://sdk.vercel.ai) for a chat interface with quick-start prompts.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    See [Prompt Input docs](/docs/components/prompt-input#vercel-ai-sdk-integration) for the route implementation.
  </Step>

  <Step>
    <h3>
      Wire Suggestions + Prompt Input to 

      `useChat`
    </h3>

    ```tsx
    "use client";

    import { useState } from "react";
    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport } from "ai";
    import { Button } from "@/components/ui/button";
    import {
      PromptInput,
      PromptInputActions,
      PromptInputAction,
      PromptInputActionGroup,
      PromptInputTextarea,
    } from "@/components/nexus-ui/prompt-input";
    import {
      Suggestions,
      SuggestionList,
      Suggestion,
    } from "@/components/nexus-ui/suggestions";
    import {
      ArrowUp02Icon,
      PlusSignIcon,
      SquareIcon,
    } from "@hugeicons/core-free-icons";
    import { HugeiconsIcon } from "@hugeicons/react";

    export default function ChatWithSuggestions() {
      const { sendMessage, status } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });
      const [input, setInput] = useState("");
      const isLoading = status !== "ready";

      const handleSubmit = (e?: React.FormEvent) => {
        e?.preventDefault();
        if (input.trim()) {
          sendMessage({ text: input });
          setInput("");
        }
      };

      return (
        <div className="flex w-full flex-col gap-6">
          <form onSubmit={handleSubmit} className="w-full">
            <PromptInput onSubmit={handleSubmit}>
              <PromptInputTextarea
                value={input}
                onChange={(e) => setInput(e.target.value)}
                placeholder="Ask anything..."
                disabled={isLoading}
              />
              <PromptInputActions>
                <PromptInputActionGroup>
                  <PromptInputAction asChild>
                    <Button
                      type="button"
                      variant="ghost"
                      size="icon-sm"
                      className="cursor-pointer rounded-full text-secondary-foreground active:scale-97 disabled:opacity-70 hover:dark:bg-secondary"
                    >
                      <HugeiconsIcon icon={PlusSignIcon} strokeWidth={2.0} className="size-4" />
                    </Button>
                  </PromptInputAction>
                </PromptInputActionGroup>
                <PromptInputActionGroup>
                  <PromptInputAction asChild>
                    <Button
                      type="submit"
                      size="icon-sm"
                      className="cursor-pointer rounded-full active:scale-97 disabled:opacity-70"
                      disabled={isLoading || !input.trim()}
                    >
                      {isLoading ? (
                        <HugeiconsIcon icon={SquareIcon} strokeWidth={2.0} className="size-3.5 fill-current" />
                      ) : (
                        <HugeiconsIcon icon={ArrowUp02Icon} strokeWidth={2.0} className="size-4" />
                      )}
                    </Button>
                  </PromptInputAction>
                </PromptInputActionGroup>
              </PromptInputActions>
            </PromptInput>
          </form>

          <Suggestions onSelect={(value) => setInput(value)}>
            <SuggestionList className="justify-center">
              <Suggestion>What is AI?</Suggestion>
              <Suggestion>Teach me Engineering from scratch</Suggestion>
              <Suggestion>How to learn React?</Suggestion>
              <Suggestion>Design a weekly workout plan</Suggestion>
              <Suggestion>Places to visit in France</Suggestion>
            </SuggestionList>
          </Suggestions>
        </div>
      );
    }
    ```

    For one-click submission (suggestion sends immediately without editing), call `sendMessage` in `onSelect`:

    ```tsx
    <Suggestions
      onSelect={(value) => {
        if (value.trim()) {
          sendMessage({ text: value });
          setInput("");
        }
      }}
    >
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Suggestions [#suggestions]

The root container that provides `onSelect` context to all child `Suggestion` components. Extends `React.HTMLAttributes<HTMLDivElement>`.

<TypeTable
  type={{
  onSelect: {
    type: "(value: string) => void",
    description:
      "Callback fired when any Suggestion is clicked. Receives the suggestion's value or text content.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the root container.",
  },
}}
/>

SuggestionList [#suggestionlist]

Layout wrapper for arranging suggestions horizontally or vertically. Extends `React.HTMLAttributes<HTMLDivElement>`.

<TypeTable
  type={{
  orientation: {
    type: '"horizontal" | "vertical"',
    default: '"horizontal"',
    description:
      "Layout direction. Horizontal wraps items in a row, vertical stacks them in a column.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the list container.",
  },
}}
/>

Suggestion [#suggestion]

A clickable pill that triggers `onSelect` from the parent `Suggestions` context. Renders as a shadcn `Button`. Extends `Button` props (except `variant`).

<TypeTable
  type={{
  variant: {
    type: '"filled" | "outline" | "ghost"',
    default: '"filled"',
    description: "Visual style of the suggestion pill.",
  },
  highlight: {
    type: "string | string[]",
    description:
      "Defines the text(s) or word(s) in the suggestion to be highlighted. Matches are case-insensitive. Style the highlighted text via className or by composing your own wrapper.",
  },
  value: {
    type: "string",
    description: "The value passed to onSelect. Defaults to children when children is a string.",
  },
  children: {
    type: "React.ReactNode",
    description: "The content to display. Used as the value for onSelect when value is not provided.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called before onSelect.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the button.",
  },
}}
/>

SuggestionPanel [#suggestionpanel]

Full-width panel that displays below the input, covering the category pills. Uses `Presence` for enter/exit animations. Closes on Escape via `onOpenChange`. Use with `SuggestionPanelHeader`, `SuggestionPanelTitle`, `SuggestionPanelClose`, and `SuggestionPanelContent`.

<TypeTable
  type={{
  open: {
    type: "boolean",
    default: "true",
    description: "Controls panel visibility. Use for controlled open/close state.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description:
      "Callback fired when the panel open state changes (e.g. Escape key, SuggestionPanelClose click).",
  },
  onClose: {
    type: "() => void",
    description:
      "Callback fired when the panel's exit animation completes. Use for cleanup (e.g. focus management).",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the panel container.",
  },
  ref: {
    type: "React.Ref<HTMLDivElement>",
    description: "Forwarded ref to the panel div element.",
  },
}}
/>

SuggestionPanelHeader [#suggestionpanelheader]

Header row for the panel. Typically contains `SuggestionPanelTitle` and `SuggestionPanelClose`. Extends `React.HTMLAttributes<HTMLDivElement>`.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the header.",
  },
}}
/>

SuggestionPanelTitle [#suggestionpaneltitle]

Title area for the panel header. Use for the category icon and label. Extends `React.HTMLAttributes<HTMLDivElement>`.

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the title container.",
  },
}}
/>

SuggestionPanelClose [#suggestionpanelclose]

Close button for the panel. Clicking it calls `onOpenChange(false)` via context. Use `asChild` to merge props onto a child element. Extends `React.ButtonHTMLAttributes<HTMLButtonElement>`.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "Merges props onto the child element instead of rendering a button.",
  },
  onClick: {
    type: "React.MouseEventHandler<HTMLButtonElement>",
    description: "Optional click handler. Called after the panel close is triggered.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the close button.",
  },
}}
/>

SuggestionPanelContent [#suggestionpanelcontent]

Content wrapper for the panel's suggestion list. Use with `Suggestions` and `SuggestionList` inside. Use `asChild` to merge props onto a child element. Extends `React.HTMLAttributes<HTMLDivElement>`.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "Merges props onto the child element instead of rendering a div.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the content container.",
  },
}}
/>


# Text Shimmer



import TextShimmerDefault from "@/components/nexus-ui/examples/text-shimmer/default";
import TextShimmerRepeatDelay from "@/components/nexus-ui/examples/text-shimmer/repeat-delay";
import TextShimmerAngled from "@/components/nexus-ui/examples/text-shimmer/angled";
import TextShimmerInverted from "@/components/nexus-ui/examples/text-shimmer/inverted";
import TextShimmerAsParagraph from "@/components/nexus-ui/examples/text-shimmer/as-paragraph";
import TextShimmerCustomColor from "@/components/nexus-ui/examples/text-shimmer/custom-color";
import TextShimmerSlowSweep from "@/components/nexus-ui/examples/text-shimmer/slow-sweep";
import TextShimmerTightBeam from "@/components/nexus-ui/examples/text-shimmer/tight-beam";

An animated shimmer effect for loading and "in progress" feedback. `TextShimmer` renders a moving highlight across text and exposes controls for speed, spread, direction, contrast, and pause between passes.

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/default.tsx" previewClassName="p-0!">
  <TextShimmerDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/text-shimmer
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/text-shimmer
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/text-shimmer
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/text-shimmer
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/text-shimmer.tsx" title="components/nexus-ui/text-shimmer.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noLineNumbers
import { TextShimmer } from "@/components/nexus-ui/text-shimmer";
```

```tsx keepBackground noLineNumbers
<TextShimmer> Running tool calls... </TextShimmer>
```

Examples [#examples]

With Repeat Delay [#with-repeat-delay]

Use `repeatDelay` to create a pause between shimmer passes:

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/repeat-delay.tsx">
  <TextShimmerRepeatDelay />
</DemoWithCode>

Angled Shimmer [#angled-shimmer]

Tune beam geometry with `spread` and `angle`:

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/angled.tsx">
  <TextShimmerAngled />
</DemoWithCode>

Inverted Contrast [#inverted-contrast]

Use `invert` for lower-luminance emphasis (often better on lighter text):

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/inverted.tsx">
  <TextShimmerInverted />
</DemoWithCode>

As Custom Element [#as-custom-element]

Render another element type with `as`:

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/as-paragraph.tsx">
  <TextShimmerAsParagraph />
</DemoWithCode>

Custom Highlight Color [#custom-highlight-color]

Use `color` to override the highlight beam for accent-driven states:

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/custom-color.tsx">
  <TextShimmerCustomColor />
</DemoWithCode>

Slow Sweep [#slow-sweep]

Increase `duration` for a calmer motion profile:

<DemoWithCode src="components/nexus-ui/examples/text-shimmer/slow-sweep.tsx">
  <TextShimmerSlowSweep />
</DemoWithCode>

{/* ### Tight Beam

  Lower `spread` for a narrower, sharper highlight pass:

  <DemoWithCode src="components/nexus-ui/examples/text-shimmer/tight-beam.tsx">
  <TextShimmerTightBeam />
  </DemoWithCode> */}

API Reference [#api-reference]

TextShimmer [#textshimmer]

Animated text wrapper component. Uses a generated keyframe name per instance to avoid collisions and applies shimmer via a gradient background clipped to text.

<TypeTable
  type={{
  as: {
    type: "React.ElementType",
    default: '"span"',
    description: "Element or component to render as the wrapper.",
  },
  duration: {
    type: "number",
    default: "1",
    description:
      "Active shimmer movement duration in seconds. Values <= 0 stop movement but keep rendering stable.",
  },
  repeatDelay: {
    type: "number",
    default: "0",
    description:
      "Pause duration in seconds after each shimmer pass before the next pass starts.",
  },
  spread: {
    type: "number",
    default: "20",
    description:
      "Highlight beam width around center in percentage points. Clamped internally to 5..45.",
  },
  angle: {
    type: "number",
    default: "0",
    description:
      "Gradient beam angle in degrees. Positive values tilt the shimmer direction.",
  },
  color: {
    type: "string",
    description:
      "Optional highlight color override. Defaults to an OKLCH color derived from current text color.",
  },
  invert: {
    type: "boolean",
    default: "false",
    description:
      "Uses a darker highlight formula in both light and dark themes.",
  },
  invertLight: {
    type: "boolean",
    default: "false",
    description:
      "Uses the same darker highlight formula as `invert`, but only in light theme.",
  },
  invertDark: {
    type: "boolean",
    default: "false",
    description:
      "Uses the same darker highlight formula as `invert`, but only in dark theme.",
  },
  disableShimmer: {
    type: "boolean",
    default: "false",
    description:
      "Disables shimmer rendering and animation, showing plain text content.",
  },
  className: {
    type: "string",
    description:
      "Additional classes merged into the wrapper element.",
  },
  style: {
    type: "React.CSSProperties",
    description:
      "Inline style overrides merged after generated shimmer styles.",
  },
  children: {
    type: "React.ReactNode",
    description: "Text (or inline content) to render with shimmer treatment.",
  },
}}
/>


# Thread



import ThreadDefault from "@/components/nexus-ui/examples/thread/default";

A viewport for stacked **[Message](/docs/components/message)** turns (or any content) that **sticks to the bottom** as content grows—built on [**use-stick-to-bottom**](https://github.com/stackblitz/use-stick-to-bottom). **`Thread`** wraps the scroll root, **`ThreadContent`** wraps the scrolling column, and **`ThreadScrollToBottom`** shows a control when the user has scrolled away from the bottom.

<DemoWithCode src="components/nexus-ui/examples/thread/default.tsx" previewClassName="p-0!">
  <ThreadDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/thread
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/thread
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/thread
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/thread
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npm install @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons use-stick-to-bottom
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm add @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons use-stick-to-bottom
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn add @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons use-stick-to-bottom
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bun add @radix-ui/react-slot @hugeicons/react @hugeicons/core-free-icons use-stick-to-bottom
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy and paste the following code into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/thread.tsx" title="components/nexus-ui/thread.tsx" />
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Thread,
  ThreadContent,
  ThreadScrollToBottom,
} from "@/components/nexus-ui/thread";
```

```tsx keepBackground
<Thread className="h-[50vh]">
  <ThreadContent>{/* messages */}</ThreadContent>
  <ThreadScrollToBottom />
</Thread>
```

**`ThreadScrollToBottom`** must be rendered **inside** **`Thread`** so it can read **`useStickToBottomContext`**.

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

Render [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) messages inside **Thread** by mapping the same **`messages`** array you would render with **[Message](/docs/components/message)** alone. Read each **[`UIMessage`](https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message)** **`parts`** array and join **`text`** parts for **MessageMarkdown** (streaming updates apply as the SDK appends or grows **`TextUIPart`** content).

See [Prompt Input](/docs/components/prompt-input#vercel-ai-sdk-integration) for a minimal **`POST /api/chat`** route with **`streamText`** and **`toUIMessageStreamResponse`**.

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react @ai-sdk/openai
    ```
  </Step>

  <Step>
    <h3>
      Create your chat API route
    </h3>

    Use the same handler as in the Prompt Input docs: **`messages: await convertToModelMessages(messages)`** and **`return result.toUIMessageStreamResponse()`**.
  </Step>

  <Step>
    <h3>
      Map 

      `messages`

       to Message inside Thread
    </h3>

    Use **`isTextUIPart`** from **`ai`** so you only aggregate **`type: "text"`** segments. Skip **`system`** turns unless you surface them deliberately. Assistant messages can also include **reasoning**, **tool**, **source**, and other part types—extend this loop when you need those in the UI.

    ```tsx
    "use client";

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
    import {
      Message,
      MessageStack,
      MessageContent,
      MessageMarkdown,
    } from "@/components/nexus-ui/message";
    import {
      Thread,
      ThreadContent,
      ThreadScrollToBottom,
    } from "@/components/nexus-ui/thread";

    function textFromMessage(message: UIMessage) {
      return message.parts.filter(isTextUIPart).map((p) => p.text).join("");
    }

    export default function ChatThread() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      return (
        <Thread className="h-[min(70vh,32rem)]">
          <ThreadContent className="items-stretch">
            {messages
              .filter((m) => m.role !== "system")
              .map((m) => (
                <Message key={m.id} from={m.role === "user" ? "user" : "assistant"}>
                  <MessageStack>
                    <MessageContent>
                      <MessageMarkdown>{textFromMessage(m)}</MessageMarkdown>
                    </MessageContent>
                  </MessageStack>
                </Message>
              ))}
          </ThreadContent>
          <ThreadScrollToBottom />
        </Thread>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Thread [#thread]

Root scroll container wrapping [**`StickToBottom`**](https://github.com/stackblitz/use-stick-to-bottom). **`ThreadContent`** and **`ThreadScrollToBottom`** must live under **`Thread`** so the scrollable list and jump control share the same stick-to-bottom context.

<TypeTable
  type={{
  resize: {
    type: "ScrollBehavior | SpringAnimation",
    default: '"smooth"',
    description:
      "How the container reacts when content size changes. Pass a spring object ({ mass, damping, stiffness }) for animated resize.",
  },
  initial: {
    type: "boolean | ScrollBehavior | SpringAnimation",
    default: '"smooth"',
    description:
      "Scroll behavior on first mount. false skips scrolling to bottom initially.",
  },
  mass: {
    type: "number",
    default: "1.25",
    description:
      "Spring mass for smooth animations (from use-stick-to-bottom).",
  },
  damping: {
    type: "number",
    default: "0.7",
    description: "Spring damping for smooth animations.",
  },
  stiffness: {
    type: "number",
    default: "0.05",
    description: "Spring stiffness for smooth animations.",
  },
  targetScrollTop: {
    type: "(targetScrollTop, context) => number",
    description: "Optional override to compute scroll top from layout.",
  },
  contextRef: {
    type: "Ref<StickToBottomContext>",
    description: "Optional ref to the library context object.",
  },
  instance: {
    type: "StickToBottomInstance",
    description: "Optional external instance from useStickToBottom.",
  },
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the thread root.",
  },
  children: {
    type: "ReactNode | ((context) => ReactNode)",
    description:
      "Typically ThreadContent plus ThreadScrollToBottom; render props receive the library context if needed.",
  },
}}
/>

ThreadContent [#threadcontent]

Wraps the transcript—usually **`Message`** rows—so **`Thread`** can keep the viewport following the bottom as new content arrives (**`StickToBottom.Content`**).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional CSS classes to apply to the content wrapper.",
  },
  children: {
    type: "ReactNode | ((context) => ReactNode)",
    description: "Message list or other thread body.",
  },
}}
/>

ThreadScrollToBottom [#threadscrolltobottom]

Optional control that appears when the user has scrolled away from the bottom, so they can jump back to the latest messages. Supports polymorphism via **`asChild`**.

<TypeTable
  type={{
  asChild: {
    type: "boolean",
    default: "false",
    description:
      "Merge props into the child element (Radix Slot) instead of rendering a button.",
  },
  className: {
    type: "string",
    description:
      "Additional CSS classes to apply to the scroll-to-bottom control.",
  },
  children: {
    type: "React.ReactNode",
    description: "Optional. Defaults to ArrowDown02Icon via HugeiconsIcon.",
  },
}}
/>


# Toaster



import ToasterDefault from "@/components/nexus-ui/examples/toaster/default";
import ToasterWithDescription from "@/components/nexus-ui/examples/toaster/with-description";
import ToasterWithAction from "@/components/nexus-ui/examples/toaster/with-action";
import ToasterVariantsRow from "@/components/nexus-ui/examples/toaster/variants-row";
import ToasterPosition from "@/components/nexus-ui/examples/toaster/position";

Headless toast notifications with variant-aware styling and custom action/cancel controls; Powered by [Sonner](https://sonner.emilkowal.ski/).

<DemoWithCode src="components/nexus-ui/examples/toaster/default.tsx">
  <ToasterDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Steps>
      <Step>
        Run the following command:
      </Step>

      ```bash
      npx shadcn@latest add @nexus-ui/toaster
      ```

      <Step>
        Add the 

        `Toaster`

         component
      </Step>

      ```tsx title="app/layout.tsx"
      import { Toaster } from "@/components/nexus-ui/toaster"

      export default function RootLayout({ children }) {
        return (
          <html lang="en">
            <head />
            <body>
              <main>{children}</main>
              <Toaster />
            </body>
          </html>
        )
      }
      ```
    </Steps>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        Install the following dependencies:
      </Step>

      ```bash
      npx shadcn@latest add button && npm install @hugeicons/core-free-icons @hugeicons/react next-themes sonner
      ```

      <Step>
        Copy and paste the following code into your project.
      </Step>

      <ComponentSource src="components/nexus-ui/toaster.tsx" title="components/nexus-ui/toaster.tsx" />

      <Step>
        Add the 

        `Toaster`

         component
      </Step>

      ```tsx title="app/layout.tsx"
      import { Toaster } from "@/components/nexus-ui/toaster"

      export default function RootLayout({ children }) {
        return (
          <html lang="en">
            <head />
            <body>
              <Toaster />
              <main>{children}</main>
            </body>
          </html>
        )
      }
      ```
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground noLineNumbers
import { toast } from "@/components/nexus-ui/toaster";
```

```tsx keepBackground noLineNumbers
toast.default("Event has been created.");
```

Examples [#examples]

With Description [#with-description]

Add supporting copy with the `description` option.

<DemoWithCode src="components/nexus-ui/examples/toaster/with-description.tsx">
  <ToasterWithDescription />
</DemoWithCode>

With Action [#with-action]

Add an inline CTA using the `action` option.

<DemoWithCode src="components/nexus-ui/examples/toaster/with-action.tsx">
  <ToasterWithAction />
</DemoWithCode>

Variants [#variants]

Change the variant on each toast call to control the feedback type and visual style.

<DemoWithCode src="components/nexus-ui/examples/toaster/variants-row.tsx">
  <ToasterVariantsRow />
</DemoWithCode>

Position [#position]

Show toasts in different corners and center positions using per-toast `position`.

<DemoWithCode src="components/nexus-ui/examples/toaster/position.tsx">
  <ToasterPosition />
</DemoWithCode>

API Reference [#api-reference]

toast [#toast]

Custom headless toast helper built on `sonnerToast.custom`. It renders the `Toast` UI while forwarding supported Sonner toast options for behavior and lifecycle.

<TypeTable
  type={{
  title: {
    type: "React.ReactNode",
    description: "Primary toast title text/content.",
  },
  description: {
    type: "React.ReactNode",
    description: "Optional secondary body content.",
  },
  variant: {
    type: '"default" | "success" | "info" | "warning" | "error" | "loading"',
    default: '"default"',
    description:
      "Controls variant color tokens and leading icon. `default` uses neutral theme colors and no icon.",
  },
  icon: {
    type: "React.ReactNode | null",
    description:
      "Overrides the leading icon for this toast. Use `null` to hide it.",
  },
  action: {
    type: "{ label: React.ReactNode; onClick?: () => void }",
    description:
      "Optional custom action button rendered inside the toast card.",
  },
  cancel: {
    type: "{ label: React.ReactNode; onClick?: () => void }",
    description:
      "Optional secondary cancel button rendered inside the toast card.",
  },
  duration: {
    type: "number",
    default: "4000",
    description:
      "Auto-dismiss duration in milliseconds. Use `Infinity` to keep toast open.",
  },
  id: {
    type: "string | number",
    description: "Custom toast id for updates/deduping.",
  },
  position: {
    type: '"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"',
    description: "Override destination stack position for this toast.",
  },
  toasterId: {
    type: "string",
    description: "Route toast to a specific mounted toaster.",
  },
  dismissible: {
    type: "boolean",
    default: "true",
    description: "Whether the toast can be dismissed by interaction.",
  },
  onDismiss: {
    type: "() => void",
    description: "Called when toast is dismissed.",
  },
  onAutoClose: {
    type: "() => void",
    description: "Called when toast closes from timeout.",
  },
  invert: {
    type: "boolean",
    default: "false",
    description: "Use Sonner inverted styling mode where applicable.",
  },
  closeButton: {
    type: "boolean",
    default: "true",
    description:
      "Show/hide the custom toast close button for this toast instance.",
  },
  testId: {
    type: "string",
    description: "Testing identifier attached to toast node.",
  },
}}
/>

Toaster [#toaster]

Custom wrapper that mounts Sonner and syncs `theme` from `next-themes`, with `toastOptions={{ unstyled: true }}` by default.

<TypeTable
  type={{
  theme: {
    type: '"light" | "dark" | "system"',
    default: '"system"',
    description:
      "Theme synced from `next-themes`; can be overridden via prop.",
  },
  position: {
    type: '"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"',
    default: '"bottom-right"',
    description: "Global toast placement.",
  },
  expand: {
    type: "boolean",
    default: "false",
    description: "Expand stacked toasts on hover, or always when true.",
  },
  visibleToasts: {
    type: "number",
    default: "3",
    description: "Maximum visible toasts in stack.",
  },
  closeButton: {
    type: "boolean",
    default: "false",
    description:
      "Show default Sonner close controls globally (custom toast UI already renders its own close button).",
  },
  offset: {
    type: "string | number",
    description: "Desktop viewport offset for toast stack.",
  },
  mobileOffset: {
    type: "string | number",
    description: "Mobile viewport offset for toast stack.",
  },
  toastOptions: {
    type: "object",
    default: "{ unstyled: true }",
    description: "Default options applied to all toasts from this toaster.",
  },
  id: {
    type: "string",
    description: "Toaster id for multi-toaster setups.",
  },
}}
/>

For advanced usage and full configuration options, see the [Sonner docs](https://sonner.emilkowal.ski/).


# Tool



import ToolDefault from "@/components/nexus-ui/examples/tool/default";
import ToolPending from "@/components/nexus-ui/examples/tool/pending";
import ToolReady from "@/components/nexus-ui/examples/tool/ready";
import ToolRunning from "@/components/nexus-ui/examples/tool/running";
import ToolErrorState from "@/components/nexus-ui/examples/tool/error";

Status-aware UI for rendering tool calls (input and output JSON) in agent/chat interfaces. `Tool` is provider-agnostic and works well with Vercel AI SDK tool parts by mapping runtime tool states to the component status model.

<DemoWithCode src="components/nexus-ui/examples/tool/default.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <ToolDefault />
</DemoWithCode>

Installation [#installation]

<Tabs items={["CLI", "Manual"]} framed={false}>
  <Tab value="CLI">
    <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
      <Tab value="npm">
        ```bash
        npx shadcn@latest add @nexus-ui/tool
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dlx shadcn@latest add @nexus-ui/tool
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dlx shadcn@latest add @nexus-ui/tool
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bunx shadcn@latest add @nexus-ui/tool
        ```
      </Tab>
    </Tabs>
  </Tab>

  <Tab value="Manual">
    <Steps>
      <Step>
        <h3>
          Install the following dependencies:
        </h3>

        <Tabs items={["npm", "pnpm", "yarn", "bun"]}>
          <Tab value="npm">
            ```bash
            npx shadcn@latest add badge collapsible && npm install @hugeicons/react @hugeicons/core-free-icons @react-symbols/icons shiki
            ```
          </Tab>

          <Tab value="pnpm">
            ```bash
            pnpm dlx shadcn@latest add badge collapsible && pnpm add @hugeicons/react @hugeicons/core-free-icons @react-symbols/icons shiki
            ```
          </Tab>

          <Tab value="yarn">
            ```bash
            yarn dlx shadcn@latest add badge collapsible && yarn add @hugeicons/react @hugeicons/core-free-icons @react-symbols/icons shiki
            ```
          </Tab>

          <Tab value="bun">
            ```bash
            bunx shadcn@latest add badge collapsible && bun add @hugeicons/react @hugeicons/core-free-icons @react-symbols/icons shiki
            ```
          </Tab>
        </Tabs>
      </Step>

      <Step>
        <h3>
          Copy the following files into your project.
        </h3>

        <ComponentSource src="components/nexus-ui/tool.tsx" title="components/nexus-ui/tool.tsx" />

        <ComponentSource src="components/nexus-ui/codeblock-new.tsx" title="components/nexus-ui/codeblock-new.tsx" />

        <ComponentSource src="lib/shiki/highlighter.ts" title="lib/shiki/highlighter.ts" />

        <ComponentSource src="app/shiki.css" title="app/shiki.css" />
      </Step>

      <Step>
        <h3>
          Ensure your Shiki CSS is loaded.
        </h3>

        `CodeblockShiki` expects `.shiki` styles. Import `app/shiki.css` once at your app root (for example in `app/layout.tsx`):

        ```tsx
        import "./global.css";
        import "./shiki.css";
        ```
      </Step>

      <Step>
        <h3>
          Update import paths to match your project setup.
        </h3>
      </Step>
    </Steps>
  </Tab>
</Tabs>

Usage [#usage]

```tsx keepBackground
import {
  Tool,
  ToolTrigger,
  ToolContent,
  ToolInput,
  ToolOutput,
  type ToolStatus,
} from "@/components/nexus-ui/tool";
```

```tsx keepBackground noCollapse
<Tool status="completed" defaultOpen>
  <ToolTrigger name="get_weather" />
  <ToolContent>
    <ToolInput payload={{ city: "Paris", unit: "celsius" }} />
    <ToolOutput
      payload={{ city: "Paris", temperature: 22, condition: "sunny" }}
    />
  </ToolContent>
</Tool>
```

Examples [#examples]

Pending [#pending]

Set `status="pending"` and render only `ToolTrigger` + `ToolContent` while arguments are still streaming.

<DemoWithCode src="components/nexus-ui/examples/tool/pending.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <ToolPending />
</DemoWithCode>

Ready [#ready]

Set `status="ready"`, render `ToolInput` with parsed arguments, and omit `ToolOutput` until execution starts.

<DemoWithCode src="components/nexus-ui/examples/tool/ready.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <ToolReady />
</DemoWithCode>

Running [#running]

Set `status="running"` and keep only `ToolInput` visible while the tool call is in progress.

<DemoWithCode src="components/nexus-ui/examples/tool/running.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <ToolRunning />
</DemoWithCode>

Error State [#error-state]

Set `status="error"`, keep `ToolInput` visible, and render `ToolOutput` with `errorText` (and `showWhen={["error"]}`) to show a destructive error message.

<DemoWithCode src="components/nexus-ui/examples/tool/error.tsx" previewClassName="max-h-none! h-auto min-h-[420px] flex flex-col items-center py-30! justify-start">
  <ToolErrorState />
</DemoWithCode>

Vercel AI SDK Integration [#vercel-ai-sdk-integration]

`Tool` maps cleanly to AI SDK tool parts by converting streamed tool part state into `ToolStatus` and passing `input` / `output` payloads directly.

Use it with [Vercel AI SDK](https://sdk.vercel.ai) by:

* mapping AI SDK tool part states (`input-streaming`, `input-available`, `output-available`, `output-error`) into your UI states (`pending`, `ready`, `running`, `completed`, `error`)
* rendering `part.input` in `ToolInput`
* rendering `part.output` in `ToolOutput` when available
* passing `errorText` to `ToolOutput` for `error` state

`ready` is an app-level state in this mapping (for example, waiting on user approval before execution). AI SDK does not emit `ready` as a native tool part state.

`pending` via `input-streaming` is only available in streaming flows (for example, `streamText` / streamed UI messages).

<Steps>
  <Step>
    <h3>
      Install the AI SDK
    </h3>

    ```bash
    npm install ai @ai-sdk/react
    ```
  </Step>

  <Step>
    <h3>
      Stream messages from your chat API route
    </h3>

    ```ts title="app/api/chat/route.ts"
    import { streamText, convertToModelMessages, tool, type UIMessage } from "ai";
    import { z } from "zod";

    export async function POST(req: Request) {
      const { messages }: { messages: UIMessage[] } = await req.json();

      const result = streamText({
        model: "anthropic/claude-sonnet-4.5",
        messages: await convertToModelMessages(messages),
        tools: {
          displayWeather: tool({
            description: "Get weather for a city",
            inputSchema: z.object({
              city: z.string(),
              unit: z.enum(["celsius", "fahrenheit"]),
            }),
            execute: async ({ city, unit }) => ({
              city,
              unit,
              temperature: 22,
              condition: "sunny",
            }),
          }),
        },
      });

      return result.toUIMessageStreamResponse();
    }
    ```
  </Step>

  <Step>
    <h3>
      Map assistant tool parts into 

      <code className="tracking-0!">Tool</code>
    </h3>

    ```tsx
    "use client";

    import { useChat } from "@ai-sdk/react";
    import { DefaultChatTransport, type UIMessage } from "ai";
    import {
      Tool,
      ToolTrigger,
      ToolContent,
      ToolInput,
      ToolOutput,
      type ToolStatus,
    } from "@/components/nexus-ui/tool";

    type ToolPartLike = {
      type: string;
      state?: string;
      input?: unknown;
      output?: unknown;
      errorText?: string;
      error?: unknown;
    };

    function mapToolStatus(
      part: ToolPartLike,
      isAwaitingApproval: boolean,
    ): ToolStatus | null {
      switch (part.state) {
        case "input-streaming":
          // Streaming-only: arguments are still being generated.
          return "pending";
        case "input-available":
          // App-level mapping: use "ready" while waiting for approval/user action.
          return isAwaitingApproval ? "ready" : "running";
        case "output-available":
          return "completed";
        case "output-error":
          return "error";
        default:
          return null;
      }
    }

    function toolNameFromPartType(type: string): string {
      return type.startsWith("tool-") ? type.slice(5) : type;
    }

    export default function ToolCallsFromUseChat() {
      const { messages } = useChat({
        transport: new DefaultChatTransport({ api: "/api/chat" }),
      });

      const assistant = [...messages].reverse().find((m) => m.role === "assistant");
      if (!assistant) return null;

      const toolParts = assistant.parts.filter(
        (part): part is ToolPartLike => part.type.startsWith("tool-"),
      );

      if (toolParts.length === 0) return null;

      return (
        <div className="space-y-3">
          {toolParts.map((part, index) => {
            const status = mapToolStatus(part, false);
            if (!status) return null;

            return (
              <Tool key={`${part.type}-${index}`} status={status} defaultOpen>
                <ToolTrigger name={toolNameFromPartType(part.type)} />
                <ToolContent>
                  <ToolInput payload={part.input} />
                  <ToolOutput
                    payload={part.output}
                    errorText={part.errorText}
                    showWhen={["completed", "error"]}
                  />
                </ToolContent>
              </Tool>
            );
          })}
        </div>
      );
    }
    ```
  </Step>
</Steps>

API Reference [#api-reference]

Tool [#tool]

Root wrapper for a single tool call. Provides status context and status color tokens for child components. Wraps [Collapsible Root](https://www.radix-ui.com/primitives/docs/components/collapsible#root).

<TypeTable
  type={{
  status: {
    type: '"pending" | "ready" | "running" | "completed" | "error"',
    description:
      "Tool lifecycle state. Drives icon, badge label, and status colors used by descendants.",
  },
  open: {
    type: "boolean",
    description: "Controlled open state for the collapsible container.",
  },
  defaultOpen: {
    type: "boolean",
    description: "Initial open state in uncontrolled mode.",
  },
  onOpenChange: {
    type: "(open: boolean) => void",
    description: "Called when open state changes.",
  },
  className: {
    type: "string",
    description: "Additional classes merged with the root container styles.",
  },
}}
/>

ToolTrigger [#tooltrigger]

Header row for tool name, status icon, status badge, and expand/collapse chevron. Wraps [Collapsible Trigger](https://www.radix-ui.com/primitives/docs/components/collapsible#trigger).

<TypeTable
  type={{
  name: {
    type: "string",
    description: "Tool name shown in the trigger (for example, get_weather).",
  },
  className: {
    type: "string",
    description: "Additional classes merged with trigger row styles.",
  },
}}
/>

ToolContent [#toolcontent]

Body wrapper for input/output/error sections. Wraps [Collapsible Content](https://www.radix-ui.com/primitives/docs/components/collapsible#content).

<TypeTable
  type={{
  className: {
    type: "string",
    description: "Additional classes for spacing/layout inside expanded content.",
  },
  children: {
    type: "React.ReactNode",
    description: "Typically ToolInput and ToolOutput.",
  },
}}
/>

ToolInput [#toolinput]

Renders the tool input payload in a JSON codeblock.

<TypeTable
  type={{
  payload: {
    type: "unknown",
    description:
      "Input payload. Objects are pretty-stringified; strings are rendered as-is.",
  },
}}
/>

ToolOutput [#tooloutput]

Renders tool output payload in a JSON codeblock when the current tool state matches `showWhen`.

<TypeTable
  type={{
  payload: {
    type: "unknown",
    description:
      "Output payload. Objects are pretty-stringified; strings are rendered as-is.",
  },
  showWhen: {
    type: 'Array<"pending" | "ready" | "running" | "completed" | "error">',
    default: '["completed"]',
    description:
      "Controls which tool states render the output section.",
  },
  errorText: {
    type: "string",
    description:
      "Error message shown as a destructive text block when tool state is error.",
  },
}}
/>
