Agent handoff

Pro

A support conversation where an AI assistant hands off to a person. When it transfers, a card shows the summary it passes to the team and the typical wait. The person then joins and takes over, and a thin timeline on the left marks each step.

Live · keyboard ready

Order NW-48213

Northwind Home support

Assistant

Ask the assistant anything

A person from the team can join at any time.

  • A support widget where the AI answers first and escalates to a human agent.
  • Showing customers what the assistant shared with the team, so they do not repeat themselves.
  • Marketing or docs pages that explain an AI to human handoff flow.
  • A general chat with one assistant and no handoff: use chat-thread.
  • Showing an agent's internal steps or tool calls: use agent-timeline or tool-call.
  • A full help desk inbox for agents: build it from list and detail layouts instead.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { AgentHandoff } from "@/components/arc/agent-handoff/agent-handoff"; // Demo: plays a simulated support chat<AgentHandoff /> // Wired to your support backend<AgentHandoff  stage={conversation.stage}  title="Order NW-48213"  subtitle="Northwind Home support"  messages={conversation.aiMessages}  agentMessages={conversation.agentMessages}  typing={conversation.typing}  summary={{    issue: "Desk arrived with a cracked leg. Customer wants a full refund.",    fields: [{ label: "Order", value: "NW-48213" }, { label: "Asks for", value: "Full refund" }],  }}  agent={{ name: "Jordan Reyes", role: "Support specialist", avatar: "/jordan.jpg", joinedAt: "14:05" }}  provider="intercom"  onSend={text => sendMessage(text)}  onRequestHuman={() => escalate()}  onCancelTransfer={() => cancelEscalation()}  onRetry={() => escalate()}/>

API reference

2 parts. The first is the root.

AgentHandoff

A support conversation where an AI assistant hands off to a person, with a summary card and a step timeline. Leave out `stage` to play the built in simulated chat.

PropTypeDefaultDescription
stage"assistant" | "transferring" | "connected" | "resolved" | "unavailable"–Where the conversation is. Leave it out to play the simulated demo.
messagesHandoffMessage[][]The conversation with the assistant, before the handoff. Each message is { id, from: "customer" | "assistant" | "agent", text, time?, streaming? }.
agentMessagesHandoffMessage[][]Everything after the handoff card: notes the customer adds while waiting and the conversation with the person. The join step sits before the person's first message.
typing"assistant" | "agent" | nullnullWho is typing. The assistant shows a short shimmering Thinking line, a person shows typing dots.
summary{ issue: string; fields?: { label: string; value: string }[] }–What the assistant passes to the person. Shown in the handoff card from transferring on, collapsible.
assistant{ name?: string; model?: string; mark?: ReactNode }{ name: "Assistant", model: "Claude Sonnet 5.5" }The AI assistant on the first step. The mark defaults to the Claude mark; pass a 14px mark for another model.
agent{ name: string; role?: string; avatar?: string; joinedAt?: string }–The person who takes over. Shown on the join step, in the card and in the header from connected on.
provider"intercom" | "zendesk" | { name: string; mark: ReactNode }"intercom"The help desk the conversation moves to. Its mark sits on the timeline next to the handoff card.
replyEstimatestring"2 min"Typical wait shown while transferring.
titlestring"Support chat"Header title, such as the order or ticket number.
subtitlestring–Muted line under the title.
onSend(text: string) => void–Shows the composer. Called with the trimmed text; append the message to messages or agentMessages yourself.
onRequestHuman() => void–Shows a Talk to a person reply under the assistant's last answer while the stage is assistant.
onCancelTransfer() => void–Shows Cancel on the handoff card while transferring.
onRetry() => void–Shows Try again on the handoff card when the stage is unavailable.
placeholderstring–Composer placeholder. Defaults to one that follows the stage, such as Message Jordan.
heightnumber | string600Card height. The thread scrolls inside and stays pinned to the newest message.
noticestring–A one-off message for screen readers, such as Refund issued.
scenario"handoff" | "unavailable""handoff"Simulation only: unavailable stops when nobody picks the transfer up, so Try again shows.
classNamestring–Class on the root card.

useHandoffSimulation

The hook behind the demo: returns stage, messages, agentMessages, typing, summary, notice and the handlers, so you can drive the view yourself or prototype a flow.

PropTypeDefaultDescription
scenarioRequired"handoff" | "unavailable"–Which story to play.
reducedRequiredboolean–Reduced motion: replies appear whole and the loop plays once.
Tab
Moves through the thread, Talk to a person, the card actions, the summary toggle and the composer.
Enter
Sends the composer message, or activates the focused button.
Space
Toggles the summary when the summary button has focus.
  • The thread is a focusable log so keyboard users can scroll it; it does not announce every streamed chunk.
  • Stage changes and action results are announced once through a polite status region, including that the demo is simulated.
  • Every status pairs an icon with a label: Transferring, With Jordan, Resolved, No one available. Color is never the only signal.
  • The summary toggle uses aria-expanded and aria-controls. Typing dots carry a label such as Jordan is typing.
  • Marks and avatars on the timeline are decorative next to their visible labels.
  • Assistant replies stream in chunks of 3 to 8 words every ~34ms; each chunk only fades in over 140ms.
  • The thread animates to its measured height with a bounce-free spring (0.24s), so new rows and streamed lines push it up without a snap.
  • While transferring, a small accent dot travels down the timeline beside the handoff card.
  • Status labels crossfade (100ms out, 160ms in, 2px rise). Loops pause off screen.
  • Reduced motion: replies appear whole, no travel dot, typing dots and spinner hold still, no height animation, the demo plays once.
  • Fills its container up to 560px wide; holds up at 360px with tighter padding and wider bubbles.
  • Fixed height with an internal scroll pinned to the newest message, so the page never shifts as the thread grows.
  • Long names and meta lines truncate with an ellipsis; message text wraps.
  • One ResizeObserver and one IntersectionObserver per card. Streaming updates re-render only the message being streamed and its chunks.
  • CSS keyframes for the dots, spinner and travel dot; all pause off screen.
  • For very long threads, pass only the recent messages; the card is meant for a live conversation, not an archive.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use for support or sales chat where an AI assistant escalates to a person. Keep the assistant's messages in `messages` and everything after the handoff in `agentMessages`.
  • Drive `stage` from your backend: transferring when you create the ticket, connected when a person accepts, unavailable when the queue times out.
  • Stream assistant text by updating the message's `text` with `streaming: true`; the component fades each appended chunk.
  • Pass `summary` with the same facts you send to the help desk so the customer sees what was shared.
  • Do not use `scenario` outside demos; it only affects the built in simulation.

The full library index for assistants is at /llms.txt.