{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-chat-3",
  "title": "Live Chat With Typing Indicator",
  "description": "AI chat that replies live, showing a shimmering typing indicator, suggestion chips, and a textarea composer with keyboard shortcut hints.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "avatar",
    "bubble",
    "button",
    "card",
    "kbd",
    "marker",
    "message",
    "message-scroller",
    "separator",
    "textarea"
  ],
  "files": [
    {
      "path": "registry/blocks/ai-chat/3/ai-chat-block.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Bubble, BubbleContent } from \"@/components/ui/bubble\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardFooter, CardHeader } from \"@/components/ui/card\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\nimport { Marker, MarkerContent } from \"@/components/ui/marker\"\nimport { Message, MessageAvatar, 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 { Separator } from \"@/components/ui/separator\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { cn } from \"@/lib/utils\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\ntype MessageRole = \"user\" | \"assistant\"\n\ninterface ChatMessage {\n  id: number\n  role: MessageRole\n  content: string\n}\n\nconst initialMessages: ChatMessage[] = [\n  {\n    id: 1,\n    role: \"assistant\",\n    content:\n      \"Hi, I'm Acme Copilot. Ask me to draft, summarise, or plan something and I'll get to work.\",\n  },\n  {\n    id: 2,\n    role: \"user\",\n    content: \"What can you help me with on the Acme dashboard?\",\n  },\n  {\n    id: 3,\n    role: \"assistant\",\n    content:\n      \"Plenty. I can surface revenue trends, draft customer replies, generate release notes, and explain any metric you click on. Want me to start with a quick weekly summary?\",\n  },\n]\n\nconst cannedReplies: string[] = [\n  \"Got it. Here's a quick take: your weekly active users are up 12% and trial conversion held steady at 4.8%. Want me to break this down by plan tier?\",\n  \"Done. I drafted three subject-line variants and a 90-word body for the launch email, each tuned for a slightly different audience. I can refine the tone if you'd like.\",\n  \"Sure thing. The top driver this week was the Acme Pro upgrade flow (+$18.4k). I can pull the full attribution report whenever you're ready.\",\n  \"Here's a starting outline with five sections and suggested owners. Let me know which part you'd like me to expand first.\",\n]\n\nconst suggestions: string[] = [\n  \"Summarise This Week\",\n  \"Draft A Launch Email\",\n  \"Explain Churn Rate\",\n]\n\nexport default function AiChatBlock() {\n  const [messages, setMessages] = useState<ChatMessage[]>(initialMessages)\n  const [draft, setDraft] = useState(\"\")\n  const [isTyping, setIsTyping] = useState(false)\n  const replyIndex = useRef(0)\n  const nextId = useRef(initialMessages.length + 1)\n  const replyTimers = useRef<ReturnType<typeof setTimeout>[]>([])\n\n  useEffect(() => {\n    const timers = replyTimers.current\n    return () => timers.forEach(clearTimeout)\n  }, [])\n\n  function send(text: string) {\n    const trimmed = text.trim()\n    if (!trimmed || isTyping) return\n\n    const userMessage: ChatMessage = {\n      id: nextId.current++,\n      role: \"user\",\n      content: trimmed,\n    }\n    setMessages((prev) => [...prev, userMessage])\n    setDraft(\"\")\n    setIsTyping(true)\n\n    const reply = cannedReplies[replyIndex.current % cannedReplies.length]\n    replyIndex.current += 1\n\n    const timer = setTimeout(() => {\n      setMessages((prev) => [\n        ...prev,\n        { id: nextId.current++, role: \"assistant\", content: reply },\n      ])\n      setIsTyping(false)\n    }, 1400)\n    replyTimers.current.push(timer)\n  }\n\n  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {\n    event.preventDefault()\n    send(draft)\n  }\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {\n    if (event.key === \"Enter\" && !event.shiftKey) {\n      event.preventDefault()\n      send(draft)\n    }\n  }\n\n  return (\n    <section className=\"flex w-full items-center justify-center bg-muted/30 px-6 py-16 text-foreground\">\n      <MessageScrollerProvider autoScroll defaultScrollPosition=\"end\">\n        <Card className=\"flex h-[80vh] max-h-[640px] w-full max-w-2xl flex-col overflow-hidden border border-border shadow-sm\">\n          <CardHeader className=\"px-5 py-4\">\n            <div className=\"flex items-center gap-3\">\n              <span className=\"flex size-9 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground\">\n                <IconPlaceholder\n                  lucide=\"Sparkles\"\n                  tabler=\"IconSparkles\"\n                  hugeicons=\"SparklesIcon\"\n                  phosphor=\"Sparkle\"\n                  remixicon=\"RiSparkling2Fill\"\n                  className=\"size-5\"\n                  aria-hidden=\"true\"\n                />\n              </span>\n              <div className=\"flex flex-col gap-0.5\">\n                <span className=\"text-sm leading-none font-semibold tracking-tight\">\n                  Acme Copilot\n                </span>\n                <span className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                  <span className=\"inline-block size-1.5 rounded-full bg-primary\" />\n                  {isTyping ? \"Typing…\" : \"Online\"}\n                </span>\n              </div>\n            </div>\n          </CardHeader>\n\n          <Separator />\n\n          <CardContent className=\"min-h-0 flex-1 overflow-hidden p-0\">\n            <MessageScroller className=\"size-full\">\n              <MessageScrollerViewport>\n                <MessageScrollerContent className=\"gap-5 px-5 py-5\">\n                  {messages.map((msg) => (\n                    <MessageScrollerItem\n                      key={msg.id}\n                      messageId={String(msg.id)}\n                      scrollAnchor={msg.role === \"user\"}\n                    >\n                      <Message align={msg.role === \"user\" ? \"end\" : \"start\"}>\n                        <MessageAvatar>\n                          <Avatar className=\"size-8 border border-border\">\n                            {msg.role === \"user\" ? (\n                              <>\n                                <AvatarImage\n                                  src=\"https://i.pravatar.cc/150?img=12\"\n                                  alt=\"You\"\n                                  className=\"grayscale\"\n                                />\n                                <AvatarFallback className=\"text-xs font-medium\">\n                                  JD\n                                </AvatarFallback>\n                              </>\n                            ) : (\n                              <AvatarFallback className=\"bg-primary text-primary-foreground\">\n                                <IconPlaceholder\n                                  lucide=\"Sparkles\"\n                                  tabler=\"IconSparkles\"\n                                  hugeicons=\"SparklesIcon\"\n                                  phosphor=\"Sparkle\"\n                                  remixicon=\"RiSparkling2Fill\"\n                                  className=\"size-4\"\n                                  aria-hidden=\"true\"\n                                />\n                              </AvatarFallback>\n                            )}\n                          </Avatar>\n                        </MessageAvatar>\n                        <MessageContent>\n                          <Bubble\n                            variant={msg.role === \"user\" ? \"default\" : \"muted\"}\n                          >\n                            <BubbleContent className=\"text-sm whitespace-pre-line\">\n                              {msg.content}\n                            </BubbleContent>\n                          </Bubble>\n                        </MessageContent>\n                      </Message>\n                    </MessageScrollerItem>\n                  ))}\n\n                  {isTyping && (\n                    <Marker role=\"status\">\n                      <MarkerContent className=\"shimmer\">\n                        Acme Copilot is typing...\n                      </MarkerContent>\n                    </Marker>\n                  )}\n                </MessageScrollerContent>\n              </MessageScrollerViewport>\n              <MessageScrollerButton />\n            </MessageScroller>\n          </CardContent>\n\n          <CardFooter className=\"flex flex-col gap-3 px-5 py-4\">\n            <div className=\"flex w-full flex-wrap gap-2\">\n              {suggestions.map((s) => (\n                <button\n                  key={s}\n                  type=\"button\"\n                  onClick={() => send(s)}\n                  disabled={isTyping}\n                  className={cn(\n                    \"rounded-md border border-border bg-background px-2.5 py-1 text-xs text-muted-foreground transition-colors\",\n                    \"hover:bg-muted hover:text-foreground\",\n                    \"disabled:cursor-not-allowed disabled:opacity-50\"\n                  )}\n                >\n                  {s}\n                </button>\n              ))}\n            </div>\n\n            <form\n              onSubmit={handleSubmit}\n              className=\"flex w-full items-end gap-2\"\n            >\n              <Textarea\n                value={draft}\n                onChange={(e) => setDraft(e.target.value)}\n                onKeyDown={handleKeyDown}\n                rows={1}\n                placeholder=\"Message Acme Copilot…\"\n                aria-label=\"Message Acme Copilot\"\n                className=\"max-h-32 min-h-10 flex-1 resize-none text-sm\"\n              />\n              <Button\n                type=\"submit\"\n                size=\"icon\"\n                disabled={!draft.trim() || isTyping}\n                className=\"size-10 shrink-0\"\n                aria-label=\"Send message\"\n              >\n                <IconPlaceholder\n                  lucide=\"Send\"\n                  tabler=\"IconSend\"\n                  hugeicons=\"SentIcon\"\n                  phosphor=\"PaperPlane\"\n                  remixicon=\"RiSendPlane2Fill\"\n                  className=\"size-4\"\n                  aria-hidden=\"true\"\n                />\n              </Button>\n            </form>\n\n            <div className=\"flex w-full flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-muted-foreground\">\n              <span className=\"flex items-center gap-1.5\">\n                <Kbd>Enter</Kbd>\n                Send\n              </span>\n              <span className=\"flex items-center gap-1.5\">\n                <KbdGroup>\n                  <Kbd>Shift</Kbd>\n                  <Kbd>Enter</Kbd>\n                </KbdGroup>\n                New Line\n              </span>\n            </div>\n          </CardFooter>\n        </Card>\n      </MessageScrollerProvider>\n    </section>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "height": "776px",
    "tier": "free"
  },
  "categories": [
    "ai-chat"
  ],
  "type": "registry:block"
}