{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-01",
  "type": "registry:block",
  "title": "Chat Basic",
  "description": "Chat Basic. AI chat interfaces with message history, prompt controls, and conversation navigation.",
  "author": "Ephraim Duncan",
  "registryDependencies": [],
  "dependencies": [
    "@base-ui/react@1.9.0",
    "@shadcn/react@0.3.1",
    "@tabler/icons-react@3.50.0",
    "class-variance-authority@0.7.1",
    "cn@0.2.6",
    "lucide-react@0.577.0"
  ],
  "files": [
    {
      "path": "content/components/chat/chat-01.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/chat-01.tsx",
      "content": "'use client';\n\nimport {\n  IconArrowUp,\n  IconCopy,\n  IconMicrophone,\n  IconPaperclip,\n  IconPhoto,\n  IconPlayerStopFilled,\n  IconPlus,\n  IconRefresh,\n  IconThumbDown,\n  IconThumbUp,\n  IconWorldSearch,\n} from '@tabler/icons-react';\nimport type React from 'react';\nimport { useState } from 'react';\nimport { Bubble, BubbleContent } from \"./shared/components/ui/bubble\";\nimport { Button } from \"./shared/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"./shared/components/ui/dropdown-menu\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"./shared/components/ui/input-group\";\nimport {\n  Message,\n  MessageContent,\n  MessageFooter,\n} from \"./shared/components/ui/message\";\nimport {\n  MessageScroller,\n  MessageScrollerButton,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerProvider,\n  MessageScrollerViewport,\n} from \"./shared/components/ui/message-scroller\";\nimport { Textarea } from \"./shared/components/ui/textarea\";\nimport { cn } from \"./shared/lib/utils\";\n\ntype Turn = {\n  id: string;\n  role: 'user' | 'assistant';\n  text: string;\n  streaming?: boolean;\n};\n\nconst thread: Turn[] = [\n  {\n    id: '1',\n    role: 'user',\n    text: 'Is it true that a day on Venus is longer than its year?',\n  },\n  {\n    id: '2',\n    role: 'assistant',\n    text: 'Yes. Venus takes 243 Earth days to rotate once, but only 225 days to orbit the Sun, so its day is longer than its year.\\n\\nIt also spins in the opposite direction to most planets, which means the Sun rises in the west there.',\n  },\n  { id: '3', role: 'user', text: 'How hot does it get on the surface?' },\n  {\n    id: '4',\n    role: 'assistant',\n    text: 'The surface sits around 465°C, hot enough to melt lead. The atmosphere traps so much heat that Venus stays hotter than Mercury, even though Mercury is much closer to the Sun.',\n  },\n];\n\nconst replies = [\n  'Good question. Venus has no moons and no rings, which makes it unusual among the inner planets. Most explanations point to its slow, retrograde spin: any moon it once had would have drifted inward and been pulled apart by tidal forces.\\n\\nMercury is the only other moonless planet in the solar system.',\n  'The clouds are mostly sulfuric acid droplets, sitting about 50 to 70 km above the surface. They reflect roughly 75% of incoming sunlight, which is why Venus is the brightest object in our sky after the Moon.\\n\\nBelow the clouds the air is almost entirely carbon dioxide, with pressure about 92 times that of Earth at sea level.',\n  'Several landers have made it down. The Soviet Venera 7 was the first to transmit from the surface in 1970, and Venera 13 survived for 127 minutes in 1982, long enough to send back the first color photos.\\n\\nNothing has lasted more than a couple of hours; the heat and pressure destroy electronics quickly.',\n];\n\nconst thinkDelay = 600;\nconst wordDelay = 35;\n\nconst easeOut = 'ease-[cubic-bezier(0.23,1,0.32,1)]';\nconst press = `${easeOut} transition-[scale,background-color] duration-150 active:scale-[0.96]`;\nconst enter = `${easeOut} motion-reduce:slide-in-from-bottom-0 fade-in slide-in-from-bottom-1 animate-in duration-200`;\nconst swap = `${easeOut} fade-in zoom-in-95 animate-in duration-150`;\n\nexport default function Chat01() {\n  const [turns, setTurns] = useState(thread);\n  const [draft, setDraft] = useState('');\n  const [listening, setListening] = useState(false);\n  const [replyIndex, setReplyIndex] = useState(0);\n  const [edit, setEdit] = useState<{ id: string; text: string } | null>(null);\n  const streaming = turns.some((t) => t.streaming);\n\n  const stream = (replyId: string) => {\n    const words = replies[replyIndex % replies.length].split(' ');\n    setReplyIndex(replyIndex + 1);\n    setTurns((prev) =>\n      prev.map((t) =>\n        t.id === replyId ? { ...t, text: '', streaming: true } : t\n      )\n    );\n    let shown = 0;\n    setTimeout(() => {\n      const timer = setInterval(() => {\n        shown += 1;\n        const done = shown >= words.length;\n        setTurns((prev) =>\n          prev.map((t) =>\n            t.id === replyId\n              ? {\n                  ...t,\n                  text: words.slice(0, shown).join(' '),\n                  streaming: !done,\n                }\n              : t\n          )\n        );\n        if (done) {\n          clearInterval(timer);\n        }\n      }, wordDelay);\n    }, thinkDelay);\n  };\n\n  const send = (e: React.FormEvent) => {\n    e.preventDefault();\n    const text = draft.trim();\n    if (!text || streaming) {\n      return;\n    }\n    const replyId = crypto.randomUUID();\n    setTurns([\n      ...turns,\n      { id: crypto.randomUUID(), role: 'user', text },\n      { id: replyId, role: 'assistant', text: '' },\n    ]);\n    setDraft('');\n    stream(replyId);\n  };\n\n  const resend = () => {\n    const text = edit?.text.trim();\n    if (!(edit && text) || streaming) {\n      return;\n    }\n    const at = turns.findIndex((t) => t.id === edit.id);\n    const replyId = crypto.randomUUID();\n    setTurns([\n      ...turns.slice(0, at),\n      { id: edit.id, role: 'user', text },\n      { id: replyId, role: 'assistant', text: '' },\n    ]);\n    setEdit(null);\n    stream(replyId);\n  };\n\n  const editLast = (e: React.KeyboardEvent) => {\n    const last = turns.findLast((t) => t.role === 'user');\n    if (e.key === 'ArrowUp' && !draft && last) {\n      e.preventDefault();\n      setEdit({ id: last.id, text: last.text });\n    }\n  };\n\n  const body = (turn: Turn) => {\n    const mine = turn.role === 'user';\n    if (edit?.id === turn.id) {\n      return (\n        <>\n          <Textarea\n            aria-label=\"Edit message\"\n            autoFocus\n            className={cn(\n              enter,\n              'min-h-0 w-md max-w-full resize-none rounded-[22px] border-0 bg-muted px-4 py-3 font-medium text-[15px]/6 focus-visible:ring-0 md:text-[15px]/6'\n            )}\n            onChange={(e) => setEdit({ ...edit, text: e.target.value })}\n            onKeyDown={(e) => {\n              if (e.key === 'Escape') {\n                setEdit(null);\n              }\n              if (e.key === 'Enter' && !e.shiftKey) {\n                e.preventDefault();\n                resend();\n              }\n            }}\n            value={edit.text}\n          />\n          <div className={cn(enter, 'flex items-center gap-1.5 self-end')}>\n            <Button\n              aria-label=\"Copy\"\n              className={cn(press, 'rounded-full')}\n              onClick={() => navigator.clipboard.writeText(turn.text)}\n              size=\"icon-sm\"\n              variant=\"ghost\"\n            >\n              <IconCopy stroke={1.6} />\n            </Button>\n            <Button\n              className={cn(press, 'rounded-full')}\n              onClick={() => setEdit(null)}\n              size=\"sm\"\n              variant=\"secondary\"\n            >\n              Cancel\n            </Button>\n            <Button\n              className={cn(press, 'rounded-full')}\n              disabled={!edit.text.trim() || streaming}\n              onClick={resend}\n              size=\"sm\"\n            >\n              Send\n            </Button>\n          </div>\n        </>\n      );\n    }\n    if (turn.streaming && !turn.text) {\n      return (\n        <span className=\"shimmer text-[15px]/6 text-muted-foreground\">\n          Thinking…\n        </span>\n      );\n    }\n    return turn.text.split('\\n\\n').map((paragraph) => (\n      <Bubble\n        align={mine ? 'end' : 'start'}\n        className={\n          mine\n            ? '-mx-2.5 -my-1 box-content max-w-md cursor-pointer rounded-xl px-2.5 py-1 text-right font-medium transition-colors duration-150 ease-out hover:bg-muted'\n            : undefined\n        }\n        key={paragraph}\n        onDoubleClick={\n          mine ? () => setEdit({ id: turn.id, text: turn.text }) : undefined\n        }\n        title={mine ? 'Double-click to edit' : undefined}\n        variant=\"ghost\"\n      >\n        <BubbleContent className=\"text-[15px]/6\">{paragraph}</BubbleContent>\n      </Bubble>\n    ));\n  };\n\n  return (\n    <div className=\"flex h-dvh w-full flex-col bg-background\">\n      <MessageScrollerProvider autoScroll>\n        <MessageScroller>\n          <MessageScrollerViewport>\n            <MessageScrollerContent className=\"mx-auto w-full max-w-2xl gap-3.5 px-1.5 pt-12 pb-4\">\n              {turns.map((turn) => {\n                const mine = turn.role === 'user';\n                return (\n                  <MessageScrollerItem\n                    className={cn(\n                      'px-2.5',\n                      mine && 'pt-5.5 pb-1 first:pt-1',\n                      !thread.some((t) => t.id === turn.id) && enter\n                    )}\n                    key={turn.id}\n                    messageId={turn.id}\n                  >\n                    <Message align={mine ? 'end' : 'start'}>\n                      <MessageContent className=\"gap-2\">\n                        {body(turn)}\n                        {!(mine || turn.streaming) && (\n                          <MessageFooter className={cn(enter, 'gap-0.5')}>\n                            <Button\n                              aria-label=\"Copy\"\n                              className={press}\n                              onClick={() =>\n                                navigator.clipboard.writeText(turn.text)\n                              }\n                              size=\"icon-sm\"\n                              variant=\"ghost\"\n                            >\n                              <IconCopy stroke={1.6} />\n                            </Button>\n                            <Button\n                              aria-label=\"Good response\"\n                              className={press}\n                              size=\"icon-sm\"\n                              variant=\"ghost\"\n                            >\n                              <IconThumbUp stroke={1.6} />\n                            </Button>\n                            <Button\n                              aria-label=\"Bad response\"\n                              className={press}\n                              size=\"icon-sm\"\n                              variant=\"ghost\"\n                            >\n                              <IconThumbDown stroke={1.6} />\n                            </Button>\n                            <Button\n                              aria-label=\"Regenerate\"\n                              className={press}\n                              onClick={() => stream(turn.id)}\n                              size=\"icon-sm\"\n                              variant=\"ghost\"\n                            >\n                              <IconRefresh stroke={1.6} />\n                            </Button>\n                          </MessageFooter>\n                        )}\n                      </MessageContent>\n                    </Message>\n                  </MessageScrollerItem>\n                );\n              })}\n            </MessageScrollerContent>\n          </MessageScrollerViewport>\n          <MessageScrollerButton className={easeOut} />\n        </MessageScroller>\n      </MessageScrollerProvider>\n\n      <form\n        className=\"mx-auto flex w-full max-w-2xl flex-col items-center gap-2.5 px-4 pt-3 pb-4\"\n        onSubmit={send}\n      >\n        <InputGroup className=\"h-13 rounded-full border-0 bg-muted px-2.5 has-[[data-slot=input-group-control]:focus-visible]:ring-0\">\n          <InputGroupAddon>\n            <DropdownMenu>\n              <DropdownMenuTrigger\n                render={\n                  <InputGroupButton\n                    aria-label=\"Add attachment\"\n                    className={cn(press, 'rounded-full hover:bg-foreground/10')}\n                    size=\"icon-sm\"\n                  />\n                }\n              >\n                <IconPlus stroke={1.8} />\n              </DropdownMenuTrigger>\n              <DropdownMenuContent\n                align=\"start\"\n                className=\"w-60 rounded-[20px] p-2 shadow-[0_0_0_1px_oklch(0_0_0/0.06),0_8px_24px_-6px_oklch(0_0_0/0.14)]\"\n                sideOffset={10}\n              >\n                <DropdownMenuGroup className=\"flex flex-col gap-0.5\">\n                  <DropdownMenuItem className=\"gap-3 rounded-xl px-3 py-2.5 text-[15px] [&>svg]:text-muted-foreground\">\n                    <IconPaperclip className=\"size-4.5\" stroke={1.6} />\n                    Add files\n                  </DropdownMenuItem>\n                  <DropdownMenuItem className=\"gap-3 rounded-xl px-3 py-2.5 text-[15px] [&>svg]:text-muted-foreground\">\n                    <IconPhoto className=\"size-4.5\" stroke={1.6} />\n                    Add photos\n                  </DropdownMenuItem>\n                  <DropdownMenuItem className=\"gap-3 rounded-xl px-3 py-2.5 text-[15px] [&>svg]:text-muted-foreground\">\n                    <IconWorldSearch className=\"size-4.5\" stroke={1.6} />\n                    Search the web\n                  </DropdownMenuItem>\n                </DropdownMenuGroup>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </InputGroupAddon>\n          <InputGroupInput\n            className=\"text-[15px] md:text-[15px]\"\n            onChange={(e) => setDraft(e.target.value)}\n            onKeyDown={editLast}\n            placeholder={listening ? 'Listening…' : 'Ask anything'}\n            value={draft}\n          />\n          <InputGroupAddon align=\"inline-end\">\n            {draft.trim() ? (\n              <InputGroupButton\n                aria-label=\"Send\"\n                className={cn(swap, press, 'rounded-full')}\n                disabled={streaming}\n                size=\"icon-sm\"\n                type=\"submit\"\n                variant=\"default\"\n              >\n                <IconArrowUp stroke={2} />\n              </InputGroupButton>\n            ) : (\n              <InputGroupButton\n                aria-label={listening ? 'Stop listening' : 'Voice input'}\n                aria-pressed={listening}\n                className={cn(\n                  swap,\n                  press,\n                  'rounded-full',\n                  listening\n                    ? 'bg-destructive/15 text-destructive hover:bg-destructive/25'\n                    : 'hover:bg-foreground/10'\n                )}\n                onClick={() => setListening(!listening)}\n                size=\"icon-sm\"\n              >\n                {listening ? (\n                  <IconPlayerStopFilled className=\"animate-pulse motion-reduce:animate-none\" />\n                ) : (\n                  <IconMicrophone stroke={1.8} />\n                )}\n              </InputGroupButton>\n            )}\n          </InputGroupAddon>\n        </InputGroup>\n        <p className=\"text-muted-foreground text-xs\">\n          AI can make mistakes. Check important info.\n        </p>\n      </form>\n    </div>\n  );\n}\n"
    },
    {
      "path": "licenses/chat-01/LICENSE.txt",
      "type": "registry:file",
      "target": "@components/uiblocks/chat-01/LICENSE.txt",
      "content": "Includes source from Blocks.so and its bundled primitives.\nSource: https://github.com/ephraimduncan/blocks\nRevision: 3f759d5171c2391253ec4c772e9e5b3db2bc7c7f\n\nMIT License\n\nCopyright (c) 2025 Ephraim Duncan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
    },
    {
      "path": "components/ui/bubble.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/bubble.tsx",
      "content": "import { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport type * as React from 'react';\n\nimport { cn } from \"../../lib/utils\";\n\nfunction BubbleGroup({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex min-w-0 flex-col gap-2', className)}\n      data-slot=\"bubble-group\"\n      {...props}\n    />\n  );\n}\n\nconst bubbleVariants = cva(\n  'group/bubble relative flex w-fit min-w-0 max-w-[80%] flex-col gap-1 data-[variant=ghost]:max-w-full data-[align=end]:self-end group-data-[align=end]/message:self-end',\n  {\n    variants: {\n      variant: {\n        default:\n          '*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80',\n        secondary:\n          '*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]',\n        muted:\n          '*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]',\n        tinted:\n          '*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]',\n        outline:\n          '*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30',\n        ghost:\n          'border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50',\n        destructive:\n          '*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  }\n);\n\nfunction Bubble({\n  variant = 'default',\n  align = 'start',\n  className,\n  ...props\n}: React.ComponentProps<'div'> &\n  VariantProps<typeof bubbleVariants> & {\n    align?: 'start' | 'end';\n  }) {\n  return (\n    <div\n      className={cn(bubbleVariants({ variant }), className)}\n      data-align={align}\n      data-slot=\"bubble\"\n      data-variant={variant}\n      {...props}\n    />\n  );\n}\n\nfunction BubbleContent({\n  className,\n  render,\n  ...props\n}: useRender.ComponentProps<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    props: mergeProps<'div'>(\n      {\n        className: cn(\n          'wrap-break-word w-fit min-w-0 max-w-full overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [button,a]:outline-none [button,a]:transition-colors [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 [button]:text-left',\n          className\n        ),\n      },\n      props\n    ),\n    render,\n    state: {\n      slot: 'bubble-content',\n    },\n  });\n}\n\nconst bubbleReactionsVariants = cva(\n  'absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0',\n  {\n    variants: {\n      side: {\n        top: '-translate-y-3/4 top-0',\n        bottom: 'bottom-0 translate-y-3/4',\n      },\n      align: {\n        start: 'left-3',\n        end: 'right-3',\n      },\n    },\n    defaultVariants: {\n      side: 'bottom',\n      align: 'end',\n    },\n  }\n);\n\nfunction BubbleReactions({\n  side = 'bottom',\n  align = 'end',\n  className,\n  ...props\n}: React.ComponentProps<'div'> & {\n  align?: 'start' | 'end';\n  side?: 'top' | 'bottom';\n}) {\n  return (\n    <div\n      className={cn(bubbleReactionsVariants({ side, align }), className)}\n      data-align={align}\n      data-side={side}\n      data-slot=\"bubble-reactions\"\n      {...props}\n    />\n  );\n}\n\nexport { BubbleGroup, Bubble, BubbleContent, BubbleReactions };\n"
    },
    {
      "path": "components/ui/button.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/button.tsx",
      "content": "import { Button as ButtonPrimitive } from '@base-ui/react/button';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from \"../../lib/utils\";\n\nconst buttonVariants = cva(\n  \"group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-lg border border-transparent bg-clip-padding font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: 'bg-primary text-primary-foreground hover:bg-primary/80',\n        outline:\n          'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',\n        secondary:\n          'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',\n        ghost:\n          'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',\n        destructive:\n          'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30',\n        link: 'text-primary underline-offset-4 hover:underline',\n      },\n      size: {\n        default:\n          'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n        xs: \"h-6 gap-1 in-data-[slot=button-group]:rounded-lg rounded-[min(var(--radius-md),10px)] px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-7 gap-1 in-data-[slot=button-group]:rounded-lg rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n        lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',\n        icon: 'size-8',\n        'icon-xs':\n          \"size-6 in-data-[slot=button-group]:rounded-lg rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3\",\n        'icon-sm':\n          'size-7 in-data-[slot=button-group]:rounded-lg rounded-[min(var(--radius-md),12px)]',\n        'icon-lg': 'size-9',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  }\n);\n\nfunction Button({\n  className,\n  variant = 'default',\n  size = 'default',\n  ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {\n  return (\n    <ButtonPrimitive\n      className={cn(buttonVariants({ variant, size, className }))}\n      data-slot=\"button\"\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n"
    },
    {
      "path": "components/ui/dropdown-menu.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/dropdown-menu.tsx",
      "content": "'use client';\n\nimport { Menu as MenuPrimitive } from '@base-ui/react/menu';\nimport { CheckIcon, ChevronRightIcon } from 'lucide-react';\nimport type * as React from 'react';\nimport { cn } from \"../../lib/utils\";\n\nfunction DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {\n  return <MenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {\n  return <MenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n}\n\nfunction DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {\n  return <MenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />;\n}\n\nfunction DropdownMenuContent({\n  align = 'start',\n  alignOffset = 0,\n  side = 'bottom',\n  sideOffset = 4,\n  className,\n  ...props\n}: MenuPrimitive.Popup.Props &\n  Pick<\n    MenuPrimitive.Positioner.Props,\n    'align' | 'alignOffset' | 'side' | 'sideOffset'\n  >) {\n  return (\n    <MenuPrimitive.Portal>\n      <MenuPrimitive.Positioner\n        align={align}\n        alignOffset={alignOffset}\n        className=\"isolate z-50 outline-none\"\n        side={side}\n        sideOffset={sideOffset}\n      >\n        <MenuPrimitive.Popup\n          className={cn(\n            'data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in data-closed:overflow-hidden',\n            className\n          )}\n          data-slot=\"dropdown-menu-content\"\n          {...props}\n        />\n      </MenuPrimitive.Positioner>\n    </MenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {\n  return <MenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: MenuPrimitive.GroupLabel.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.GroupLabel\n      className={cn(\n        'px-1.5 py-1 font-medium text-muted-foreground text-xs data-inset:pl-7',\n        className\n      )}\n      data-inset={inset}\n      data-slot=\"dropdown-menu-label\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = 'default',\n  ...props\n}: MenuPrimitive.Item.Props & {\n  inset?: boolean;\n  variant?: 'default' | 'destructive';\n}) {\n  return (\n    <MenuPrimitive.Item\n      className={cn(\n        \"group/dropdown-menu-item relative flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive\",\n        className\n      )}\n      data-inset={inset}\n      data-slot=\"dropdown-menu-item\"\n      data-variant={variant}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {\n  return <MenuPrimitive.SubmenuRoot data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: MenuPrimitive.SubmenuTrigger.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.SubmenuTrigger\n      className={cn(\n        \"flex cursor-default select-none items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-popup-open:bg-accent data-inset:pl-7 data-open:text-accent-foreground data-popup-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className\n      )}\n      data-inset={inset}\n      data-slot=\"dropdown-menu-sub-trigger\"\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto\" />\n    </MenuPrimitive.SubmenuTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  align = 'start',\n  alignOffset = -3,\n  side = 'right',\n  sideOffset = 0,\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuContent>) {\n  return (\n    <DropdownMenuContent\n      align={align}\n      alignOffset={alignOffset}\n      className={cn(\n        'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in',\n        className\n      )}\n      data-slot=\"dropdown-menu-sub-content\"\n      side={side}\n      sideOffset={sideOffset}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  inset,\n  ...props\n}: MenuPrimitive.CheckboxItem.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.CheckboxItem\n      checked={checked}\n      className={cn(\n        \"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className\n      )}\n      data-inset={inset}\n      data-slot=\"dropdown-menu-checkbox-item\"\n      {...props}\n    >\n      <span\n        className=\"pointer-events-none absolute right-2 flex items-center justify-center\"\n        data-slot=\"dropdown-menu-checkbox-item-indicator\"\n      >\n        <MenuPrimitive.CheckboxItemIndicator>\n          <CheckIcon />\n        </MenuPrimitive.CheckboxItemIndicator>\n      </span>\n      {children}\n    </MenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {\n  return (\n    <MenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  inset,\n  ...props\n}: MenuPrimitive.RadioItem.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.RadioItem\n      className={cn(\n        \"relative flex cursor-default select-none items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n        className\n      )}\n      data-inset={inset}\n      data-slot=\"dropdown-menu-radio-item\"\n      {...props}\n    >\n      <span\n        className=\"pointer-events-none absolute right-2 flex items-center justify-center\"\n        data-slot=\"dropdown-menu-radio-item-indicator\"\n      >\n        <MenuPrimitive.RadioItemIndicator>\n          <CheckIcon />\n        </MenuPrimitive.RadioItemIndicator>\n      </span>\n      {children}\n    </MenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: MenuPrimitive.Separator.Props) {\n  return (\n    <MenuPrimitive.Separator\n      className={cn('-mx-1 my-1 h-px bg-border', className)}\n      data-slot=\"dropdown-menu-separator\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<'span'>) {\n  return (\n    <span\n      className={cn(\n        'ml-auto text-muted-foreground text-xs tracking-widest group-focus/dropdown-menu-item:text-accent-foreground',\n        className\n      )}\n      data-slot=\"dropdown-menu-shortcut\"\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n"
    },
    {
      "path": "components/ui/input-group.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/input-group.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport type * as React from 'react';\nimport { Button } from \"./button\";\nimport { Input } from \"./input\";\nimport { Textarea } from \"./textarea\";\nimport { cn } from \"../../lib/utils\";\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input outline-none transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',\n        className\n      )}\n      data-slot=\"input-group\"\n      role=\"group\"\n      {...props}\n    />\n  );\n}\n\nconst inputGroupAddonVariants = cva(\n  \"flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      align: {\n        'inline-start':\n          'order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]',\n        'inline-end':\n          'order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]',\n        'block-start':\n          'order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2',\n        'block-end':\n          'order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2',\n      },\n    },\n    defaultVariants: {\n      align: 'inline-start',\n    },\n  }\n);\n\nfunction InputGroupAddon({\n  className,\n  align = 'inline-start',\n  ...props\n}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {\n  return (\n    <div\n      className={cn(inputGroupAddonVariants({ align }), className)}\n      data-align={align}\n      data-slot=\"input-group-addon\"\n      onClick={(e) => {\n        if ((e.target as HTMLElement).closest('button')) {\n          return;\n        }\n        e.currentTarget.parentElement?.querySelector('input')?.focus();\n      }}\n      role=\"group\"\n      {...props}\n    />\n  );\n}\n\nconst inputGroupButtonVariants = cva(\n  'flex items-center gap-2 text-sm shadow-none',\n  {\n    variants: {\n      size: {\n        xs: \"h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5\",\n        sm: '',\n        'icon-xs':\n          'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0',\n        'icon-sm': 'size-8 p-0 has-[>svg]:p-0',\n      },\n    },\n    defaultVariants: {\n      size: 'xs',\n    },\n  }\n);\n\nfunction InputGroupButton({\n  className,\n  type = 'button',\n  variant = 'ghost',\n  size = 'xs',\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, 'size' | 'type'> &\n  VariantProps<typeof inputGroupButtonVariants> & {\n    type?: 'button' | 'submit' | 'reset';\n  }) {\n  return (\n    <Button\n      className={cn(inputGroupButtonVariants({ size }), className)}\n      data-size={size}\n      type={type}\n      variant={variant}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {\n  return (\n    <span\n      className={cn(\n        \"flex items-center gap-2 text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none\",\n        className\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupInput({\n  className,\n  ...props\n}: React.ComponentProps<'input'>) {\n  return (\n    <Input\n      className={cn(\n        'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',\n        className\n      )}\n      data-slot=\"input-group-control\"\n      {...props}\n    />\n  );\n}\n\nfunction InputGroupTextarea({\n  className,\n  ...props\n}: React.ComponentProps<'textarea'>) {\n  return (\n    <Textarea\n      className={cn(\n        'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent',\n        className\n      )}\n      data-slot=\"input-group-control\"\n      {...props}\n    />\n  );\n}\n\nexport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupText,\n  InputGroupInput,\n  InputGroupTextarea,\n};\n"
    },
    {
      "path": "components/ui/input.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/input.tsx",
      "content": "import { Input as InputPrimitive } from '@base-ui/react/input';\nimport type * as React from 'react';\n\nimport { cn } from \"../../lib/utils\";\n\nfunction Input({ className, type, ...props }: React.ComponentProps<'input'>) {\n  return (\n    <InputPrimitive\n      className={cn(\n        'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80',\n        className\n      )}\n      data-slot=\"input\"\n      type={type}\n      {...props}\n    />\n  );\n}\n\nexport { Input };\n"
    },
    {
      "path": "components/ui/message-scroller.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/message-scroller.tsx",
      "content": "'use client';\n\nimport {\n  MessageScroller as MessageScrollerPrimitive,\n  useMessageScroller,\n  useMessageScrollerScrollable,\n  useMessageScrollerVisibility,\n} from '@shadcn/react/message-scroller';\nimport { ArrowDownIcon } from 'lucide-react';\nimport type * as React from 'react';\nimport { Button } from \"./button\";\nimport { cn } from \"../../lib/utils\";\n\nfunction MessageScrollerProvider(\n  props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>\n) {\n  return <MessageScrollerPrimitive.Provider {...props} />;\n}\n\nfunction MessageScroller({\n  className,\n  ...props\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {\n  return (\n    <MessageScrollerPrimitive.Root\n      className={cn(\n        'group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden',\n        className\n      )}\n      data-slot=\"message-scroller\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageScrollerViewport({\n  className,\n  ...props\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {\n  return (\n    <MessageScrollerPrimitive.Viewport\n      className={cn(\n        'scroll-fade-b scrollbar-thin scrollbar-gutter-stable data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain contain-content data-pending-scroll:invisible',\n        className\n      )}\n      data-slot=\"message-scroller-viewport\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageScrollerContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {\n  return (\n    <MessageScrollerPrimitive.Content\n      className={cn('flex h-max min-h-full flex-col gap-6', className)}\n      data-slot=\"message-scroller-content\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageScrollerItem({\n  className,\n  scrollAnchor = false,\n  ...props\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {\n  return (\n    <MessageScrollerPrimitive.Item\n      className={cn(\n        'min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]',\n        className\n      )}\n      data-slot=\"message-scroller-item\"\n      scrollAnchor={scrollAnchor}\n      {...props}\n    />\n  );\n}\n\nfunction MessageScrollerButton({\n  direction = 'end',\n  className,\n  children,\n  render,\n  variant = 'secondary',\n  size = 'icon-sm',\n  ...props\n}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &\n  Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {\n  return (\n    <MessageScrollerPrimitive.Button\n      className={cn(\n        '-translate-x-1/2 data-[direction=start]:data-[active=false]:-translate-y-full absolute inset-s-1/2 border-border bg-background text-foreground transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[direction=end]:data-[active=false]:translate-y-full data-[active=false]:pointer-events-none data-[direction=start]:top-4 data-[direction=end]:bottom-4 data-[active=true]:translate-y-0 data-[active=false]:scale-95 data-[active=true]:scale-100 data-[active=false]:opacity-0 data-[active=true]:opacity-100 data-[active=false]:duration-400 data-[active=false]:ease-[cubic-bezier(0.7,0,0.84,0)] data-[active=true]:ease-[cubic-bezier(0.23,1,0.32,1)] rtl:translate-x-1/2 data-[direction=start]:[&_svg]:rotate-180',\n        className\n      )}\n      data-direction={direction}\n      data-size={size}\n      data-slot=\"message-scroller-button\"\n      data-variant={variant}\n      direction={direction}\n      render={render ?? <Button size={size} variant={variant} />}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <ArrowDownIcon />\n          <span className=\"sr-only\">\n            {direction === 'end' ? 'Scroll to end' : 'Scroll to start'}\n          </span>\n        </>\n      )}\n    </MessageScrollerPrimitive.Button>\n  );\n}\n\nexport {\n  MessageScrollerProvider,\n  MessageScroller,\n  MessageScrollerViewport,\n  MessageScrollerContent,\n  MessageScrollerItem,\n  MessageScrollerButton,\n  useMessageScroller,\n  useMessageScrollerScrollable,\n  useMessageScrollerVisibility,\n};\n"
    },
    {
      "path": "components/ui/message.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/message.tsx",
      "content": "import type * as React from 'react';\n\nimport { cn } from \"../../lib/utils\";\n\nfunction MessageGroup({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn('flex min-w-0 flex-col gap-2', className)}\n      data-slot=\"message-group\"\n      {...props}\n    />\n  );\n}\n\nfunction Message({\n  className,\n  align = 'start',\n  ...props\n}: React.ComponentProps<'div'> & { align?: 'start' | 'end' }) {\n  return (\n    <div\n      className={cn(\n        'group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse',\n        className\n      )}\n      data-align={align}\n      data-slot=\"message\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageAvatar({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'group-has-data-[slot=message-footer]/message:-translate-y-8 flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted',\n        className\n      )}\n      data-slot=\"message-avatar\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageContent({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'wrap-break-word flex w-full min-w-0 flex-col gap-2.5 group-data-[align=end]/message:*:data-slot:self-end',\n        className\n      )}\n      data-slot=\"message-content\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageHeader({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0',\n        className\n      )}\n      data-slot=\"message-header\"\n      {...props}\n    />\n  );\n}\n\nfunction MessageFooter({ className, ...props }: React.ComponentProps<'div'>) {\n  return (\n    <div\n      className={cn(\n        'flex min-w-0 max-w-full items-center px-3 font-medium text-muted-foreground text-xs group-has-data-[variant=ghost]/message:px-0 group-data-[align=end]/message:justify-end',\n        className\n      )}\n      data-slot=\"message-footer\"\n      {...props}\n    />\n  );\n}\n\nexport {\n  MessageGroup,\n  Message,\n  MessageAvatar,\n  MessageContent,\n  MessageFooter,\n  MessageHeader,\n};\n"
    },
    {
      "path": "components/ui/textarea.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/components/ui/textarea.tsx",
      "content": "import type * as React from 'react';\n\nimport { cn } from \"../../lib/utils\";\n\nfunction Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {\n  return (\n    <textarea\n      className={cn(\n        'field-sizing-content flex min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80',\n        className\n      )}\n      data-slot=\"textarea\"\n      {...props}\n    />\n  );\n}\n\nexport { Textarea };\n"
    },
    {
      "path": "lib/utils.ts",
      "type": "registry:component",
      "target": "@components/uiblocks/chat-01/shared/lib/utils.ts",
      "content": "export { cn } from \"cn\";\n"
    }
  ],
  "categories": [
    "chat",
    "application",
    "clean-saas"
  ],
  "docs": "Import the default component from @components/uiblocks/chat-01/chat-01.tsx. React 19, Tailwind CSS 4, and the standard shadcn CSS theme variables are required. This block is framework-independent React and works in TanStack Start, Vite, and Next.js projects. Connect links or navigation callbacks to your host application. Demo avatars use public image URLs; replace them with your own assets. Demo handlers must be connected to your product. See https://uiblocks.org/chat/chat-01 for content guidelines and source attribution.",
  "meta": {
    "kind": "block",
    "version": "0.1.0",
    "source": {
      "name": "Blocks.so",
      "url": "https://github.com/ephraimduncan/blocks",
      "license": "MIT",
      "author": "Ephraim Duncan",
      "revision": "3f759d5171c2391253ec4c772e9e5b3db2bc7c7f"
    },
    "license": "MIT",
    "style": "clean-saas",
    "pageType": "application",
    "featured": false,
    "tags": [
      "chat",
      "ai",
      "conversation",
      "messages",
      "聊天",
      "对话",
      "消息",
      "助手"
    ],
    "contentGuidelines": [
      "Connect message streaming and persistence to your application.",
      "Test empty history, long responses, and failed requests."
    ],
    "recommendedWith": [],
    "entry": "@components/uiblocks/chat-01/chat-01.tsx",
    "exportName": "default",
    "frameworkIndependent": true,
    "frameworkDependencies": [],
    "compatibleHosts": [
      "react",
      "vite",
      "tanstack-start",
      "nextjs"
    ],
    "requirements": {
      "react": ">=19",
      "tailwind": ">=4"
    },
    "previewUrl": "https://uiblocks.org/preview/chat-01"
  }
}
