{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-chat",
  "title": "AI Chat",
  "description": "Model selection, streamed task progress, file diffs, and a message composer.",
  "dependencies": [
    "lucide-react@^1.33.0"
  ],
  "registryDependencies": [
    "https://components.exe.xyz/r/agent-progress.json",
    "https://components.exe.xyz/r/app-shell.json",
    "https://components.exe.xyz/r/avatar.json",
    "https://components.exe.xyz/r/button.json",
    "https://components.exe.xyz/r/chip.json",
    "https://components.exe.xyz/r/dropdown-menu.json",
    "https://components.exe.xyz/r/icon-button.json",
    "https://components.exe.xyz/r/kbd.json",
    "https://components.exe.xyz/r/sidebar.json",
    "https://components.exe.xyz/r/tabs.json",
    "https://components.exe.xyz/r/textarea.json",
    "https://components.exe.xyz/r/tooltip.json",
    "https://components.exe.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/templates/ai-chat.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport type { ReactNode } from \"react\"\nimport {\n  GitBranch,\n  Menu,\n  Mic,\n  Send,\n  Sparkles,\n  ThumbsDown,\n  ThumbsUp,\n} from \"lucide-react\"\n\nimport { AgentProgress } from \"@/components/blocks/agent-progress\"\nimport { Sidebar } from \"@/components/blocks/sidebar\"\nimport { AppShell } from \"@/components/templates/app-shell\"\nimport type { SidebarRepository } from \"@/components/blocks/sidebar\"\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\"\nimport { Button } from \"@/components/ui/button\"\nimport { Chip } from \"@/components/ui/chip\"\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { IconButton } from \"@/components/ui/icon-button\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nexport type ChatMessage = {\n  id: string\n  role: \"user\" | \"assistant\"\n  content: ReactNode\n}\n\nexport type ChatFile = {\n  path: string\n  additions: number\n  deletions: number\n  status: \"new\" | \"modified\" | \"deleted\"\n}\n\nconst DEFAULT_REPOS: SidebarRepository[] = [\n  {\n    id: \"sea\",\n    name: \"project-sea\",\n    chats: [\n      { id: \"c1\", title: \"Dark mode tokens\", relativeTime: \"2m\" },\n      { id: \"c2\", title: \"Theme toggle\", relativeTime: \"1h\" },\n      { id: \"c3\", title: \"Canvas grid\", relativeTime: \"1d\" },\n    ],\n  },\n  {\n    id: \"ink\",\n    name: \"ink-kit\",\n    chats: [{ id: \"c4\", title: \"Chip restyle\", relativeTime: \"3d\" }],\n  },\n]\n\nconst DEFAULT_FILES: ChatFile[] = [\n  {\n    path: \"app/components/button.tsx\",\n    additions: 74,\n    deletions: 0,\n    status: \"new\",\n  },\n  {\n    path: \"src/styles/theme.css\",\n    additions: 32,\n    deletions: 11,\n    status: \"modified\",\n  },\n  {\n    path: \"src/components/ui/theme-toggle.tsx\",\n    additions: 28,\n    deletions: 0,\n    status: \"new\",\n  },\n  { path: \"src/styles.css\", additions: 12, deletions: 8, status: \"modified\" },\n  {\n    path: \"src/components/docs/shell.tsx\",\n    additions: 6,\n    deletions: 4,\n    status: \"modified\",\n  },\n  { path: \"src/lib/theme.tsx\", additions: 4, deletions: 0, status: \"modified\" },\n]\n\nconst DEFAULT_MODELS = [\"Fable 5\", \"Fable 5 Mini\", \"Forge 32\"]\n\nfunction ContextRing({ percent }: { percent: number }) {\n  const r = 7\n  const circ = 2 * Math.PI * r\n  return (\n    <svg viewBox=\"0 0 20 20\" className=\"size-5\" aria-hidden>\n      <circle\n        cx=\"10\"\n        cy=\"10\"\n        r={r}\n        fill=\"none\"\n        stroke=\"var(--muted)\"\n        strokeWidth=\"2.5\"\n      />\n      <circle\n        cx=\"10\"\n        cy=\"10\"\n        r={r}\n        fill=\"none\"\n        stroke=\"var(--chart-1)\"\n        strokeWidth=\"2.5\"\n        strokeDasharray={circ}\n        strokeDashoffset={circ * (1 - percent / 100)}\n        transform=\"rotate(-90 10 10)\"\n      />\n    </svg>\n  )\n}\n\nexport function AiChatTemplate({\n  repos = DEFAULT_REPOS,\n  messages,\n  model: modelProp = \"Fable 5\",\n  models = DEFAULT_MODELS,\n  onModelChange,\n  onSend,\n  files = DEFAULT_FILES,\n}: {\n  repos?: SidebarRepository[]\n  messages?: ChatMessage[]\n  model?: string\n  models?: string[]\n  onModelChange?: (id: string) => void\n  onSend?: (text: string) => void\n  files?: ChatFile[]\n} = {}) {\n  const [mobileOpen, setMobileOpen] = useState(false)\n  const [collapsed, setCollapsed] = useState(false)\n  const [model, setModel] = useState(modelProp)\n  const [draft, setDraft] = useState(\"\")\n  const [running, setRunning] = useState(messages == null)\n  const [selectedFile, setSelectedFile] = useState(files[0]?.path ?? \"\")\n  const [feedback, setFeedback] = useState<Record<string, \"up\" | \"down\">>({})\n  const [thread, setThread] = useState<ChatMessage[]>(\n    messages ?? [\n      {\n        id: \"u1\",\n        role: \"user\",\n        content:\n          \"update our color tokens for dark mode and add a reusable theme toggle…\",\n      },\n      {\n        id: \"a1\",\n        role: \"assistant\",\n        content: (\n          <div className=\"flex flex-col gap-3\">\n            <p>\n              I'll restyle the Harbor tokens for dark mode, then register a\n              reusable theme toggle. Working on{\" \"}\n              <Chip variant=\"outline\">src/styles/theme.css</Chip> and{\" \"}\n              <Chip variant=\"outline\">theme-toggle.tsx</Chip>.\n            </p>\n            <AgentProgress\n              steps={[\n                \"Read project files\",\n                \"Update light mode tokens\",\n                \"Implement dark mode tokens\",\n                \"Register the theme toggle\",\n                \"Run lint and production build\",\n              ]}\n              onFinished={() => setRunning(false)}\n            />\n          </div>\n        ),\n      },\n    ]\n  )\n\n  const additions = files.reduce((sum, file) => sum + file.additions, 0)\n  const deletions = files.reduce((sum, file) => sum + file.deletions, 0)\n\n  function send() {\n    const text = draft.trim()\n    if (!text || running) return\n    const next: ChatMessage = {\n      id: `u-${thread.length}`,\n      role: \"user\",\n      content: text,\n    }\n    setThread((current) => [...current, next])\n    setDraft(\"\")\n    onSend?.(text)\n  }\n\n  return (\n    <AppShell>\n      <a\n        href=\"#thread\"\n        className=\"sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-20 focus:bg-card focus:px-2 focus:py-1\"\n      >\n        Skip to thread\n      </a>\n      <Sidebar\n        variant=\"agent\"\n        collapsed={collapsed}\n        onCollapsedChange={setCollapsed}\n        mobileOpen={mobileOpen}\n        onMobileOpenChange={setMobileOpen}\n        items={[\n          {\n            id: \"agents\",\n            href: \"#thread\",\n            label: \"Agents\",\n            icon: Sparkles,\n            current: true,\n          },\n        ]}\n        user={{\n          name: \"Noah Reyes\",\n          email: \"noah@harbor.dev\",\n          initials: \"NR\",\n        }}\n        repositories={repos}\n        agentPrimaryAction={{\n          label: \"New thread\",\n          onPress: () => {\n            setThread([])\n            setDraft(\"\")\n            setRunning(false)\n            setFeedback({})\n          },\n        }}\n        agentSecondaryAction={{\n          label: \"Review changes\",\n          href: \"#changes-panel\",\n        }}\n        plan={{\n          name: \"Studio plan\",\n          description: \"12k tokens left this cycle\",\n        }}\n      />\n      <div className=\"flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row\">\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <header className=\"flex items-center gap-2 border-b border-border px-3 py-2\">\n            <IconButton\n              icon={Menu}\n              size=\"small\"\n              variant=\"ghost\"\n              aria-label=\"Open menu\"\n              className=\"md:hidden\"\n              onClick={() => setMobileOpen(true)}\n            />\n            <p className=\"min-w-0 flex-1 truncate text-sm\">Dark mode tokens</p>\n            <Chip variant=\"outline\">project-sea</Chip>\n          </header>\n          <main\n            id=\"thread\"\n            className=\"flex min-h-0 flex-1 flex-col overflow-auto px-4 py-4\"\n          >\n            <ol className=\"mx-auto flex w-full max-w-2xl flex-col gap-4\">\n              {thread.map((message) => (\n                <li key={message.id} className=\"flex gap-3\">\n                  <Avatar size=\"sm\">\n                    <AvatarFallback>\n                      {message.role === \"user\" ? \"ME\" : \"F5\"}\n                    </AvatarFallback>\n                  </Avatar>\n                  <div className=\"min-w-0 flex-1\">\n                    <p className=\"font-mono text-[11px] text-muted-foreground\">\n                      {message.role === \"user\" ? \"You\" : \"Fable 5\"}\n                    </p>\n                    <div className=\"mt-1 text-sm\">{message.content}</div>\n                    {message.role === \"assistant\" ? (\n                      <div className=\"mt-2 flex gap-1\">\n                        <IconButton\n                          icon={ThumbsUp}\n                          size=\"small\"\n                          variant=\"ghost\"\n                          aria-label=\"Helpful\"\n                          aria-pressed={feedback[message.id] === \"up\"}\n                          onClick={() =>\n                            setFeedback((current) => ({\n                              ...current,\n                              [message.id]: \"up\",\n                            }))\n                          }\n                        />\n                        <IconButton\n                          icon={ThumbsDown}\n                          size=\"small\"\n                          variant=\"ghost\"\n                          aria-label=\"Not helpful\"\n                          aria-pressed={feedback[message.id] === \"down\"}\n                          onClick={() =>\n                            setFeedback((current) => ({\n                              ...current,\n                              [message.id]: \"down\",\n                            }))\n                          }\n                        />\n                      </div>\n                    ) : null}\n                  </div>\n                </li>\n              ))}\n            </ol>\n          </main>\n          <form\n            className=\"border-t border-border px-3 py-3\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              send()\n            }}\n          >\n            <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-2 rounded-[2px] border border-border bg-card p-2\">\n              <Textarea\n                value={draft}\n                onChange={(event) => setDraft(event.target.value)}\n                onKeyDown={(event) => {\n                  if (\n                    event.key === \"Enter\" &&\n                    (event.metaKey || event.ctrlKey)\n                  ) {\n                    event.preventDefault()\n                    send()\n                  }\n                }}\n                placeholder=\"Ask Fable to edit the Harbor tokens…\"\n                rows={3}\n                disabled={running}\n                className=\"min-h-16 rounded-[2px] border-0 shadow-none focus-visible:ring-0\"\n              />\n              <div className=\"flex flex-wrap items-center gap-2\">\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <Button type=\"button\" size=\"sm\" variant=\"outline\">\n                      {model}\n                    </Button>\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent align=\"start\">\n                    <DropdownMenuLabel>Model</DropdownMenuLabel>\n                    <DropdownMenuRadioGroup\n                      value={model}\n                      onValueChange={(value) => {\n                        setModel(value)\n                        onModelChange?.(value)\n                      }}\n                    >\n                      {models.map((item) => (\n                        <DropdownMenuRadioItem key={item} value={item}>\n                          {item}\n                        </DropdownMenuRadioItem>\n                      ))}\n                    </DropdownMenuRadioGroup>\n                  </DropdownMenuContent>\n                </DropdownMenu>\n                <span className=\"ml-auto flex items-center gap-1 text-xs text-muted-foreground\">\n                  Send\n                  <Kbd>⌘</Kbd>\n                  <Kbd>Enter</Kbd>\n                </span>\n                <Tooltip>\n                  <TooltipTrigger asChild>\n                    <IconButton\n                      icon={Mic}\n                      size=\"small\"\n                      variant=\"ghost\"\n                      aria-label=\"Dictate\"\n                      disabled={running}\n                      onClick={() =>\n                        setDraft((current) =>\n                          `${current.trim()} Check the focus states too.`.trim()\n                        )\n                      }\n                    />\n                  </TooltipTrigger>\n                  <TooltipContent>Dictate</TooltipContent>\n                </Tooltip>\n                <IconButton\n                  icon={Send}\n                  size=\"small\"\n                  variant=\"secondary\"\n                  aria-label=\"Send\"\n                  disabled={running || draft.trim().length === 0}\n                  onClick={send}\n                />\n              </div>\n            </div>\n          </form>\n          <footer className=\"flex flex-wrap items-center gap-3 border-t border-border px-3 py-1.5 font-mono text-[11px] text-muted-foreground\">\n            <span className=\"inline-flex items-center gap-1.5\">\n              <ContextRing percent={57} />\n              Context 57%\n            </span>\n            <span className=\"inline-flex items-center gap-1\">\n              <GitBranch className=\"size-3\" />\n              Main / project-sea\n            </span>\n            <span>Agent 57%</span>\n          </footer>\n        </div>\n        <aside\n          id=\"changes-panel\"\n          className=\"flex min-h-64 w-full flex-col border-t border-border bg-card lg:w-80 lg:border-t-0 lg:border-l\"\n          aria-label=\"Changes\"\n        >\n          <Tabs\n            defaultValue=\"changes\"\n            className=\"flex min-h-0 flex-1 flex-col gap-0\"\n          >\n            <TabsList className=\"w-full rounded-none border-b border-border bg-transparent p-0\">\n              <TabsTrigger value=\"changes\" className=\"flex-1 rounded-none\">\n                Changes\n              </TabsTrigger>\n              <TabsTrigger value=\"browser\" className=\"flex-1 rounded-none\">\n                Browser\n              </TabsTrigger>\n            </TabsList>\n            <TabsContent\n              value=\"changes\"\n              className=\"min-h-0 flex-1 overflow-auto p-0\"\n            >\n              <p className=\"px-3 py-2 font-mono text-[11px] text-muted-foreground\">\n                12 uncommitted · +{additions} −{deletions}\n              </p>\n              <ul\n                role=\"listbox\"\n                aria-label=\"Changed files\"\n                className=\"flex flex-col\"\n              >\n                {files.map((file) => (\n                  <li key={file.path}>\n                    <button\n                      type=\"button\"\n                      role=\"option\"\n                      aria-selected={selectedFile === file.path}\n                      onClick={() => setSelectedFile(file.path)}\n                      className={cn(\n                        \"flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-muted\",\n                        selectedFile === file.path && \"bg-muted\"\n                      )}\n                    >\n                      <span className=\"min-w-0 flex-1 truncate font-mono text-[11px]\">\n                        {file.path}\n                      </span>\n                      <span className=\"font-mono text-[11px] text-[var(--chart-2)]\">\n                        +{file.additions}\n                      </span>\n                      {file.deletions > 0 ? (\n                        <span className=\"font-mono text-[11px] text-destructive\">\n                          −{file.deletions}\n                        </span>\n                      ) : null}\n                      <Chip variant=\"outline\">\n                        {file.status === \"new\" ? \"New\" : file.status}\n                      </Chip>\n                    </button>\n                  </li>\n                ))}\n              </ul>\n              <pre\n                tabIndex={0}\n                className=\"border-t border-border p-3 font-mono text-[11px] leading-5 text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50\"\n              >\n                {`// ${selectedFile || \"Select a changed file\"}\nexport function Button() {\n  return (\n    <button className=\"rounded-[2px] bg-primary\">\n      Press\n    </button>\n  )\n}`}\n              </pre>\n            </TabsContent>\n            <TabsContent value=\"browser\" className=\"p-3\">\n              <div className=\"overflow-hidden rounded-[2px] border border-border bg-background\">\n                <div className=\"flex items-center gap-1 border-b border-border px-2 py-1.5\">\n                  <span className=\"size-2 rounded-full bg-destructive/60\" />\n                  <span className=\"size-2 rounded-full bg-amber-500/60\" />\n                  <span className=\"size-2 rounded-full bg-primary/60\" />\n                  <span className=\"ml-2 truncate font-mono text-[9px] text-muted-foreground\">\n                    localhost:3000/settings/appearance\n                  </span>\n                </div>\n                <div className=\"space-y-3 p-3\">\n                  <p className=\"text-xs font-medium\">Appearance</p>\n                  <div className=\"grid grid-cols-3 gap-2\">\n                    {[\"Ground\", \"Ink\", \"Signal\"].map((token, index) => (\n                      <div\n                        key={token}\n                        className=\"rounded-[2px] border border-border p-2\"\n                      >\n                        <span\n                          className={cn(\n                            \"block h-8 rounded-[2px]\",\n                            index === 0\n                              ? \"bg-background\"\n                              : index === 1\n                                ? \"bg-foreground\"\n                                : \"bg-primary\"\n                          )}\n                        />\n                        <p className=\"mt-1 font-mono text-[8px] text-muted-foreground\">\n                          {token}\n                        </p>\n                      </div>\n                    ))}\n                  </div>\n                  <Button type=\"button\" size=\"sm\" className=\"w-full\">\n                    Save theme\n                  </Button>\n                </div>\n              </div>\n            </TabsContent>\n          </Tabs>\n        </aside>\n      </div>\n    </AppShell>\n  )\n}\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}