{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "foundation-chat",
  "title": "Foundation Chat",
  "description": "A production-ready AI Gateway chat shell for Next.js, AI SDK 6, AI Elements, and shadcn/ui.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "ai",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "button-group",
    "card",
    "separator",
    "textarea",
    "tooltip",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json"
  ],
  "files": [
    {
      "path": "registry/foundation-chat/app/demos/foundation-chat/page.tsx",
      "content": "import { FoundationChatScreen } from \"@/components/foundation-chat/foundation-chat-screen\";\n\nexport default function FoundationChatPage() {\n  return <FoundationChatScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/foundation-chat/page.tsx"
    },
    {
      "path": "registry/foundation-chat/app/api/demos/foundation-chat/route.ts",
      "content": "import { handleFoundationChatRequest } from \"@/lib/foundation-chat/runtime\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleFoundationChatRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/foundation-chat/route.ts"
    },
    {
      "path": "registry/foundation-chat/components/foundation-chat/foundation-chat-screen.tsx",
      "content": "import { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { DemoWorkspaceShell } from \"@/components/demo-workspace-shell\";\n\nimport { getFoundationChatRuntimeState } from \"@/lib/foundation-chat/runtime\";\nimport { FoundationChatWorkspace } from \"@/components/foundation-chat/foundation-chat-workspace\";\n\nexport function FoundationChatScreen() {\n  const runtimeState = getFoundationChatRuntimeState();\n\n  return (\n    <TooltipProvider>\n      <DemoWorkspaceShell\n        badges={[runtimeState.statusLabel, runtimeState.chatModel]}\n        breadcrumbClassName=\"font-heading text-xs tracking-[0.16em]\"\n        breadcrumbTitle=\"Foundation Chat\"\n        headerFrame=\"card\"\n        summary=\"This first slot validates the shared route pattern, environment contract, and AI Elements workspace before the cookbook batches start branching into larger agents.\"\n        title=\"Production-ready AI Gateway chat shell for the rest of the demos\"\n      >\n        <FoundationChatWorkspace\n          chatModel={runtimeState.chatModel}\n          isChatAvailable={runtimeState.isChatAvailable}\n          nodeVersion={runtimeState.nodeVersion}\n          setupMessage={runtimeState.setupMessage}\n        />\n      </DemoWorkspaceShell>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/foundation-chat/foundation-chat-screen.tsx"
    },
    {
      "path": "registry/foundation-chat/components/foundation-chat/foundation-chat-workspace.tsx",
      "content": "\"use client\";\n\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport type { UIMessage } from \"ai\";\nimport { BotIcon, RefreshCwIcon, SquareIcon } from \"lucide-react\";\n\nimport {\n  ConversationErrorMessage,\n  useConversationErrorRetry,\n} from \"@/components/demo-chat/conversation-error-message\";\n\nimport { useFoundationChat } from \"./use-foundation-chat\";\n\nconst foundationChatSamplePrompts = [\n  \"Explain the minimum files this Foundation Chat demo needs in a fresh Next.js app.\",\n  \"Show how the AI Gateway key flows from env vars into the chat route.\",\n  \"Suggest the next demo slice to build after this baseline chat is working.\",\n] as const;\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nexport interface FoundationChatWorkspaceProps {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n}\n\nexport function FoundationChatWorkspace({\n  chatModel,\n  isChatAvailable,\n  nodeVersion,\n  setupMessage,\n}: FoundationChatWorkspaceProps) {\n  const {\n    clearError,\n    error,\n    hasMessages,\n    isBusy,\n    messages,\n    regenerate,\n    sendMessage,\n    status,\n    stop,\n  } = useFoundationChat();\n  const retryConversationError = useConversationErrorRetry({\n    clearError,\n    regenerate,\n  });\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_18rem]\">\n      <Card className=\"min-h-[70svh] gap-0 bg-background py-0 text-base text-foreground leading-normal lg:h-full lg:min-h-0\">\n        {isChatAvailable ? null : (\n          <>\n            <div className=\"px-4 py-3 text-muted-foreground text-xs/relaxed\">\n              {setupMessage}\n            </div>\n            <Separator />\n          </>\n        )}\n\n        <Conversation className=\"min-h-0\">\n          <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n            {hasMessages || error ? (\n              <>\n                {messages.map((message) => {\n                  const text = getTextContent(message);\n\n                  return (\n                    <Message from={message.role} key={message.id}>\n                      <MessageContent\n                        className={cn(\n                          message.role === \"assistant\"\n                            ? \"max-w-3xl\"\n                            : \"max-w-2xl\"\n                        )}\n                      >\n                        {text ? (\n                          <MessageResponse>{text}</MessageResponse>\n                        ) : (\n                          <p className=\"text-muted-foreground text-sm\">\n                            Waiting for visible output.\n                          </p>\n                        )}\n                      </MessageContent>\n                    </Message>\n                  );\n                })}\n                {error ? (\n                  <ConversationErrorMessage\n                    error={error}\n                    isRetryDisabled={isBusy || !isChatAvailable}\n                    onRetry={retryConversationError}\n                  />\n                ) : null}\n              </>\n            ) : (\n              <ConversationEmptyState\n                description=\"Ask about the project, the stack, or any capability you want to turn into the next demo.\"\n                icon={<BotIcon className=\"size-5\" />}\n                title=\"Foundation workspace is ready\"\n              />\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <Separator />\n        <div className=\"px-4 py-4\">\n          <div className=\"mx-auto w-full max-w-3xl\">\n            <PromptInput onSubmit={({ text }) => sendMessage({ text })}>\n              <PromptInputBody>\n                <PromptInputTextarea\n                  disabled={!isChatAvailable || isBusy}\n                  placeholder=\"Ask this demo to explain the stack or help shape the next agent.\"\n                />\n              </PromptInputBody>\n              <Separator className=\"mt-3\" />\n              <PromptInputFooter className=\"flex items-center justify-between gap-3 px-3 py-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <Badge variant=\"outline\">AI SDK 6</Badge>\n                  <Badge variant=\"outline\">Gateway</Badge>\n                  <Badge variant=\"outline\">{chatModel}</Badge>\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  {isBusy ? (\n                    <Button\n                      onClick={stop}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <SquareIcon className=\"size-3.5\" />\n                      Stop\n                    </Button>\n                  ) : null}\n                  {hasMessages ? (\n                    <Button\n                      onClick={() => regenerate()}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant=\"outline\"\n                    >\n                      <RefreshCwIcon className=\"size-3.5\" />\n                      Retry\n                    </Button>\n                  ) : null}\n                  <PromptInputSubmit\n                    disabled={!isChatAvailable}\n                    status={status}\n                  />\n                </div>\n              </PromptInputFooter>\n            </PromptInput>\n\n            {hasMessages ? null : (\n              <div className=\"mt-3 flex flex-wrap gap-2\">\n                {foundationChatSamplePrompts.map((prompt) => (\n                  <Button\n                    disabled={!isChatAvailable || isBusy}\n                    key={prompt}\n                    onClick={() => sendMessage({ text: prompt })}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    <BotIcon className=\"size-3.5\" />\n                    {prompt}\n                  </Button>\n                ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </Card>\n\n      <Card className=\"bg-background p-4 text-base text-foreground leading-normal lg:min-h-0 lg:overflow-y-auto\">\n        <div className=\"space-y-4\">\n          <div>\n            <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n              Runtime\n            </p>\n            <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n          </div>\n          <div>\n            <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n              Contract\n            </p>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              This slot proves the minimum copy-paste baseline: Next.js route,\n              AI Gateway provider wiring, AI Elements workspace, and explicit\n              setup errors.\n            </p>\n          </div>\n          <div>\n            <p className=\"font-heading text-muted-foreground text-xs uppercase tracking-[0.16em]\">\n              Demo role\n            </p>\n            <p className=\"mt-1 text-muted-foreground text-sm\">\n              Future demos can duplicate this slice and replace only feature\n              logic, route path, metadata, and model behavior.\n            </p>\n          </div>\n        </div>\n      </Card>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/foundation-chat/foundation-chat-workspace.tsx"
    },
    {
      "path": "registry/foundation-chat/components/foundation-chat/use-foundation-chat.ts",
      "content": "\"use client\";\n\nimport { useDemoChat } from \"@/components/demo-chat/use-demo-chat\";\n\nexport function useFoundationChat() {\n  return useDemoChat({ api: \"/api/demos/foundation-chat\" });\n}\n",
      "type": "registry:component",
      "target": "@components/foundation-chat/use-foundation-chat.ts"
    },
    {
      "path": "registry/foundation-chat/components/demo-chat/use-demo-chat.ts",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport { type ChatStatus, DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useState } from \"react\";\n\ninterface CreateDemoChatOptions {\n  api: string;\n}\n\ninterface UseDemoChatWithApiOptions extends CreateDemoChatOptions {\n  createChat?: never;\n}\n\ninterface UseDemoChatWithFactoryOptions<TMessage extends UIMessage> {\n  api?: never;\n  createChat: () => Chat<TMessage>;\n}\n\ntype UseDemoChatOptions<TMessage extends UIMessage> =\n  | UseDemoChatWithApiOptions\n  | UseDemoChatWithFactoryOptions<TMessage>;\n\nexport function createDemoChat<TMessage extends UIMessage = UIMessage>({\n  api,\n}: CreateDemoChatOptions) {\n  return new Chat<TMessage>({\n    transport: new DefaultChatTransport({\n      api,\n    }),\n  });\n}\n\nexport function isDemoChatBusyStatus(status: ChatStatus) {\n  return status === \"submitted\" || status === \"streaming\";\n}\n\nexport function useDemoChat<TMessage extends UIMessage = UIMessage>(\n  options: UseDemoChatOptions<TMessage>\n) {\n  const [chat] = useState(() => {\n    if (options.createChat) {\n      return options.createChat();\n    }\n\n    return createDemoChat<TMessage>({ api: options.api });\n  });\n  const controller = useChat({ chat });\n  const hasMessages = controller.messages.length > 0;\n  const isBusy = isDemoChatBusyStatus(controller.status);\n\n  return {\n    ...controller,\n    chat,\n    hasMessages,\n    isBusy,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/demo-chat/use-demo-chat.ts"
    },
    {
      "path": "registry/foundation-chat/components/demo-chat/conversation-error-message.tsx",
      "content": "\"use client\";\n\nimport {\n  Message,\n  MessageContent,\n} from \"@/components/ai-elements/message\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { RefreshCcwIcon } from \"lucide-react\";\nimport { useCallback } from \"react\";\n\ninterface ConversationErrorMessageProps {\n  className?: string;\n  error: Error | string;\n  isRetryDisabled?: boolean;\n  onRetry?: () => Promise<void> | void;\n  retryLabel?: string;\n  title?: string;\n}\n\ninterface UseConversationErrorRetryInput {\n  clearError: () => void;\n  regenerate: () => Promise<void>;\n}\n\nexport function useConversationErrorRetry({\n  clearError,\n  regenerate,\n}: UseConversationErrorRetryInput) {\n  return useCallback(async () => {\n    clearError();\n    await regenerate();\n  }, [clearError, regenerate]);\n}\n\nexport function ConversationErrorMessage({\n  className,\n  error,\n  isRetryDisabled = false,\n  retryLabel = \"Retry\",\n  onRetry,\n  title = \"Assistant response failed\",\n}: ConversationErrorMessageProps) {\n  const errorMessage = typeof error === \"string\" ? error : error.message;\n\n  return (\n    <Message from=\"assistant\">\n      <MessageContent\n        className={cn(\n          \"max-w-3xl border border-destructive/25 bg-destructive/5 px-4 py-3 text-destructive\",\n          className\n        )}\n      >\n        <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n          <div className=\"min-w-0\">\n            <p className=\"font-medium text-sm\">{title}</p>\n            <p className=\"mt-1 break-words text-xs/relaxed opacity-90\">\n              {errorMessage}\n            </p>\n          </div>\n          {onRetry ? (\n            <Button\n              className=\"self-start\"\n              disabled={isRetryDisabled}\n              onClick={() => {\n                void onRetry();\n              }}\n              size=\"sm\"\n              type=\"button\"\n              variant=\"destructive\"\n            >\n              <RefreshCcwIcon className=\"size-3.5\" />\n              {retryLabel}\n            </Button>\n          ) : null}\n        </div>\n      </MessageContent>\n    </Message>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-chat/conversation-error-message.tsx"
    },
    {
      "path": "registry/foundation-chat/components/demo-workspace-shell.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport type { ReactNode } from \"react\";\n\nimport { DemoBreadcrumb } from \"@/components/demo-breadcrumb\";\n\ntype DemoWorkspaceHeaderFrame = \"header\" | \"card\";\n\ninterface DemoWorkspaceShellProps {\n  badges?: readonly ReactNode[];\n  breadcrumbClassName?: string;\n  breadcrumbTitle?: string;\n  children: ReactNode;\n  contentClassName?: string;\n  headerClassName?: string;\n  headerFrame?: DemoWorkspaceHeaderFrame;\n  maxWidthClassName?: string;\n  summary: ReactNode;\n  summaryClassName?: string;\n  title: string;\n  titleClassName?: string;\n  workspaceClassName?: string | null;\n}\n\nconst defaultHeaderClassName =\n  \"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\";\n\nconst cardHeaderClassName =\n  \"grid gap-4 bg-background px-4 py-5 text-base text-foreground leading-normal md:grid-cols-[minmax(0,1fr)_auto] md:items-end\";\n\nexport function DemoWorkspaceShell({\n  badges = [],\n  breadcrumbClassName,\n  breadcrumbTitle,\n  children,\n  contentClassName,\n  headerClassName,\n  headerFrame = \"header\",\n  maxWidthClassName = \"max-w-7xl\",\n  summary,\n  summaryClassName = \"max-w-3xl\",\n  title,\n  titleClassName = \"max-w-3xl\",\n  workspaceClassName = \"lg:h-svh\",\n}: DemoWorkspaceShellProps) {\n  const headerContent = (\n    <>\n      <div className=\"space-y-2\">\n        <DemoBreadcrumb\n          className={breadcrumbClassName}\n          title={breadcrumbTitle ?? title}\n        />\n        <h1\n          className={cn(\"font-medium text-2xl tracking-tight\", titleClassName)}\n        >\n          {title}\n        </h1>\n        <p\n          className={cn(\n            \"text-muted-foreground text-sm/relaxed\",\n            summaryClassName\n          )}\n        >\n          {summary}\n        </p>\n      </div>\n\n      {badges.length > 0 ? (\n        <div className=\"flex flex-wrap items-center gap-2\">\n          {badges.map((badge, index) => (\n            <Badge key={String(index)} variant=\"outline\">\n              {badge}\n            </Badge>\n          ))}\n        </div>\n      ) : null}\n    </>\n  );\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div\n        className={cn(\n          \"mx-auto flex w-full flex-col gap-6 px-4 py-6 md:px-6\",\n          maxWidthClassName,\n          contentClassName\n        )}\n      >\n        {headerFrame === \"card\" ? (\n          <Card className={cn(cardHeaderClassName, headerClassName)}>\n            {headerContent}\n          </Card>\n        ) : (\n          <header className={cn(defaultHeaderClassName, headerClassName)}>\n            {headerContent}\n          </header>\n        )}\n\n        {workspaceClassName ? (\n          <div className={workspaceClassName}>{children}</div>\n        ) : (\n          children\n        )}\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-workspace-shell.tsx"
    },
    {
      "path": "registry/foundation-chat/components/demo-breadcrumb.tsx",
      "content": "import {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowLeft } from \"lucide-react\";\n\ninterface DemoBreadcrumbProps {\n  className?: string;\n  title: string;\n}\n\nexport function DemoBreadcrumb({ className, title }: DemoBreadcrumbProps) {\n  return (\n    <Breadcrumb>\n      <BreadcrumbList\n        className={cn(\n          \"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\",\n          className\n        )}\n      >\n        <BreadcrumbItem>\n          <BreadcrumbLink\n            aria-label=\"Back to demos\"\n            className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n            href=\"/\"\n          >\n            <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n            <span>Demo</span>\n          </BreadcrumbLink>\n        </BreadcrumbItem>\n        <BreadcrumbSeparator className=\"text-muted-foreground\">\n          /\n        </BreadcrumbSeparator>\n        <BreadcrumbItem>\n          <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n            {title}\n          </BreadcrumbPage>\n        </BreadcrumbItem>\n      </BreadcrumbList>\n    </Breadcrumb>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/demo-breadcrumb.tsx"
    },
    {
      "path": "registry/foundation-chat/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/foundation-chat/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/foundation-chat/lib/foundation-chat/env-source.ts",
      "content": "export function getFoundationChatAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/foundation-chat/env-source.ts"
    },
    {
      "path": "registry/foundation-chat/lib/foundation-chat/env.ts",
      "content": "import {\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n} from \"@/lib/ai-gateway/contract\";\nimport { getFoundationChatAppEnv } from \"./env-source\";\n\nexport const DEFAULT_FOUNDATION_CHAT_MODEL = \"openai/gpt-4.1-mini\";\n\nexport type FoundationChatEnv = AiGatewayEnvRecord;\n\nexport type FoundationChatConfig = AiGatewayContractConfig;\n\nexport type FoundationChatSetupState =\n  AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type FoundationChatGateway = ReturnType<\n  typeof createAiGatewayFromContract\n>;\n\nconst foundationChatContract = {\n  defaultChatModel: DEFAULT_FOUNDATION_CHAT_MODEL,\n  missingApiKeyError:\n    \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using foundation chat.\",\n  missingApiKeyIssue:\n    \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\",\n} as const;\n\nexport function readFoundationChatEnv(): FoundationChatEnv {\n  return getFoundationChatAppEnv();\n}\n\nexport function getFoundationChatConfig(\n  env: FoundationChatEnv = readFoundationChatEnv()\n): FoundationChatConfig {\n  return readAiGatewayContractConfig(env, foundationChatContract);\n}\n\nexport function getFoundationChatSetupState(\n  env: FoundationChatEnv = readFoundationChatEnv()\n): FoundationChatSetupState {\n  return buildAiGatewayContractSetupState(env, foundationChatContract);\n}\n\nexport function createFoundationChatGateway(\n  env: FoundationChatEnv = readFoundationChatEnv()\n): FoundationChatGateway {\n  return createAiGatewayFromContract(env, foundationChatContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/foundation-chat/env.ts"
    },
    {
      "path": "registry/foundation-chat/lib/foundation-chat/runtime.ts",
      "content": "import { convertToModelMessages, streamText, type UIMessage } from \"ai\";\n\nimport {\n  createFoundationChatGateway,\n  type FoundationChatEnv,\n  getFoundationChatConfig,\n  getFoundationChatSetupState,\n} from \"./env\";\n\nconst systemPrompt = [\n  \"You are the foundation chat demo in a production-ready agent demos monorepo.\",\n  \"Keep answers concise, direct, and useful to engineers evaluating the stack.\",\n  \"When a request is ambiguous, surface the assumption clearly and keep moving.\",\n].join(\" \");\nconst invalidMessagesError = 'Expected a JSON body with a \"messages\" array.';\n\ninterface FoundationChatRequestBody {\n  messages?: UIMessage[];\n}\n\nexport interface FoundationChatRuntimeState {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n}\n\nexport function getFoundationChatRuntimeState(\n  env?: FoundationChatEnv\n): FoundationChatRuntimeState {\n  const setup = getFoundationChatSetupState(env);\n\n  return {\n    chatModel: setup.config.chatModel,\n    isChatAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n  };\n}\n\nfunction readFoundationChatMessages(body: unknown): UIMessage[] {\n  const { messages } = (body ?? {}) as FoundationChatRequestBody;\n\n  if (!Array.isArray(messages)) {\n    throw new Error(invalidMessagesError);\n  }\n\n  return messages;\n}\n\nasync function streamFoundationChat(\n  messages: UIMessage[],\n  env?: FoundationChatEnv\n) {\n  const gateway = createFoundationChatGateway(env);\n  const { chatModel } = getFoundationChatConfig(env);\n\n  const result = streamText({\n    model: gateway(chatModel),\n    system: systemPrompt,\n    messages: await convertToModelMessages(messages),\n  });\n\n  return result.toUIMessageStreamResponse();\n}\n\nexport async function handleFoundationChatRequest(\n  request: Request,\n  env?: FoundationChatEnv\n) {\n  const runtimeState = getFoundationChatRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  const body = await request.json();\n\n  try {\n    return await streamFoundationChat(readFoundationChatMessages(body), env);\n  } catch (error) {\n    if (error instanceof Error && error.message === invalidMessagesError) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/foundation-chat/runtime.ts"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-4.1-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item depends on official shadcn registry primitives plus AI Elements conversation and message, writes the route files into the consumer app directory, and adds AI Gateway env vars to .env.local.",
  "type": "registry:block"
}
