{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat",
  "title": "Chat",
  "author": "Jamie Thompson <jamie@jamiethompson.design> (https://jamiethompson.design)",
  "description": "AI chat block for AI SDK useChat: a MessageScroller transcript that follows streamed output, an Empty state with one-click Suggestions, user text in Bubbles, assistant parts as Message Response (Streamdown), Reasoning, Tool (tool-* and dynamic-tool) and Attachments, a shimmering Marker while a request is pending, an error row with Retry on failure, and a Prompt Input composer with attachments whose submit button becomes Stop while a response is in flight (given onStop). Ships as a composable Chat component (plus ChatMessages, ChatMessage, ChatMessagePart, ChatToolPart, ChatAttachments, ChatEmpty, ChatThinkingMarker, ChatErrorMarker and ChatComposer exports), a scripted demo conversation built with @shadcn/helpers createChat that streams with no API key, and an app/chat page wiring it up. Use it to start a chatbot, assistant or agent UI, then swap the demo transport for your own route.",
  "dependencies": [
    "ai@^7",
    "@ai-sdk/react@^4",
    "@shadcn/helpers@^0.2",
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "attachment",
    "bubble",
    "button",
    "empty",
    "marker",
    "message",
    "message-scroller",
    "@uifiles/prompt-input",
    "@uifiles/reasoning",
    "@uifiles/response",
    "@uifiles/suggestion",
    "@uifiles/tool"
  ],
  "files": [
    {
      "path": "registry/blocks/chat/components/blocks/chat.tsx",
      "content": "\"use client\"\n\nimport type {\n  ChatStatus,\n  FileUIPart,\n  UIDataTypes,\n  UIMessage,\n  UIMessagePart,\n  UITools,\n} from \"ai\"\nimport { isToolUIPart } from \"ai\"\nimport { cn } from \"cn\"\nimport {\n  CircleAlertIcon,\n  FileIcon,\n  LoaderCircleIcon,\n  MessageCircleDashedIcon,\n  XIcon,\n} from \"lucide-react\"\nimport type { ComponentProps, KeyboardEvent, ReactNode } from \"react\"\nimport { Component, useCallback } from \"react\"\nimport {\n  Attachment,\n  AttachmentAction,\n  AttachmentActions,\n  AttachmentContent,\n  AttachmentDescription,\n  AttachmentGroup,\n  AttachmentMedia,\n  AttachmentTitle,\n} from \"@/components/ui/attachment\"\nimport { Bubble, BubbleContent } from \"@/components/ui/bubble\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/components/ui/empty\"\nimport { Marker, MarkerContent, MarkerIcon } from \"@/components/ui/marker\"\nimport { Message, MessageContent } from \"@/components/ui/message\"\nimport {\n  MessageScroller,\n  MessageScrollerButton,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerProvider,\n  MessageScrollerViewport,\n} from \"@/components/ui/message-scroller\"\nimport {\n  PromptInput,\n  PromptInputActionAddAttachments,\n  PromptInputActionMenu,\n  PromptInputActionMenuContent,\n  PromptInputActionMenuTrigger,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputHeader,\n  type PromptInputMessage,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  PromptInputTools,\n  usePromptInputAttachments,\n} from \"@/registry/ai/prompt-input\"\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/registry/ai/reasoning\"\nimport { MessageResponse } from \"@/registry/ai/response\"\nimport { Suggestion, Suggestions } from \"@/registry/ai/suggestion\"\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n  type ToolPart,\n} from \"@/registry/ai/tool\"\n\n/** One part of an AI SDK `UIMessage`, with the default data and tool maps. */\nexport type ChatMessagePartType = UIMessagePart<UIDataTypes, UITools>\n\n/** What the composer hands back on submit: the text and any attached files. */\nexport type ChatSubmitMessage = PromptInputMessage\n\n/** Called by the error row's Retry button; `regenerate` from `useChat` fits as-is. */\nexport type ChatRetryHandler = () => void | Promise<void>\n\nfunction isGenerating(status: ChatStatus) {\n  return status === \"submitted\" || status === \"streaming\"\n}\n\n/**\n * Calls a consumer's `onSubmit` without waiting for it (`sendMessage`\n * settles only once the answer has streamed) and reports a rejection through\n * `console.error` instead of leaving it unhandled. A synchronous throw is\n * reported the same way; the return value says whether the call went out.\n */\nfunction submitInBackground(\n  onSubmit: (message: ChatSubmitMessage) => void | Promise<void>,\n  message: ChatSubmitMessage\n): boolean {\n  try {\n    Promise.resolve(onSubmit(message)).catch((reason: unknown) =>\n      console.error(reason)\n    )\n    return true\n  } catch (reason) {\n    console.error(reason)\n    return false\n  }\n}\n\nexport type ChatProps = Omit<ComponentProps<\"div\">, \"onSubmit\"> & {\n  messages: UIMessage[]\n  status: ChatStatus\n  onSubmit: (message: ChatSubmitMessage) => void | Promise<void>\n  onStop?: (() => void) | undefined\n  /** The `error` from `useChat`; shown as a row at the end of the transcript. */\n  error?: Error | undefined\n  /** Called by the error row's Retry button; pass `regenerate` from `useChat`. */\n  onRetry?: ChatRetryHandler | undefined\n  /** One-click prompts shown in the empty state; each is submitted as-is. */\n  suggestions?: string[] | undefined\n  placeholder?: string | undefined\n  emptyTitle?: ReactNode\n  emptyDescription?: ReactNode\n}\n\n/**\n * A complete chat surface for AI SDK `useChat`: a scrolling transcript with an\n * empty state, streamed assistant parts, a \"thinking\" marker, an error row\n * with retry, and a composer. Fills its parent, so render it inside a\n * height-constrained container.\n */\nexport function Chat({\n  messages,\n  status,\n  onSubmit,\n  onStop,\n  error,\n  onRetry,\n  suggestions,\n  placeholder,\n  emptyTitle,\n  emptyDescription,\n  className,\n  ...props\n}: ChatProps) {\n  const busy = isGenerating(status)\n  const handleSuggestion = useCallback(\n    (text: string) => {\n      if (busy) return\n      submitInBackground(onSubmit, { text, files: [] })\n    },\n    [onSubmit, busy]\n  )\n\n  return (\n    <div\n      data-slot=\"chat\"\n      className={cn(\"flex h-full min-h-0 w-full flex-col gap-4\", className)}\n      {...props}\n    >\n      <MessageScrollerProvider autoScroll>\n        <ChatMessages\n          emptyDescription={emptyDescription}\n          emptyTitle={emptyTitle}\n          error={error}\n          messages={messages}\n          onRetry={onRetry}\n          onSuggestionClick={handleSuggestion}\n          status={status}\n          suggestions={suggestions}\n        />\n      </MessageScrollerProvider>\n      <ChatComposer\n        onStop={onStop}\n        onSubmit={onSubmit}\n        placeholder={placeholder}\n        status={status}\n      />\n    </div>\n  )\n}\n\nexport type ChatMessagesProps = ComponentProps<typeof MessageScroller> & {\n  messages: UIMessage[]\n  status: ChatStatus\n  error?: Error | undefined\n  onRetry?: ChatRetryHandler | undefined\n  suggestions?: string[] | undefined\n  onSuggestionClick?: ((suggestion: string) => void) | undefined\n  emptyTitle?: ReactNode\n  emptyDescription?: ReactNode\n}\n\n/**\n * The transcript: a `MessageScroller` frame (render it inside a\n * `MessageScrollerProvider`) that shows `ChatEmpty` until the first message,\n * one `ChatMessage` per message, `ChatThinkingMarker` while a request is\n * pending, and `ChatErrorMarker` when `error` is set or `status` is \"error\"\n * (a generic message when no `error` is given).\n */\nexport function ChatMessages({\n  messages,\n  status,\n  error,\n  onRetry,\n  suggestions,\n  onSuggestionClick,\n  emptyTitle,\n  emptyDescription,\n  className,\n  ...props\n}: ChatMessagesProps) {\n  return (\n    <MessageScroller className={cn(\"flex-1\", className)} {...props}>\n      <MessageScrollerViewport>\n        <MessageScrollerContent aria-busy={isGenerating(status)}>\n          {messages.length === 0 ? (\n            <MessageScrollerItem className=\"flex flex-1\">\n              <ChatEmpty\n                description={emptyDescription}\n                onSuggestionClick={onSuggestionClick}\n                suggestions={suggestions}\n                title={emptyTitle}\n              />\n            </MessageScrollerItem>\n          ) : (\n            messages.map((message, index) => (\n              <ChatMessage\n                key={message.id}\n                message={message}\n                status={index === messages.length - 1 ? status : \"ready\"}\n              />\n            ))\n          )}\n          {status === \"submitted\" ? <ChatThinkingMarker /> : null}\n          {error || status === \"error\" ? (\n            <ChatErrorMarker error={error} onRetry={onRetry} />\n          ) : null}\n        </MessageScrollerContent>\n      </MessageScrollerViewport>\n      <MessageScrollerButton />\n    </MessageScroller>\n  )\n}\n\nexport type ChatEmptyProps = Omit<ComponentProps<typeof Empty>, \"title\"> & {\n  title?: ReactNode\n  description?: ReactNode\n  suggestions?: string[] | undefined\n  onSuggestionClick?: ((suggestion: string) => void) | undefined\n}\n\n/** Empty-transcript state with an optional row of one-click suggestions. */\nexport function ChatEmpty({\n  title = \"Start a conversation\",\n  description = \"Ask a question or pick a suggestion to begin.\",\n  suggestions,\n  onSuggestionClick,\n  className,\n  ...props\n}: ChatEmptyProps) {\n  return (\n    <Empty className={cn(\"h-full\", className)} {...props}>\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\">\n          <MessageCircleDashedIcon />\n        </EmptyMedia>\n        <EmptyTitle>{title}</EmptyTitle>\n        <EmptyDescription>{description}</EmptyDescription>\n      </EmptyHeader>\n      {suggestions && suggestions.length > 0 ? (\n        <EmptyContent>\n          <Suggestions>\n            {suggestions.map((suggestion) => (\n              <Suggestion\n                key={suggestion}\n                suggestion={suggestion}\n                {...(onSuggestionClick !== undefined && {\n                  onClick: onSuggestionClick,\n                })}\n              />\n            ))}\n          </Suggestions>\n        </EmptyContent>\n      ) : null}\n    </Empty>\n  )\n}\n\nexport type ChatMessageProps = Omit<\n  ComponentProps<typeof MessageScrollerItem>,\n  \"children\"\n> & {\n  message: UIMessage\n  /**\n   * The chat status while this is the live message. Once it is not\n   * generating, parts left marked streaming or running (after Stop, or a\n   * stream that ended early) are shown settled. Omit for standalone use.\n   */\n  status?: ChatStatus | undefined\n}\n\n/** Parts the block draws; step, source, data and custom parts are not among them. */\nfunction isRenderablePart(part: ChatMessagePartType) {\n  if (isToolUIPart(part)) return true\n  switch (part.type) {\n    case \"text\":\n      return part.text.trim() !== \"\"\n    case \"reasoning\":\n    case \"file\":\n      return true\n    default:\n      return false\n  }\n}\n\n/**\n * One transcript row (render it inside a `MessageScrollerProvider`). User\n * messages align end and render text in a `Bubble`; assistant messages render\n * each part with the matching AI component. File parts from either role\n * render as an `AttachmentGroup` above the text. A message with nothing to\n * draw yet, such as the `start`/`start-step` window before the first token,\n * renders no row at all.\n */\nexport function ChatMessage({ message, status, ...props }: ChatMessageProps) {\n  const isUser = message.role === \"user\"\n\n  if (message.role === \"system\") {\n    const text = getMessageText(message)\n    if (text.trim() === \"\") return null\n    return (\n      <MessageScrollerItem messageId={message.id} {...props}>\n        <Marker variant=\"separator\">\n          <MarkerContent>{text}</MarkerContent>\n        </Marker>\n      </MessageScrollerItem>\n    )\n  }\n\n  if (!message.parts.some(isRenderablePart)) return null\n\n  const files = message.parts.filter(\n    (part): part is FileUIPart => part.type === \"file\"\n  )\n\n  return (\n    <MessageScrollerItem\n      messageId={message.id}\n      scrollAnchor={isUser}\n      {...props}\n    >\n      <Message align={isUser ? \"end\" : \"start\"}>\n        <MessageContent>\n          {files.length > 0 ? <ChatAttachments files={files} /> : null}\n          {message.parts.map((part, index) => (\n            <ChatMessagePart\n              // biome-ignore lint/suspicious/noArrayIndexKey: UI parts carry no id; the array is append-only while streaming, so the index is stable.\n              key={`${message.id}-${part.type}-${index}`}\n              part={part}\n              role={message.role}\n              status={status}\n            />\n          ))}\n        </MessageContent>\n      </Message>\n    </MessageScrollerItem>\n  )\n}\n\nexport type ChatMessagePartProps = {\n  part: ChatMessagePartType\n  role: UIMessage[\"role\"]\n  /** See `ChatMessageProps[\"status\"]`. */\n  status?: ChatStatus | undefined\n}\n\n/** Whether parts may still be receiving deltas; unknown status counts as live. */\nfunction isLive(status: ChatStatus | undefined) {\n  return status === undefined || isGenerating(status)\n}\n\n/**\n * Renders a single UI part: user text → `Bubble`, assistant text →\n * `MessageResponse`, reasoning → `Reasoning`, tool-* / dynamic-tool → `Tool`.\n * Whitespace-only text renders nothing, so a files-only turn shows just its\n * attachments. File parts are rendered by `ChatAttachments`; step, source,\n * data and custom parts render nothing. A part that throws while rendering\n * becomes an inline error row instead of unmounting the chat.\n */\nexport function ChatMessagePart({ part, role, status }: ChatMessagePartProps) {\n  return (\n    <ChatMessagePartBoundary\n      partType={part.type}\n      resetKey={\"state\" in part ? part.state : undefined}\n    >\n      <ChatMessagePartBody part={part} role={role} status={status} />\n    </ChatMessagePartBoundary>\n  )\n}\n\nfunction ChatMessagePartBody({ part, role, status }: ChatMessagePartProps) {\n  if (isToolUIPart(part)) {\n    return <ChatToolPart part={part} status={status} />\n  }\n\n  switch (part.type) {\n    case \"text\":\n      if (part.text.trim() === \"\") {\n        return null\n      }\n      if (role === \"user\") {\n        return (\n          <Bubble>\n            <BubbleContent className=\"whitespace-pre-wrap\">\n              {part.text}\n            </BubbleContent>\n          </Bubble>\n        )\n      }\n      return <MessageResponse>{part.text}</MessageResponse>\n    case \"reasoning\":\n      return (\n        <Reasoning\n          className=\"mb-0\"\n          isStreaming={part.state === \"streaming\" && isLive(status)}\n        >\n          <ReasoningTrigger />\n          <ReasoningContent>{part.text}</ReasoningContent>\n        </Reasoning>\n      )\n    default:\n      return null\n  }\n}\n\ntype ChatMessagePartBoundaryProps = {\n  children: ReactNode\n  partType: string\n  /** A change (the part's `state`) retries the part after a render error. */\n  resetKey: unknown\n}\n\ntype ChatMessagePartBoundaryState = {\n  error: Error | null\n}\n\n/** Keeps one malformed part from unmounting the whole transcript. */\nclass ChatMessagePartBoundary extends Component<\n  ChatMessagePartBoundaryProps,\n  ChatMessagePartBoundaryState\n> {\n  state: ChatMessagePartBoundaryState = { error: null }\n\n  static getDerivedStateFromError(\n    error: unknown\n  ): ChatMessagePartBoundaryState {\n    return { error: error instanceof Error ? error : new Error(String(error)) }\n  }\n\n  componentDidUpdate(previous: ChatMessagePartBoundaryProps) {\n    if (this.state.error && previous.resetKey !== this.props.resetKey) {\n      this.setState({ error: null })\n    }\n  }\n\n  render() {\n    const { error } = this.state\n    if (!error) return this.props.children\n    return (\n      <Marker className=\"text-destructive\" role=\"alert\">\n        <MarkerIcon>\n          <CircleAlertIcon />\n        </MarkerIcon>\n        <MarkerContent>\n          Could not render the {this.props.partType} part: {error.message}\n        </MarkerContent>\n      </Marker>\n    )\n  }\n}\n\nexport type ChatToolPartProps = {\n  part: ToolPart\n  /** See `ChatMessageProps[\"status\"]`. */\n  status?: ChatStatus | undefined\n}\n\n/** The unparsed input the SDK keeps when a tool call's JSON failed to parse. */\nfunction formatRawInput(rawInput: unknown) {\n  if (rawInput === undefined) return undefined\n  if (typeof rawInput === \"string\") return rawInput\n  try {\n    return JSON.stringify(rawInput, null, 2)\n  } catch {\n    return String(rawInput)\n  }\n}\n\n/**\n * A tool call in any AI SDK state; opens by default when the tool errored.\n * `ToolInput` shows its \"No input yet\" placeholder until the first input\n * delta arrives; the raw input is shown as text when the call errored before\n * its input parsed. A call still marked running once the chat is no longer\n * generating (after Stop, or a client tool awaiting its output) is shown as\n * Pending rather than Running.\n */\nexport function ChatToolPart({ part, status }: ChatToolPartProps) {\n  const isErrored = part.state === \"output-error\"\n  const rawInput =\n    isErrored && part.input === undefined && \"rawInput\" in part\n      ? formatRawInput(part.rawInput)\n      : undefined\n  const state =\n    part.state === \"input-available\" && !isLive(status)\n      ? \"input-streaming\"\n      : part.state\n\n  return (\n    <Tool className=\"mb-0\" defaultOpen={isErrored}>\n      {part.type === \"dynamic-tool\" ? (\n        <ToolHeader\n          state={state}\n          toolName={part.toolName}\n          type=\"dynamic-tool\"\n          {...(part.title !== undefined && { title: part.title })}\n        />\n      ) : (\n        <ToolHeader\n          state={state}\n          type={part.type}\n          {...(part.title !== undefined && { title: part.title })}\n        />\n      )}\n      <ToolContent>\n        {rawInput !== undefined ? (\n          <div className=\"space-y-2 overflow-hidden\">\n            <div className=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">\n              Raw input\n            </div>\n            <pre className=\"rounded-md bg-muted/50 p-3 text-xs wrap-break-word whitespace-pre-wrap\">\n              {rawInput}\n            </pre>\n          </div>\n        ) : (\n          <ToolInput input={part.input} />\n        )}\n        <ToolOutput errorText={part.errorText} output={part.output} />\n      </ToolContent>\n    </Tool>\n  )\n}\n\nexport type ChatAttachmentsProps = Omit<\n  ComponentProps<typeof AttachmentGroup>,\n  \"children\"\n> & {\n  files: FileUIPart[]\n  onRemove?: ((index: number) => void) | undefined\n}\n\n/** AI SDK file parts as a scrollable row of shadcn `Attachment`s. */\nexport function ChatAttachments({\n  files,\n  onRemove,\n  ...props\n}: ChatAttachmentsProps) {\n  return (\n    <AttachmentGroup {...props}>\n      {files.map((file, index) => {\n        const isImage = file.mediaType.startsWith(\"image\")\n        const name = file.filename ?? (isImage ? \"Image\" : \"File\")\n        return (\n          <Attachment\n            // biome-ignore lint/suspicious/noArrayIndexKey: file parts have no id and the same file can be attached twice.\n            key={`${file.url}-${index}`}\n            orientation={isImage ? \"vertical\" : \"horizontal\"}\n            size=\"sm\"\n          >\n            <AttachmentMedia variant={isImage ? \"image\" : \"icon\"}>\n              {isImage ? (\n                // biome-ignore lint/performance/noImgElement: the source is usually a data: or blob: URL, which next/image cannot optimize.\n                <img alt={name} src={file.url} />\n              ) : (\n                <FileIcon />\n              )}\n            </AttachmentMedia>\n            <AttachmentContent>\n              <AttachmentTitle>{name}</AttachmentTitle>\n              <AttachmentDescription>{file.mediaType}</AttachmentDescription>\n            </AttachmentContent>\n            {onRemove ? (\n              <AttachmentActions>\n                <AttachmentAction\n                  aria-label={`Remove ${name}`}\n                  onClick={() => onRemove(index)}\n                >\n                  <XIcon />\n                </AttachmentAction>\n              </AttachmentActions>\n            ) : null}\n          </Attachment>\n        )\n      })}\n    </AttachmentGroup>\n  )\n}\n\nexport type ChatThinkingMarkerProps = ComponentProps<typeof MessageScrollerItem>\n\n/** Transcript row shown while a request is submitted but nothing has streamed. */\nexport function ChatThinkingMarker({\n  children = \"Thinking…\",\n  ...props\n}: ChatThinkingMarkerProps) {\n  return (\n    <MessageScrollerItem {...props}>\n      <Marker>\n        <MarkerIcon>\n          <LoaderCircleIcon className=\"animate-spin\" />\n        </MarkerIcon>\n        <MarkerContent className=\"shimmer\">{children}</MarkerContent>\n      </Marker>\n    </MessageScrollerItem>\n  )\n}\n\nexport type ChatErrorMarkerProps = Omit<\n  ComponentProps<typeof MessageScrollerItem>,\n  \"children\"\n> & {\n  /** Shown by message; without one (or with an empty message) a generic line. */\n  error?: Error | undefined\n  onRetry?: ChatRetryHandler | undefined\n}\n\n/**\n * Transcript row for a failed request, with a Retry button when `onRetry` is\n * given. A rejected `onRetry` (`regenerate` rejects when there is nothing to\n * regenerate) is reported through `console.error`, never as an unhandled\n * rejection.\n */\nexport function ChatErrorMarker({\n  error,\n  onRetry,\n  ...props\n}: ChatErrorMarkerProps) {\n  const retry = useCallback(() => {\n    if (!onRetry) return\n    Promise.resolve()\n      .then(() => onRetry())\n      .catch((reason: unknown) => console.error(reason))\n  }, [onRetry])\n\n  return (\n    <MessageScrollerItem {...props}>\n      <Marker className=\"text-destructive\" role=\"alert\">\n        <MarkerIcon>\n          <CircleAlertIcon />\n        </MarkerIcon>\n        <MarkerContent>\n          {error?.message || \"Something went wrong.\"}\n        </MarkerContent>\n        {onRetry ? (\n          <Button\n            className=\"ml-auto\"\n            onClick={retry}\n            size=\"sm\"\n            variant=\"outline\"\n          >\n            Retry\n          </Button>\n        ) : null}\n      </Marker>\n    </MessageScrollerItem>\n  )\n}\n\nexport type ChatComposerProps = Omit<\n  ComponentProps<typeof PromptInput>,\n  \"onSubmit\" | \"children\"\n> & {\n  onSubmit: (message: ChatSubmitMessage) => void | Promise<void>\n  onStop?: (() => void) | undefined\n  status?: ChatStatus | undefined\n  placeholder?: string | undefined\n}\n\n/**\n * The prompt input: textarea (Enter submits, Shift+Enter breaks a line), an\n * attachment menu, a preview of attached files, and a submit button. While a\n * response is in flight nothing submits and the draft and attachments are\n * kept; the button is then a Stop button that calls `onStop` when one is\n * passed (`Chat` passes its own through), and without `onStop` it keeps the\n * Submit name and a press does nothing. After an error the button is a plain\n * Submit again, since the transcript's error row carries Retry. `onSubmit`'s\n * promise is not awaited: `sendMessage` settles only when the answer has\n * finished streaming, and the composer clears as soon as the message is sent.\n * A rejection is reported through `console.error`; a synchronous throw is\n * reported too and keeps the draft.\n */\nexport function ChatComposer({\n  onSubmit,\n  onStop,\n  status = \"ready\",\n  placeholder,\n  ...props\n}: ChatComposerProps) {\n  const busy = isGenerating(status)\n\n  const handleSubmit = useCallback(\n    (message: PromptInputMessage) => {\n      // `false` rejects the submit: PromptInput keeps the attachments and\n      // restores the draft (a programmatic requestSubmit skips the guards).\n      if (busy) return false\n      if (message.text.trim() === \"\" && message.files.length === 0) {\n        return\n      }\n      // A synchronous throw rejects the submit, so the draft and the\n      // attachments stay for another try.\n      if (!submitInBackground(onSubmit, message)) return false\n    },\n    [onSubmit, busy]\n  )\n\n  // PromptInput resets the form before calling onSubmit, so Enter has to be\n  // stopped here to keep the draft while a response is in flight.\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLTextAreaElement>) => {\n      if (\n        busy &&\n        event.key === \"Enter\" &&\n        !event.shiftKey &&\n        !event.nativeEvent.isComposing\n      ) {\n        event.preventDefault()\n      }\n    },\n    [busy]\n  )\n\n  const handleSubmitClick = useCallback<\n    NonNullable<ComponentProps<typeof PromptInputSubmit>[\"onClick\"]>\n  >(\n    (event) => {\n      if (busy) event.preventDefault()\n    },\n    [busy]\n  )\n\n  return (\n    <PromptInput multiple onSubmit={handleSubmit} {...props}>\n      <ChatComposerAttachments />\n      <PromptInputBody>\n        <PromptInputTextarea\n          onKeyDown={handleKeyDown}\n          placeholder={placeholder}\n        />\n      </PromptInputBody>\n      <PromptInputFooter>\n        <PromptInputTools>\n          <PromptInputActionMenu>\n            <PromptInputActionMenuTrigger aria-label=\"Add attachment\" />\n            <PromptInputActionMenuContent>\n              <PromptInputActionAddAttachments />\n            </PromptInputActionMenuContent>\n          </PromptInputActionMenu>\n        </PromptInputTools>\n        <PromptInputSubmit\n          onClick={handleSubmitClick}\n          status={status === \"error\" ? \"ready\" : status}\n          {...(onStop !== undefined && { onStop })}\n        />\n      </PromptInputFooter>\n    </PromptInput>\n  )\n}\n\n/** Attached-but-unsent files, read from the surrounding `PromptInput`. */\nfunction ChatComposerAttachments() {\n  const attachments = usePromptInputAttachments()\n  const remove = useCallback(\n    (index: number) => {\n      const file = attachments.files[index]\n      if (file) attachments.remove(file.id)\n    },\n    [attachments]\n  )\n\n  if (attachments.files.length === 0) {\n    return null\n  }\n\n  return (\n    <PromptInputHeader>\n      <ChatAttachments files={attachments.files} onRemove={remove} />\n    </PromptInputHeader>\n  )\n}\n\n/** Concatenates the text parts of a message. */\nexport function getMessageText(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\")\n}\n",
      "type": "registry:component",
      "target": "components/blocks/chat.tsx"
    },
    {
      "path": "registry/blocks/chat/lib/demo-conversation.ts",
      "content": "import { createChat } from \"@shadcn/helpers/ai-sdk\"\nimport { getMessageText } from \"@/registry/blocks/chat/components/blocks/chat\"\n\n/**\n * A scripted conversation for the chat block. `@shadcn/helpers` turns it into\n * an AI SDK transport that streams the assistant turns word by word, so the\n * block works with no API route and no API key.\n */\nexport const demoConversation = createChat({\n  messageIdPrefix: \"demo-message\",\n  toolCallIdPrefix: \"demo-call\",\n})\n  .user(\"How do I add a new color token to this design system?\")\n  .sleep(600)\n  .assistant(({ writer }) => {\n    writer.reasoning(\n      \"The tokens live in app/globals.css as oklch() values, with a light block under :root and a dark block under .dark. I should confirm what is there before answering, then explain the @theme inline mapping that turns a token into a Tailwind utility.\"\n    )\n    writer\n      .tool(\"readFile\", { input: { path: \"app/globals.css\" } })\n      .sleep(900)\n      .output({\n        path: \"app/globals.css\",\n        lines: 212,\n        tokens: [\"--background\", \"--foreground\", \"--primary\", \"--muted\"],\n        themeInline: true,\n      })\n    writer.text(`Add the token in three places in \\`app/globals.css\\`: the light block, the dark block, and the Tailwind mapping.\n\n\\`\\`\\`css\n:root {\n  --success: oklch(0.72 0.19 150);\n}\n\n.dark {\n  --success: oklch(0.8 0.17 150);\n}\n\n@theme inline {\n  --color-success: var(--success);\n}\n\\`\\`\\`\n\nThen:\n\n- Use it through semantic utilities such as \\`bg-success\\` or \\`text-success\\`, never a palette class.\n- Run \\`pnpm registry:sync\\` so the \\`base\\` registry item picks up the new variable.\n- Add it to both blocks even if the values match, so dark mode never falls back to the light value.`)\n  })\n  .user(\"Can you give me the short version?\")\n  .sleep(400)\n  .assistant(\n    \"Define it under `:root` and `.dark`, map it in `@theme inline` as `--color-<name>`, then run `pnpm registry:sync`.\"\n  )\n\n/**\n * Transport for `useChat`. Streams the scripted assistant turns; once the\n * script is used up it answers with the fallback instead of throwing.\n */\nexport const transport = demoConversation.transport({\n  delayMs: 20,\n  fallback:\n    \"That is the end of the scripted demo. Swap `transport` for your own API route (`DefaultChatTransport`) to keep chatting.\",\n})\n\n/** The scripted opening question, offered as a one-click suggestion. */\nexport const demoSuggestions = demoConversation.get(1).map(getMessageText)\n",
      "type": "registry:lib",
      "target": "lib/demo-conversation.ts"
    },
    {
      "path": "registry/blocks/chat/page.tsx",
      "content": "\"use client\"\n\nimport { useChat } from \"@ai-sdk/react\"\nimport { Chat } from \"@/registry/blocks/chat/components/blocks/chat\"\nimport {\n  demoSuggestions,\n  transport,\n} from \"@/registry/blocks/chat/lib/demo-conversation\"\n\nexport default function ChatPage() {\n  const { error, messages, regenerate, sendMessage, status, stop } = useChat({\n    transport,\n  })\n\n  return (\n    <main className=\"mx-auto flex h-svh w-full max-w-3xl flex-col p-4\">\n      <Chat\n        error={error}\n        messages={messages}\n        onRetry={regenerate}\n        onStop={stop}\n        onSubmit={({ text, files }) => {\n          // Not returned: it settles only once the answer has streamed.\n          void sendMessage({ text, files })\n        }}\n        status={status}\n        suggestions={demoSuggestions}\n      />\n    </main>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/chat/page.tsx"
    }
  ],
  "docs": "Built for the Base UI styles (base-nova): it composes the base-nova wrappers from components/ui and the @uifiles AI items, which rely on Base UI's render prop and event signatures, so it does not work on Radix or React Aria styles. The page installs at app/chat/page.tsx and streams a scripted conversation through @shadcn/helpers, so it works without an API key. To connect a model, replace `transport` in the page with `new DefaultChatTransport({ api: \"/api/chat\" })` from `ai` and add a route that returns `result.toUIMessageStreamResponse()`. Pass `error` and `onRetry={regenerate}` from `useChat` so a failed request renders a row with its message and a Retry button; when `status` is \"error\" and no `error` is given the row still appears with a generic \"Something went wrong.\" (Retry only when `onRetry` is passed), and a rejected `onRetry` (`regenerate` rejects when there is nothing to regenerate) is reported through `console.error`, never as an unhandled rejection; after an error the composer shows a plain Submit again. While a response is in flight, Enter, the submit button and a programmatic `form.requestSubmit()` submit nothing and the draft and attachments are kept (ChatComposer returns `false` from `onSubmit`); if you pass `onStop` to Chat or ChatComposer (the page passes `stop` from `useChat`) the submit button becomes a Stop button that calls it, and without `onStop` it keeps the Submit name and a press does nothing. The page does not return `sendMessage`'s promise from `onSubmit` (it settles only once the answer has streamed) and ChatComposer does not wait for it either, so sent attachments leave the composer as soon as the message is sent and a file attached while an answer streams stays. If your `onSubmit` returns a promise that rejects, from the composer or a suggestion click, the rejection is reported through `console.error`, never as an unhandled rejection; if it throws synchronously the error is reported the same way and the composer keeps the draft and attachments. Once the chat is no longer generating (after Stop, or a stream that ended early) parts still marked streaming or running are shown settled, and a tool call left in input-available reads Pending; a tool call whose input has not started shows the Tool item's \"No input yet\" placeholder. Approval-requested tool calls are shown but not wired: compose @uifiles/confirmation with `addToolApprovalResponse` from `useChat` to add human-in-the-loop approvals. Chat fills its parent: render it inside a height-constrained container (the page uses h-svh). Suggestions are submitted as-is when clicked. CSS: the CLI adds @uifiles/response's stylesheet lines to your globals.css with this block: `@import \"streamdown/styles.css\"`, `@import \"katex/dist/katex.min.css\"` (without it every formula renders twice) and the `.katex-display` overflow rule. Add `@source \"../node_modules/streamdown/dist/*.js\"` yourself, next to your `@import \"tailwindcss\"` and with the path relative to your CSS file, so Streamdown's Tailwind classes compile. See @uifiles/response for what each line does.",
  "categories": [
    "ai",
    "chat",
    "block"
  ],
  "type": "registry:block"
}