{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-message",
  "title": "AI: Message",
  "description": "A chat message row with assistant markdown, action toolbar, branches and attachments.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "streamdown@^2.5.0"
  ],
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/button",
    "@nx-ui/button-group",
    "@nx-ui/tooltip",
    "@nx-ui/ai-types"
  ],
  "files": [
    {
      "path": "components/ai/message.tsx",
      "content": "\"use client\"\n\nimport {\n  type ComponentProps,\n  type HTMLAttributes,\n  type ReactElement,\n  createContext,\n  memo,\n  useContext,\n  useEffect,\n  useState,\n} from \"react\"\nimport { ChevronLeft, ChevronRight, Paperclip, XClose } from \"@untitledui/icons\"\nimport { Streamdown } from \"streamdown\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { ButtonGroup, ButtonGroupText } from \"@/components/ui/button-group\"\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\"\nimport { cn } from \"@/lib/utils\"\n\nimport type { FileUIPart, UIMessage } from \"./types\"\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 max-w-full min-w-0 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 MessageActionsProps = ComponentProps<\"div\">\n\nexport const MessageActions = ({\n  className,\n  children,\n  ...props\n}: MessageActionsProps) => (\n  <div className={cn(\"flex items-center gap-1\", className)} {...props}>\n    {children}\n  </div>\n)\n\nexport type MessageActionProps = ComponentProps<typeof Button> & {\n  tooltip?: string\n  label?: string\n}\n\nexport const MessageAction = ({\n  tooltip,\n  children,\n  label,\n  variant = \"ghost\",\n  size = \"icon-sm\",\n  ...props\n}: MessageActionProps) => {\n  const button = (\n    <Button size={size} type=\"button\" variant={variant} {...props}>\n      {children}\n      <span className=\"sr-only\">{label || tooltip}</span>\n    </Button>\n  )\n\n  if (tooltip) {\n    return (\n      <TooltipProvider>\n        <Tooltip>\n          <TooltipTrigger render={button} />\n          <TooltipContent>\n            <p>{tooltip}</p>\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    )\n  }\n\n  return button\n}\n\ntype MessageBranchContextType = {\n  currentBranch: number\n  totalBranches: number\n  goToPrevious: () => void\n  goToNext: () => void\n  branches: ReactElement[]\n  setBranches: (branches: ReactElement[]) => void\n}\n\nconst MessageBranchContext = createContext<MessageBranchContextType | null>(null)\n\nconst useMessageBranch = () => {\n  const context = useContext(MessageBranchContext)\n\n  if (!context) {\n    throw new Error(\"MessageBranch components must be used within MessageBranch\")\n  }\n\n  return context\n}\n\nexport type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {\n  defaultBranch?: number\n  onBranchChange?: (branchIndex: number) => void\n}\n\nexport const MessageBranch = ({\n  defaultBranch = 0,\n  onBranchChange,\n  className,\n  ...props\n}: MessageBranchProps) => {\n  const [currentBranch, setCurrentBranch] = useState(defaultBranch)\n  const [branches, setBranches] = useState<ReactElement[]>([])\n\n  const handleBranchChange = (newBranch: number) => {\n    setCurrentBranch(newBranch)\n    onBranchChange?.(newBranch)\n  }\n\n  const goToPrevious = () => {\n    const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1\n    handleBranchChange(newBranch)\n  }\n\n  const goToNext = () => {\n    const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0\n    handleBranchChange(newBranch)\n  }\n\n  const contextValue: MessageBranchContextType = {\n    currentBranch,\n    totalBranches: branches.length,\n    goToPrevious,\n    goToNext,\n    branches,\n    setBranches,\n  }\n\n  return (\n    <MessageBranchContext.Provider value={contextValue}>\n      <div className={cn(\"grid w-full gap-2 [&>div]:pb-0\", className)} {...props} />\n    </MessageBranchContext.Provider>\n  )\n}\n\nexport type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>\n\nexport const MessageBranchContent = ({\n  children,\n  ...props\n}: MessageBranchContentProps) => {\n  const { currentBranch, setBranches, branches } = useMessageBranch()\n  const childrenArray = (\n    Array.isArray(children) ? children : [children]\n  ) as ReactElement[]\n\n  // Use useEffect to update branches when they change\n  useEffect(() => {\n    if (branches.length !== childrenArray.length) {\n      setBranches(childrenArray)\n    }\n  }, [childrenArray, branches, setBranches])\n\n  return childrenArray.map((branch, index) => (\n    <div\n      className={cn(\n        \"grid gap-2 overflow-hidden [&>div]:pb-0\",\n        index === currentBranch ? \"block\" : \"hidden\"\n      )}\n      key={branch.key}\n      {...props}\n    >\n      {branch}\n    </div>\n  ))\n}\n\nexport type MessageBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {\n  from: UIMessage[\"role\"]\n}\n\nexport const MessageBranchSelector = ({\n  className,\n  from,\n  ...props\n}: MessageBranchSelectorProps) => {\n  const { totalBranches } = useMessageBranch()\n\n  // Don't render if there's only one branch\n  if (totalBranches <= 1) {\n    return null\n  }\n\n  return (\n    <ButtonGroup\n      className=\"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md\"\n      orientation=\"horizontal\"\n      {...props}\n    />\n  )\n}\n\nexport type MessageBranchPreviousProps = ComponentProps<typeof Button>\n\nexport const MessageBranchPrevious = ({\n  children,\n  ...props\n}: MessageBranchPreviousProps) => {\n  const { goToPrevious, totalBranches } = useMessageBranch()\n\n  return (\n    <Button\n      aria-label=\"Previous branch\"\n      disabled={totalBranches <= 1}\n      onClick={goToPrevious}\n      size=\"icon-sm\"\n      type=\"button\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <ChevronLeft className=\"size-3.5\" />}\n    </Button>\n  )\n}\n\nexport type MessageBranchNextProps = ComponentProps<typeof Button>\n\nexport const MessageBranchNext = ({\n  children,\n  className,\n  ...props\n}: MessageBranchNextProps) => {\n  const { goToNext, totalBranches } = useMessageBranch()\n\n  return (\n    <Button\n      aria-label=\"Next branch\"\n      disabled={totalBranches <= 1}\n      onClick={goToNext}\n      size=\"icon-sm\"\n      type=\"button\"\n      variant=\"ghost\"\n      {...props}\n    >\n      {children ?? <ChevronRight className=\"size-3.5\" />}\n    </Button>\n  )\n}\n\nexport type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>\n\nexport const MessageBranchPage = ({\n  className,\n  ...props\n}: MessageBranchPageProps) => {\n  const { currentBranch, totalBranches } = useMessageBranch()\n\n  return (\n    <ButtonGroupText\n      className={cn(\n        \"border-none bg-transparent text-muted-foreground shadow-none\",\n        className\n      )}\n      {...props}\n    >\n      {currentBranch + 1} of {totalBranches}\n    </ButtonGroupText>\n  )\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  (prevProps, nextProps) => prevProps.children === nextProps.children\n)\n\nMessageResponse.displayName = \"MessageResponse\"\n\nexport type MessageAttachmentProps = HTMLAttributes<HTMLDivElement> & {\n  data: FileUIPart\n  className?: string\n  onRemove?: () => void\n}\n\nexport function MessageAttachment({\n  data,\n  className,\n  onRemove,\n  ...props\n}: MessageAttachmentProps) {\n  const filename = data.filename || \"\"\n  const mediaType =\n    data.mediaType?.startsWith(\"image/\") && data.url ? \"image\" : \"file\"\n  const isImage = mediaType === \"image\"\n  const attachmentLabel = filename || (isImage ? \"Image\" : \"Attachment\")\n\n  return (\n    <div\n      className={cn(\n        \"group relative size-24 overflow-hidden rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {isImage ? (\n        <>\n          {/* eslint-disable-next-line @next/next/no-img-element */}\n          <img\n            alt={filename || \"attachment\"}\n            className=\"size-full object-cover\"\n            height={100}\n            src={data.url}\n            width={100}\n          />\n          {onRemove && (\n            <Button\n              aria-label=\"Remove attachment\"\n              className=\"absolute top-2 right-2 size-6 rounded-full bg-background/80 p-0 opacity-0 backdrop-blur-sm transition-opacity hover:bg-background group-hover:opacity-100 [&>svg]:size-3\"\n              onClick={(e) => {\n                e.stopPropagation()\n                onRemove()\n              }}\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              <XClose />\n              <span className=\"sr-only\">Remove</span>\n            </Button>\n          )}\n        </>\n      ) : (\n        <>\n          <Tooltip>\n            <TooltipTrigger\n              render={\n                <div className=\"flex size-full shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground\">\n                  <Paperclip className=\"size-4\" />\n                </div>\n              }\n            />\n            <TooltipContent>\n              <p>{attachmentLabel}</p>\n            </TooltipContent>\n          </Tooltip>\n          {onRemove && (\n            <Button\n              aria-label=\"Remove attachment\"\n              className=\"size-6 shrink-0 rounded-full p-0 opacity-0 transition-opacity hover:bg-accent group-hover:opacity-100 [&>svg]:size-3\"\n              onClick={(e) => {\n                e.stopPropagation()\n                onRemove()\n              }}\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              <XClose />\n              <span className=\"sr-only\">Remove</span>\n            </Button>\n          )}\n        </>\n      )}\n    </div>\n  )\n}\n\nexport type MessageAttachmentsProps = ComponentProps<\"div\">\n\nexport function MessageAttachments({\n  children,\n  className,\n  ...props\n}: MessageAttachmentsProps) {\n  if (!children) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(\"ml-auto flex w-fit flex-wrap items-start gap-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport type MessageToolbarProps = ComponentProps<\"div\">\n\nexport const MessageToolbar = ({\n  className,\n  children,\n  ...props\n}: MessageToolbarProps) => (\n  <div\n    className={cn(\n      \"mt-4 flex w-full items-center justify-between gap-4\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n  </div>\n)\n",
      "type": "registry:ui",
      "target": "components/ai/message.tsx"
    }
  ],
  "type": "registry:ui"
}