{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-05",
  "type": "registry:block",
  "title": "AI Elements Chat",
  "description": "AI Elements Chat. AI prompt inputs and assistant interfaces with model selection and media attachments.",
  "author": "Ephraim Duncan",
  "registryDependencies": [],
  "dependencies": [
    "@base-ui/react@1.9.0",
    "@tabler/icons-react@3.50.0",
    "ai@6.0.319",
    "class-variance-authority@0.7.1",
    "cn@0.2.6",
    "lucide-react@0.577.0",
    "streamdown@2.7.1",
    "use-stick-to-bottom@1.1.6"
  ],
  "files": [
    {
      "path": "content/components/ai/ai-05.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/ai-05.tsx",
      "content": "'use client';\n\n// Install AI Elements components:\n// npx ai-elements@latest add conversation message prompt-input\n\nimport {\n  IconAdjustmentsHorizontal,\n  IconBolt,\n  IconPaperclip,\n  IconPlus,\n} from '@tabler/icons-react';\nimport type { ChatStatus } from 'ai';\nimport { useState } from 'react';\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationScrollButton,\n} from \"./shared/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"./shared/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputButton,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  PromptInputTools,\n} from \"./shared/components/ai-elements/prompt-input\";\nimport { Button } from \"./shared/components/ui/button\";\nimport { cn } from \"./shared/lib/utils\";\n\ninterface DemoMessage {\n  id: string;\n  role: 'user' | 'assistant';\n  content: string;\n}\n\nconst INITIAL_MESSAGES: DemoMessage[] = [\n  {\n    id: 'intro',\n    role: 'assistant',\n    content:\n      'Hi, I am your workspace assistant. I can draft copy, summarize documents, and turn notes into tasks. What are you working on?',\n  },\n  {\n    id: 'question',\n    role: 'user',\n    content: 'Summarize the Q3 launch plan in three bullets.',\n  },\n  {\n    id: 'answer',\n    role: 'assistant',\n    content:\n      '- **Ship the new onboarding** by Sept 18 with an A/B test on pricing.\\n- **Open EU and APAC** with localized checkout.\\n- **Launch the partner API** in private beta with 12 partners.',\n  },\n];\n\nconst SUGGESTIONS = [\n  'Draft a launch announcement',\n  'List open risks',\n  'Turn this into tasks',\n];\n\nconst RESPONSES = [\n  'Here is a quick outline you can reuse:\\n\\n1. Swap the mock response with a real API call.\\n2. Stream tokens into `MessageResponse`.\\n3. Keep the layout as-is for a consistent UI.',\n  'If you want multi-model support, add a small model selector to the prompt footer and pass the selection to your backend.',\n  'You can also inject tools like file upload or voice input by adding buttons to the prompt footer.',\n];\n\nexport default function Ai05() {\n  const [messages, setMessages] = useState<DemoMessage[]>(INITIAL_MESSAGES);\n  const [inputValue, setInputValue] = useState('');\n  const [status, setStatus] = useState<ChatStatus>('ready');\n\n  const handleSend = (text: string) => {\n    const trimmed = text.trim();\n    if (!trimmed || status !== 'ready') {\n      return;\n    }\n\n    setMessages((prev) => [\n      ...prev,\n      { id: `user-${Date.now()}`, role: 'user', content: trimmed },\n    ]);\n    setInputValue('');\n    setStatus('submitted');\n\n    window.setTimeout(() => {\n      setMessages((prev) => [\n        ...prev,\n        {\n          id: `assistant-${Date.now()}`,\n          role: 'assistant',\n          content: RESPONSES[messages.length % RESPONSES.length],\n        },\n      ]);\n      setStatus('ready');\n    }, 1200);\n  };\n\n  const handleReset = () => {\n    setMessages(INITIAL_MESSAGES.slice(0, 1));\n    setInputValue('');\n    setStatus('ready');\n  };\n\n  return (\n    <div className=\"w-full px-4\">\n      <div className=\"mx-auto flex h-[560px] w-full max-w-2xl flex-col overflow-hidden rounded-3xl bg-card shadow-[0_0_0_1px_oklch(0_0_0/0.06),0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0_0_0/0.18)] dark:shadow-[0_0_0_1px_oklch(1_0_0/0.1),0_12px_32px_-12px_oklch(0_0_0/0.6)]\">\n        <header className=\"flex items-center justify-between gap-4 px-5 py-4\">\n          <div className=\"flex flex-col\">\n            <span className=\"font-semibold text-sm leading-tight\">\n              Assistant\n            </span>\n            <span className=\"inline-flex items-center gap-1.5 text-muted-foreground text-xs\">\n              <span className=\"size-1.5 rounded-full bg-emerald-500\" />\n              Online\n            </span>\n          </div>\n          <div className=\"flex items-center gap-1\">\n            <Button\n              className=\"h-8 gap-1.5 px-2.5 text-xs\"\n              onClick={handleReset}\n              size=\"sm\"\n              variant=\"ghost\"\n            >\n              <IconPlus className=\"size-4\" stroke={1.5} />\n              New chat\n            </Button>\n            <Button\n              aria-label=\"Settings\"\n              className=\"size-8\"\n              size=\"icon\"\n              title=\"Settings\"\n              variant=\"ghost\"\n            >\n              <IconAdjustmentsHorizontal className=\"size-4\" stroke={1.5} />\n            </Button>\n          </div>\n        </header>\n\n        <Conversation className=\"border-border/60 border-t\">\n          <ConversationContent className=\"gap-5 px-5 py-5\">\n            {messages.map((message) => (\n              <Message from={message.role} key={message.id}>\n                <MessageContent\n                  className={cn(\n                    'leading-relaxed',\n                    message.role === 'assistant' && 'max-w-prose',\n                    message.role === 'user' &&\n                      'group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-md group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground'\n                  )}\n                >\n                  {message.role === 'assistant' ? (\n                    <MessageResponse>{message.content}</MessageResponse>\n                  ) : (\n                    <p className=\"whitespace-pre-wrap text-pretty\">\n                      {message.content}\n                    </p>\n                  )}\n                </MessageContent>\n              </Message>\n            ))}\n            {status === 'submitted' && (\n              <Message from=\"assistant\">\n                <output\n                  aria-label=\"Assistant is typing\"\n                  className=\"flex h-7 items-center gap-1\"\n                >\n                  {[0, 1, 2].map((dot) => (\n                    <span\n                      className=\"size-1.5 animate-bounce rounded-full bg-muted-foreground/60\"\n                      key={dot}\n                      style={{ animationDelay: `${dot * 150}ms` }}\n                    />\n                  ))}\n                </output>\n              </Message>\n            )}\n          </ConversationContent>\n          <ConversationScrollButton />\n        </Conversation>\n\n        <div className=\"flex flex-col gap-3 p-3\">\n          <div className=\"flex flex-wrap gap-2 px-1\">\n            {SUGGESTIONS.map((suggestion) => (\n              <button\n                className=\"rounded-full border border-border/70 bg-background px-3 py-1.5 text-muted-foreground text-xs transition-[color,background-color,scale] hover:bg-muted hover:text-foreground active:scale-[0.96] disabled:pointer-events-none disabled:opacity-50\"\n                disabled={status !== 'ready'}\n                key={suggestion}\n                onClick={() => handleSend(suggestion)}\n                type=\"button\"\n              >\n                {suggestion}\n              </button>\n            ))}\n          </div>\n          <PromptInput\n            className=\"[&>[data-slot=input-group]]:rounded-xl [&>[data-slot=input-group]]:border-border [&>[data-slot=input-group]]:bg-muted/40 [&>[data-slot=input-group]]:shadow-none\"\n            onSubmit={(message) => handleSend(message.text)}\n          >\n            <PromptInputTextarea\n              onChange={(event) => setInputValue(event.currentTarget.value)}\n              placeholder=\"Message Assistant...\"\n              value={inputValue}\n            />\n            <PromptInputFooter>\n              <PromptInputTools>\n                <PromptInputButton aria-label=\"Attach file\">\n                  <IconPaperclip className=\"size-4\" stroke={1.5} />\n                </PromptInputButton>\n                <PromptInputButton aria-label=\"Quick prompt\">\n                  <IconBolt className=\"size-4\" stroke={1.5} />\n                </PromptInputButton>\n              </PromptInputTools>\n              <PromptInputSubmit\n                className=\"rounded-full transition-[scale,opacity] active:scale-[0.96]\"\n                disabled={!inputValue.trim() || status !== 'ready'}\n                status={status}\n              />\n            </PromptInputFooter>\n          </PromptInput>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "licenses/ai-05/LICENSE.txt",
      "type": "registry:file",
      "target": "@components/uiblocks/ai-05/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/ai-elements/conversation.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/shared/components/ai-elements/conversation.tsx",
      "content": "'use client';\n\nimport { ArrowDownIcon } from 'lucide-react';\nimport type { ComponentProps } from 'react';\nimport { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';\nimport { Button } from \"../ui/button\";\nimport { cn } from \"../../lib/utils\";\n\nexport type ConversationProps = ComponentProps<typeof StickToBottom>;\n\nexport const Conversation = ({ className, ...props }: ConversationProps) => (\n  <StickToBottom\n    className={cn('relative flex-1 overflow-y-hidden', className)}\n    initial=\"smooth\"\n    resize=\"smooth\"\n    role=\"log\"\n    {...props}\n  />\n);\n\nexport type ConversationContentProps = ComponentProps<\n  typeof StickToBottom.Content\n>;\n\nexport const ConversationContent = ({\n  className,\n  ...props\n}: ConversationContentProps) => (\n  <StickToBottom.Content\n    className={cn('flex flex-col gap-8 p-4', className)}\n    {...props}\n  />\n);\n\nexport type ConversationScrollButtonProps = ComponentProps<typeof Button>;\n\nexport const ConversationScrollButton = ({\n  className,\n  ...props\n}: ConversationScrollButtonProps) => {\n  const { isAtBottom, scrollToBottom } = useStickToBottomContext();\n\n  if (isAtBottom) {\n    return null;\n  }\n\n  return (\n    <Button\n      className={cn(\n        'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',\n        className\n      )}\n      onClick={() => scrollToBottom()}\n      size=\"icon\"\n      type=\"button\"\n      variant=\"outline\"\n      {...props}\n    >\n      <ArrowDownIcon className=\"size-4\" />\n    </Button>\n  );\n};\n"
    },
    {
      "path": "components/ai-elements/message.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/shared/components/ai-elements/message.tsx",
      "content": "'use client';\n\nimport type { UIMessage } from 'ai';\nimport type { ComponentProps, HTMLAttributes } from 'react';\nimport { memo } from 'react';\nimport { Streamdown } from 'streamdown';\nimport { cn } from \"../../lib/utils\";\n\nexport type MessageProps = HTMLAttributes<HTMLDivElement> & {\n  from: UIMessage['role'];\n};\n\nexport const Message = ({ className, from, ...props }: MessageProps) => (\n  <div\n    className={cn(\n      'group flex w-full max-w-[95%] flex-col gap-2',\n      from === 'user' ? 'is-user ml-auto justify-end' : 'is-assistant',\n      className\n    )}\n    {...props}\n  />\n);\n\nexport type MessageContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const MessageContent = ({\n  children,\n  className,\n  ...props\n}: MessageContentProps) => (\n  <div\n    className={cn(\n      'flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm',\n      'group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground',\n      'group-[.is-assistant]:text-foreground',\n      className\n    )}\n    {...props}\n  >\n    {children}\n  </div>\n);\n\nexport type MessageResponseProps = ComponentProps<typeof Streamdown>;\n\nexport const MessageResponse = memo(\n  ({ className, ...props }: MessageResponseProps) => (\n    <Streamdown\n      className={cn(\n        'size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0',\n        className\n      )}\n      {...props}\n    />\n  ),\n  (prev, next) => prev.children === next.children\n);\n\nMessageResponse.displayName = 'MessageResponse';\n"
    },
    {
      "path": "components/ai-elements/prompt-input.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/shared/components/ai-elements/prompt-input.tsx",
      "content": "'use client';\n\nimport type { ChatStatus, FileUIPart } from 'ai';\nimport { CornerDownLeftIcon, Loader2Icon, SquareIcon, XIcon } from 'lucide-react';\nimport {\n  Children,\n  type ComponentProps,\n  type FormEvent,\n  type HTMLAttributes,\n  type KeyboardEventHandler,\n  useState,\n} from 'react';\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupTextarea,\n} from \"../ui/input-group\";\nimport { cn } from \"../../lib/utils\";\n\nexport type PromptInputMessage = {\n  text: string;\n  files: FileUIPart[];\n};\n\nexport type PromptInputProps = Omit<\n  HTMLAttributes<HTMLFormElement>,\n  'onSubmit'\n> & {\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n};\n\nexport const PromptInput = ({\n  className,\n  onSubmit,\n  children,\n  ...props\n}: PromptInputProps) => (\n  <form\n    className={cn('w-full', className)}\n    onSubmit={(event) => {\n      event.preventDefault();\n      const data = new FormData(event.currentTarget);\n      void onSubmit(\n        { text: String(data.get('message') ?? ''), files: [] },\n        event\n      );\n    }}\n    {...props}\n  >\n    <InputGroup className=\"overflow-hidden\">{children}</InputGroup>\n  </form>\n);\n\nexport type PromptInputTextareaProps = ComponentProps<\n  typeof InputGroupTextarea\n>;\n\nexport const PromptInputTextarea = ({\n  className,\n  placeholder = 'What would you like to know?',\n  ...props\n}: PromptInputTextareaProps) => {\n  const [isComposing, setIsComposing] = useState(false);\n\n  const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (event) => {\n    props.onKeyDown?.(event);\n    if (\n      event.defaultPrevented ||\n      event.key !== 'Enter' ||\n      event.shiftKey ||\n      isComposing ||\n      event.nativeEvent.isComposing\n    ) {\n      return;\n    }\n\n    event.preventDefault();\n    const button = event.currentTarget.form?.querySelector(\n      'button[type=\"submit\"]'\n    );\n    if (!(button instanceof HTMLButtonElement) || !button.disabled) {\n      event.currentTarget.form?.requestSubmit();\n    }\n  };\n\n  return (\n    <InputGroupTextarea\n      className={cn('field-sizing-content max-h-48 min-h-16', className)}\n      name=\"message\"\n      onCompositionEnd={() => setIsComposing(false)}\n      onCompositionStart={() => setIsComposing(true)}\n      {...props}\n      onKeyDown={handleKeyDown}\n    />\n  );\n};\n\nexport type PromptInputFooterProps = Omit<\n  ComponentProps<typeof InputGroupAddon>,\n  'align'\n>;\n\nexport const PromptInputFooter = ({\n  className,\n  ...props\n}: PromptInputFooterProps) => (\n  <InputGroupAddon\n    align=\"block-end\"\n    className={cn('justify-between gap-1', className)}\n    {...props}\n  />\n);\n\nexport type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTools = ({\n  className,\n  ...props\n}: PromptInputToolsProps) => (\n  <div className={cn('flex items-center gap-1', className)} {...props} />\n);\n\nexport type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;\n\nexport const PromptInputButton = ({\n  variant = 'ghost',\n  className,\n  size,\n  ...props\n}: PromptInputButtonProps) => (\n  <InputGroupButton\n    className={cn(className)}\n    size={size ?? (Children.count(props.children) > 1 ? 'sm' : 'icon-sm')}\n    type=\"button\"\n    variant={variant}\n    {...props}\n  />\n);\n\nexport type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {\n  status?: ChatStatus;\n};\n\nexport const PromptInputSubmit = ({\n  className,\n  variant = 'default',\n  size = 'icon-sm',\n  status,\n  children,\n  ...props\n}: PromptInputSubmitProps) => {\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === 'submitted') {\n    icon = <Loader2Icon className=\"size-4 animate-spin\" />;\n  } else if (status === 'streaming') {\n    icon = <SquareIcon className=\"size-4\" />;\n  } else if (status === 'error') {\n    icon = <XIcon className=\"size-4\" />;\n  }\n\n  return (\n    <InputGroupButton\n      aria-label=\"Submit\"\n      className={cn(className)}\n      size={size}\n      type=\"submit\"\n      variant={variant}\n      {...props}\n    >\n      {children ?? icon}\n    </InputGroupButton>\n  );\n};\n"
    },
    {
      "path": "components/ui/button.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/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/input-group.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/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/ai-05/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/textarea.tsx",
      "type": "registry:component",
      "target": "@components/uiblocks/ai-05/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/ai-05/shared/lib/utils.ts",
      "content": "export { cn } from \"cn\";\n"
    }
  ],
  "categories": [
    "ai",
    "application",
    "clean-saas"
  ],
  "docs": "Import the default component from @components/uiblocks/ai-05/ai-05.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/ai/ai-05 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": [
      "ai",
      "prompt",
      "chat",
      "assistant",
      "人工智能",
      "提示词",
      "聊天",
      "助手"
    ],
    "contentGuidelines": [
      "Provide your own generation endpoint, cancellation, and error handling.",
      "Test a long prompt, empty submission, and pending generation state."
    ],
    "recommendedWith": [],
    "entry": "@components/uiblocks/ai-05/ai-05.tsx",
    "exportName": "default",
    "frameworkIndependent": true,
    "frameworkDependencies": [],
    "compatibleHosts": [
      "react",
      "vite",
      "tanstack-start",
      "nextjs"
    ],
    "requirements": {
      "react": ">=19",
      "tailwind": ">=4"
    },
    "previewUrl": "https://uiblocks.org/preview/ai-05"
  }
}
