{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat",
  "description": "Composable chat components with messages, reactions, replies, and delivery states.",
  "dependencies": [
    "motion@14.0.0",
    "lucide-react",
    "@base-ui/react@1.8.0"
  ],
  "registryDependencies": [
    "@coss/button",
    "@coss/collapsible",
    "@coss/empty",
    "@coss/menu",
    "@coss/popover",
    "@coss/scroll-area",
    "@coss/skeleton",
    "@coss/toggle",
    "@coss/toolbar",
    "@coss/tooltip",
    "@coss/utils"
  ],
  "files": [
    {
      "path": "registry/default/chat/chat-bubble.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatBubbleProps = useRender.ComponentProps<\"div\"> & {\n  align?: \"start\" | \"end\";\n  variant?: \"default\" | \"muted\";\n  grouped?: boolean;\n};\n\nexport function ChatBubble({\n  align = \"start\",\n  variant = \"default\",\n  grouped = false,\n  className,\n  render,\n  ...props\n}: ChatBubbleProps): ReactElement {\n  const defaultProps = {\n    \"data-slot\": \"chat-bubble\",\n    \"data-align\": align,\n    \"data-variant\": variant,\n    \"data-grouped\": grouped || undefined,\n    className: cn(\n      \"relative flex min-w-0 flex-col gap-2 rounded-2xl px-3.5 py-2.5 text-foreground\",\n      variant === \"default\" ? \"bg-foreground/12\" : \"bg-foreground/4\",\n      !grouped && (align === \"end\" ? \"rounded-se-md\" : \"rounded-ss-md\"),\n      className,\n    ),\n  };\n\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(defaultProps, props),\n  });\n}\n\nexport function ChatBubbleReactions({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-bubble-reactions\"\n      className={cn(\"flex flex-wrap gap-1 px-0.5\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-bubble.tsx"
    },
    {
      "path": "registry/default/chat/chat-collapsible-message.tsx",
      "content": "\"use client\";\n\nimport { ChevronDownIcon, ChevronUpIcon } from \"lucide-react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n  Collapsible,\n  CollapsiblePanel,\n  CollapsibleTrigger,\n} from \"@/registry/default/ui/collapsible\";\nimport { splitChatMessagePreview } from \"./lib/split-chat-message-preview\";\n\nexport type ChatCollapsibleMessageProps = {\n  text: string;\n  showMoreLabel: string;\n  showLessLabel: string;\n  side: \"incoming\" | \"outgoing\";\n  maxPreviewLength?: number;\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  renderText?: (text: string) => ReactNode;\n  className?: string;\n};\n\nfunction renderPlainText(text: string): ReactElement {\n  return (\n    <p className=\"m-0 whitespace-pre-wrap break-words text-sm leading-[1.55]\">\n      {text}\n    </p>\n  );\n}\n\nfunction ChatCollapsibleMessage({\n  text,\n  showMoreLabel,\n  showLessLabel,\n  side,\n  maxPreviewLength,\n  defaultOpen = false,\n  open,\n  onOpenChange,\n  renderText = renderPlainText,\n  className,\n}: ChatCollapsibleMessageProps): ReactElement {\n  const { preview, remainder } = splitChatMessagePreview(\n    text,\n    maxPreviewLength,\n  );\n  if (!remainder) {\n    return <div className={cn(\"min-w-0\", className)}>{renderText(text)}</div>;\n  }\n\n  return (\n    <ExpandableChatMessage\n      text={text}\n      preview={preview}\n      renderText={renderText}\n      showMoreLabel={showMoreLabel}\n      showLessLabel={showLessLabel}\n      side={side}\n      className={className}\n      defaultOpen={defaultOpen}\n      open={open}\n      onOpenChange={onOpenChange}\n    />\n  );\n}\n\nfunction ExpandableChatMessage({\n  text,\n  preview,\n  renderText,\n  showMoreLabel,\n  showLessLabel,\n  side,\n  className,\n  defaultOpen,\n  open: controlledOpen,\n  onOpenChange,\n}: {\n  text: string;\n  preview: string;\n  renderText: (text: string) => ReactNode;\n  showMoreLabel: string;\n  showLessLabel: string;\n  side: \"incoming\" | \"outgoing\";\n  className?: string;\n  defaultOpen: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}): ReactElement {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const open = controlledOpen ?? internalOpen;\n  const setOpen = (nextOpen: boolean): void => {\n    if (controlledOpen === undefined) setInternalOpen(nextOpen);\n    onOpenChange?.(nextOpen);\n  };\n  const Icon = open ? ChevronUpIcon : ChevronDownIcon;\n\n  return (\n    <Collapsible\n      open={open}\n      onOpenChange={setOpen}\n      className={cn(\"flex min-w-0 flex-col\", className)}\n    >\n      <div className=\"grid\">\n        <div\n          data-slot=\"chat-collapsible-preview\"\n          aria-hidden={open || undefined}\n          className={cn(\n            \"mask-[linear-gradient(to_bottom,black_calc(100%-3rem),transparent)] col-start-1 row-start-1 self-start\",\n            open && \"invisible\",\n          )}\n        >\n          {renderText(preview)}\n        </div>\n        <CollapsiblePanel className=\"col-start-1 row-start-1 min-w-0 motion-reduce:transition-none\">\n          <div\n            aria-hidden={!open || undefined}\n            className={open ? undefined : \"invisible\"}\n          >\n            {renderText(text)}\n          </div>\n        </CollapsiblePanel>\n      </div>\n      <CollapsibleTrigger\n        type=\"button\"\n        className={cn(\n          \"mt-2 flex w-fit items-center gap-1 text-start text-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n          side === \"outgoing\"\n            ? \"text-foreground/80 hover:text-foreground\"\n            : \"text-muted-foreground hover:text-foreground\",\n        )}\n      >\n        {open ? showLessLabel : showMoreLabel}\n        <Icon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n      </CollapsibleTrigger>\n    </Collapsible>\n  );\n}\n\nexport { ChatCollapsibleMessage, splitChatMessagePreview };\n",
      "type": "registry:component",
      "target": "@components/chat/chat-collapsible-message.tsx"
    },
    {
      "path": "registry/default/chat/chat-conversation.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n  ScrollAreaPrimitive,\n  ScrollBar,\n} from \"@/registry/default/ui/scroll-area\";\nimport { ChatJumpToLatestButton } from \"./chat-jump-to-latest-button\";\nimport type { ChatScrollController } from \"./hooks/use-chat-scroll\";\n\nexport type ChatConversationProps = ComponentProps<\"div\"> & {\n  scroll: ChatScrollController;\n  label: string;\n  jumpLabel: ReactNode;\n  contentClassName?: string;\n  overlay?: ReactNode;\n  inactive?: boolean;\n  /** \"off\" when the caller announces arrivals itself, e.g. so paged-in history isn't read out. */\n  live?: \"polite\" | \"off\";\n};\n\nexport function ChatConversation({\n  scroll,\n  label,\n  jumpLabel,\n  contentClassName,\n  children,\n  overlay,\n  inactive = false,\n  live,\n  className,\n  ...props\n}: ChatConversationProps): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-conversation\"\n      className={cn(\"relative isolate flex min-h-0 flex-1 flex-col\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Root\n        inert={inactive}\n        className=\"size-full min-h-0 touch-auto before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-6 before:bg-linear-to-b before:from-popover before:to-transparent before:opacity-0 before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-6 after:bg-linear-to-t after:from-popover after:to-transparent after:opacity-0 after:content-[''] data-overflow-y-end:after:opacity-100 data-overflow-y-start:before:opacity-100\"\n      >\n        <ScrollAreaPrimitive.Viewport\n          ref={scroll.viewportRef}\n          role=\"log\"\n          aria-label={label}\n          aria-live={live}\n          aria-relevant=\"additions text\"\n          data-slot=\"scroll-area-viewport\"\n          className=\"h-full rounded-[inherit] outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-has-overflow-y:overscroll-y-contain data-has-overflow-x:overscroll-x-contain\"\n          onScroll={scroll.onScroll}\n        >\n          <ScrollAreaPrimitive.Content\n            data-slot=\"scroll-area-content\"\n            className={cn(\"px-4 pb-4\", contentClassName)}\n            style={{ minWidth: 0 }}\n          >\n            {children}\n          </ScrollAreaPrimitive.Content>\n        </ScrollAreaPrimitive.Viewport>\n        <ScrollBar orientation=\"vertical\" />\n        <ScrollBar orientation=\"horizontal\" />\n        <ScrollAreaPrimitive.Corner data-slot=\"scroll-area-corner\" />\n      </ScrollAreaPrimitive.Root>\n      {scroll.showJump && !inactive && (\n        <div className=\"pointer-events-none absolute inset-x-0 bottom-3 z-30 flex justify-center\">\n          <ChatJumpToLatestButton onClick={scroll.scrollToLatest}>\n            {jumpLabel}\n          </ChatJumpToLatestButton>\n        </div>\n      )}\n      {overlay}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-conversation.tsx"
    },
    {
      "path": "registry/default/chat/chat-date-pill.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function ChatDatePill({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-date-pill\"\n      className={cn(\n        \"pointer-events-none sticky top-4 z-20 mx-auto flex h-6 w-fit items-center justify-center rounded-full border bg-popover/90 px-2 font-medium text-foreground text-xs backdrop-blur-xs\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-date-pill.tsx"
    },
    {
      "path": "registry/default/chat/chat-delivery-indicator.tsx",
      "content": "\"use client\";\n\nimport {\n  CheckCheckIcon,\n  CheckIcon,\n  CircleAlertIcon,\n  ClockIcon,\n} from \"lucide-react\";\nimport type { ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatDeliveryIndicatorProps = {\n  status: \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n  label: string;\n};\n\nexport function ChatDeliveryIndicator({\n  status,\n  label,\n}: ChatDeliveryIndicatorProps): ReactElement {\n  const Icon = {\n    sending: ClockIcon,\n    sent: CheckIcon,\n    delivered: CheckCheckIcon,\n    read: CheckCheckIcon,\n    failed: CircleAlertIcon,\n  }[status];\n\n  return (\n    <span\n      role=\"img\"\n      aria-label={label}\n      title={label}\n      data-slot=\"chat-delivery-indicator\"\n    >\n      <Icon\n        aria-hidden=\"true\"\n        className={cn(\n          \"size-3\",\n          status === \"read\" && \"text-sky-600 dark:text-sky-400\",\n          status === \"failed\" && \"text-destructive\",\n        )}\n      />\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-delivery-indicator.tsx"
    },
    {
      "path": "registry/default/chat/chat-jump-to-latest-button.tsx",
      "content": "\"use client\";\n\nimport { ArrowDownIcon } from \"lucide-react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Button, type ButtonProps } from \"@/registry/default/ui/button\";\n\nexport type ChatJumpToLatestButtonProps = Omit<\n  ButtonProps,\n  \"children\" | \"size\" | \"variant\"\n> & {\n  children: ReactNode;\n};\n\nexport function ChatJumpToLatestButton({\n  className,\n  children,\n  ...props\n}: ChatJumpToLatestButtonProps): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-jump-to-latest\"\n      className=\"rounded-full bg-popover/90 backdrop-blur-xs\"\n    >\n      <Button\n        variant=\"outline\"\n        size=\"xs\"\n        className={cn(\n          \"pointer-events-auto gap-2 rounded-full before:rounded-full\",\n          className,\n        )}\n        {...props}\n      >\n        <ArrowDownIcon aria-hidden=\"true\" />\n        {children}\n      </Button>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-jump-to-latest-button.tsx"
    },
    {
      "path": "registry/default/chat/chat-members.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatMemberEntry = {\n  id: string;\n  name: ReactNode;\n  avatar?: ReactNode;\n  badges?: ReactNode;\n  action?: ReactNode;\n  dimmed?: boolean;\n};\n\nexport type ChatMemberListProps = ComponentProps<\"ul\"> & {\n  members: readonly ChatMemberEntry[];\n};\n\nexport function ChatMemberList({\n  members,\n  className,\n  ...props\n}: ChatMemberListProps): ReactElement {\n  return (\n    <ul\n      data-slot=\"chat-member-list\"\n      className={cn(\"m-0 flex list-none flex-col gap-2 p-0\", className)}\n      {...props}\n    >\n      {members.map((member) => (\n        <li key={member.id} className=\"flex min-w-0 items-center gap-3\">\n          {member.avatar}\n          <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-1.5 gap-y-1\">\n            <span\n              className={cn(\n                \"truncate text-sm\",\n                member.dimmed ? \"text-muted-foreground\" : \"font-medium\",\n              )}\n            >\n              {member.name}\n            </span>\n            {member.badges}\n          </div>\n          {member.action}\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nexport type ChatMemberSectionProps = ChatMemberListProps & {\n  label: string;\n  heading?: ReactNode;\n};\n\nexport function ChatMemberSection({\n  label,\n  heading,\n  members,\n  ...props\n}: ChatMemberSectionProps): ReactElement | null {\n  if (!members.length) return null;\n  return (\n    <section aria-label={label} className=\"flex flex-col gap-4\">\n      {heading && (\n        <h3 className=\"m-0 flex items-center gap-2 font-medium text-muted-foreground text-xs\">\n          {heading}\n        </h3>\n      )}\n      <ChatMemberList members={members} {...props} />\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-members.tsx"
    },
    {
      "path": "registry/default/chat/chat-message-actions.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, EllipsisIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactElement, ReactNode, RefObject } from \"react\";\nimport { useRef, useState } from \"react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { Menu, MenuPopup, MenuTrigger } from \"@/registry/default/ui/menu\";\nimport { Toolbar, ToolbarButton } from \"@/registry/default/ui/toolbar\";\nimport {\n  Tooltip,\n  TooltipPopup,\n  TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\nimport { useChatPopupDismissal } from \"./hooks/use-chat-popup-dismissal\";\n\nexport type ChatMessageActionToolbarProps = {\n  side: \"incoming\" | \"outgoing\";\n  toolbarLabel: string;\n  reactionPicker?: (toolbarRef: RefObject<HTMLDivElement | null>) => ReactNode;\n  menuTriggerRef?: RefObject<HTMLButtonElement | null>;\n  menuFinalFocus?: ComponentProps<typeof MenuPopup>[\"finalFocus\"];\n} & (\n  | { onReply: () => void; replyLabel: string; replyTooltip: string }\n  | { onReply?: never; replyLabel?: never; replyTooltip?: never }\n) &\n  (\n    | { menuChildren: ReactNode; moreLabel: string; moreTooltip: string }\n    | { menuChildren?: never; moreLabel?: never; moreTooltip?: never }\n  );\n\nexport function ChatMessageActionToolbar({\n  side,\n  toolbarLabel,\n  reactionPicker,\n  replyLabel,\n  replyTooltip,\n  onReply,\n  moreLabel,\n  moreTooltip,\n  menuChildren,\n  menuTriggerRef: suppliedMenuTriggerRef,\n  menuFinalFocus,\n}: ChatMessageActionToolbarProps): ReactElement | null {\n  const [menuOpen, setMenuOpen] = useState(false);\n  const [menuDismissedOutside, setMenuDismissedOutside] = useState(false);\n  const toolbarRef = useRef<HTMLDivElement>(null);\n  const ownMenuTriggerRef = useRef<HTMLButtonElement>(null);\n  const menuTriggerRef = suppliedMenuTriggerRef ?? ownMenuTriggerRef;\n\n  useChatPopupDismissal(menuOpen, menuTriggerRef, () => setMenuOpen(false));\n\n  if (!reactionPicker && !onReply && !menuChildren) return null;\n\n  return (\n    <Toolbar\n      ref={toolbarRef}\n      aria-label={toolbarLabel}\n      data-slot=\"chat-message-action-toolbar\"\n      className=\"z-10 min-w-max flex-none items-center gap-0 self-center rounded-full bg-transparent p-0.5 opacity-0 pointer-coarse:opacity-100 transition-opacity group-hover/message:opacity-100 has-data-popup-open:opacity-100 has-focus-visible:opacity-100 motion-reduce:transition-none\"\n    >\n      {reactionPicker?.(toolbarRef)}\n      {onReply && (\n        <Tooltip>\n          <TooltipTrigger\n            render={\n              <ToolbarButton\n                render={\n                  <Button\n                    size=\"icon-xs\"\n                    variant=\"ghost\"\n                    className=\"pointer-coarse:size-8 rounded-full before:rounded-full\"\n                    aria-label={replyLabel}\n                    onMouseDown={(event) => {\n                      if (event.button === 0) event.preventDefault();\n                    }}\n                    onClick={onReply}\n                  />\n                }\n              />\n            }\n          >\n            <CornerDownLeftIcon aria-hidden=\"true\" />\n          </TooltipTrigger>\n          <TooltipPopup>{replyTooltip}</TooltipPopup>\n        </Tooltip>\n      )}\n      {menuChildren && (\n        <Menu\n          open={menuOpen}\n          onOpenChange={(open, details) => {\n            setMenuDismissedOutside(\n              details.reason === \"outside-press\" ||\n                details.reason === \"focus-out\",\n            );\n            setMenuOpen(open);\n          }}\n        >\n          <Tooltip>\n            <TooltipTrigger\n              render={\n                <ToolbarButton\n                  render={\n                    <MenuTrigger\n                      ref={menuTriggerRef}\n                      render={\n                        <Button\n                          size=\"icon-xs\"\n                          variant=\"ghost\"\n                          className=\"pointer-coarse:size-8 rounded-full before:rounded-full\"\n                          aria-label={moreLabel}\n                          data-message-actions=\"\"\n                        />\n                      }\n                    />\n                  }\n                />\n              }\n            >\n              <EllipsisIcon aria-hidden=\"true\" />\n            </TooltipTrigger>\n            <TooltipPopup>{moreTooltip}</TooltipPopup>\n          </Tooltip>\n          <MenuPopup\n            anchor={toolbarRef}\n            align={side === \"outgoing\" ? \"start\" : \"end\"}\n            finalFocus={menuDismissedOutside ? false : menuFinalFocus}\n          >\n            {menuChildren}\n          </MenuPopup>\n        </Menu>\n      )}\n    </Toolbar>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message-actions.tsx"
    },
    {
      "path": "registry/default/chat/chat-message-bubble.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { ChatBubble, type ChatBubbleProps } from \"./chat-bubble\";\n\nexport type ChatMessageBubbleProps = Omit<\n  ChatBubbleProps,\n  \"align\" | \"variant\" | \"grouped\" | \"render\"\n> & {\n  side: \"incoming\" | \"outgoing\";\n  firstInGroup?: boolean;\n  animateEntrance?: boolean;\n  highlighted?: boolean;\n  onAnimationComplete?: () => void;\n};\n\nexport function ChatMessageBubble({\n  side,\n  firstInGroup = true,\n  animateEntrance = false,\n  highlighted = false,\n  children,\n  className,\n  onAnimationComplete,\n  ...props\n}: ChatMessageBubbleProps): ReactElement {\n  const reduceMotion = useReducedMotion();\n  const entranceOrigin =\n    side === \"outgoing\"\n      ? \"origin-top-right rtl:origin-top-left\"\n      : \"origin-top-left rtl:origin-top-right\";\n  return (\n    <ChatBubble\n      {...props}\n      align={side === \"outgoing\" ? \"end\" : \"start\"}\n      variant={side === \"outgoing\" ? \"default\" : \"muted\"}\n      grouped={!firstInGroup}\n      data-slot=\"chat-message-bubble\"\n      className={cn(highlighted ? \"origin-center\" : entranceOrigin, className)}\n      render={\n        <motion.div\n          initial={\n            animateEntrance && !reduceMotion\n              ? { transform: \"scale(0.92)\" }\n              : false\n          }\n          animate={highlighted && !reduceMotion ? \"attention\" : \"rest\"}\n          variants={{\n            rest: {\n              transform: \"scale(1)\",\n              transition: reduceMotion\n                ? { duration: 0 }\n                : { type: \"spring\", duration: 0.35, bounce: 0.45 },\n            },\n            attention: {\n              transform: [\"scale(1)\", \"scale(1.05)\", \"scale(1)\"],\n              transition: {\n                duration: 0.42,\n                times: [0, 0.28, 1],\n                ease: [\n                  [0.16, 1, 0.3, 1],\n                  [0.34, 1.56, 0.64, 1],\n                ],\n              },\n            },\n          }}\n          onAnimationComplete={onAnimationComplete}\n        />\n      }\n    >\n      {children}\n      {highlighted && (\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit] ring-2 ring-foreground/25 ring-offset-2 ring-offset-background\"\n          initial={{ opacity: 0 }}\n          animate={{ opacity: [0, 1, 0] }}\n          transition={{ duration: 0.42, times: [0, 0.28, 1], ease: \"easeOut\" }}\n        />\n      )}\n    </ChatBubble>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message-bubble.tsx"
    },
    {
      "path": "registry/default/chat/chat-message-info.tsx",
      "content": "\"use client\";\n\nimport type { ReactElement, ReactNode } from \"react\";\nimport { type ChatMemberEntry, ChatMemberSection } from \"./chat-members\";\nimport { ChatMessageFooter } from \"./chat-message-layout\";\n\nexport type ChatMessageInfoProps = {\n  message: ReactNode;\n  metadata: ReactNode;\n  groups: readonly {\n    id: string;\n    label: string;\n    icon?: ReactNode;\n    members: readonly ChatMemberEntry[];\n  }[];\n};\n\nexport function ChatMessageInfo({\n  message,\n  metadata,\n  groups,\n}: ChatMessageInfoProps): ReactElement {\n  return (\n    <div data-slot=\"chat-message-info\">\n      <div className=\"mb-7 flex flex-col items-end gap-1\">\n        <div className=\"w-fit min-w-0 max-w-[83%]\">{message}</div>\n        <ChatMessageFooter>{metadata}</ChatMessageFooter>\n      </div>\n      <div className=\"flex flex-col gap-7\">\n        {groups.map((group) => (\n          <ChatMemberSection\n            key={group.id}\n            label={group.label}\n            heading={\n              <>\n                {group.icon}\n                {group.label}\n              </>\n            }\n            members={group.members}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message-info.tsx"
    },
    {
      "path": "registry/default/chat/chat-message-layout.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatMessageLayoutProps = useRender.ComponentProps<\"article\"> & {\n  align?: \"start\" | \"end\";\n};\n\nexport function ChatMessageLayout({\n  align = \"start\",\n  className,\n  render,\n  ...props\n}: ChatMessageLayoutProps): ReactElement {\n  const defaultProps = {\n    \"data-slot\": \"chat-message\",\n    \"data-align\": align,\n    className: cn(\n      \"group/message relative grid scroll-m-8 grid-cols-1 items-start rounded-xl outline-none focus-visible:ring-2 focus-visible:ring-ring has-[>[data-slot=chat-message-avatar]]:grid-cols-[auto_minmax(0,1fr)] has-[>[data-slot=chat-message-avatar]]:gap-x-2 has-[>[data-slot=chat-message-header]]:gap-y-1\",\n      className,\n    ),\n  };\n\n  return useRender({\n    defaultTagName: \"article\",\n    render,\n    props: mergeProps<\"article\">(defaultProps, props),\n  });\n}\n\nexport function ChatMessageAvatar({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-message-avatar\"\n      className={cn(\"col-start-1 row-start-2 w-7\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function ChatMessageHeader({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-message-header\"\n      className={cn(\n        \"col-start-1 in-[[data-slot=chat-message]:has(>[data-slot=chat-message-avatar])]:col-start-2 row-start-1 px-1 font-medium text-muted-foreground text-xs group-data-[align=end]/message:justify-self-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ChatMessageContent({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-message-content\"\n      className={cn(\n        \"col-start-1 in-[[data-slot=chat-message]:has(>[data-slot=chat-message-avatar])]:col-start-2 row-start-2 flex min-w-0 flex-col gap-1 group-data-[align=end]/message:items-end\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function ChatMessageFooter({\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-message-footer\"\n      className={cn(\n        \"flex items-center gap-1.5 px-1 pt-0.5 text-[.625rem] text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message-layout.tsx"
    },
    {
      "path": "registry/default/chat/chat-message-text.tsx",
      "content": "import type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatMessageTextProps = Omit<ComponentProps<\"p\">, \"children\"> & {\n  text: string;\n  mentions?: readonly string[];\n};\n\nexport function ChatMessageText({\n  text,\n  mentions = [],\n  className,\n  ...props\n}: ChatMessageTextProps): ReactElement {\n  const names = new Set(mentions.map((name) => `@${name}`));\n  const parts = text.split(/(@[\\p{L}\\p{N}_.-]+)/u);\n  return (\n    <p\n      className={cn(\n        \"m-0 whitespace-pre-wrap break-words text-sm leading-[1.55]\",\n        className,\n      )}\n      {...props}\n    >\n      {parts.map((part, index) => {\n        if (index > 0 && !/(?:^|\\s)$/.test(parts[index - 1] ?? \"\")) return part;\n        const mention = names.has(part) ? part : part.replace(/[.-]+$/, \"\");\n        if (!names.has(mention)) return part;\n        return (\n          <span key={`${index}-${part}`}>\n            <span className=\"font-medium\">{mention}</span>\n            {part.slice(mention.length)}\n          </span>\n        );\n      })}\n    </p>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message-text.tsx"
    },
    {
      "path": "registry/default/chat/chat-message.tsx",
      "content": "\"use client\";\n\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { ChatBubbleReactions } from \"./chat-bubble\";\nimport {\n  ChatMessageBubble,\n  type ChatMessageBubbleProps,\n} from \"./chat-message-bubble\";\nimport {\n  ChatMessageAvatar,\n  ChatMessageContent,\n  ChatMessageFooter,\n  ChatMessageHeader,\n  ChatMessageLayout,\n  type ChatMessageLayoutProps,\n} from \"./chat-message-layout\";\nimport { ChatMessageText } from \"./chat-message-text\";\n\ntype ChatMessageAnimationProps = Pick<\n  ChatMessageBubbleProps,\n  \"animateEntrance\" | \"highlighted\" | \"onAnimationComplete\"\n>;\n\nexport type ChatMessageProps = Omit<\n  ChatMessageLayoutProps,\n  \"align\" | \"children\"\n> &\n  ChatMessageAnimationProps & {\n    side: \"incoming\" | \"outgoing\";\n    author?: ReactNode;\n    avatar?: ReactNode;\n    grouped?: boolean;\n    lifted?: boolean;\n    children: ReactNode;\n    actions?: ReactNode;\n    reactions?: ReactNode;\n    footer?: ReactNode;\n    bubble?: boolean;\n    bubbleProps?: Omit<\n      ChatMessageBubbleProps,\n      \"side\" | \"firstInGroup\" | \"children\" | keyof ChatMessageAnimationProps\n    >;\n  };\n\nexport function ChatMessage({\n  side,\n  author,\n  avatar,\n  grouped = false,\n  lifted = false,\n  children,\n  actions,\n  reactions,\n  footer,\n  bubble = true,\n  bubbleProps,\n  animateEntrance,\n  highlighted,\n  onAnimationComplete,\n  ...props\n}: ChatMessageProps): ReactElement {\n  const own = side === \"outgoing\";\n  return (\n    <ChatMessageLayout align={own ? \"end\" : \"start\"} {...props}>\n      {!own && !grouped && author && (\n        <ChatMessageHeader className={lifted ? \"invisible\" : undefined}>\n          {author}\n        </ChatMessageHeader>\n      )}\n      {!own && avatar && (\n        <ChatMessageAvatar className={lifted ? \"invisible\" : undefined}>\n          {!grouped && avatar}\n        </ChatMessageAvatar>\n      )}\n      <ChatMessageContent>\n        {bubble ? (\n          <div\n            className={cn(\n              \"flex w-full min-w-0 items-center gap-2\",\n              own && \"flex-row-reverse\",\n            )}\n          >\n            <div className=\"relative min-w-0 max-w-[83%] shrink\">\n              <ChatMessageBubble\n                {...bubbleProps}\n                side={side}\n                firstInGroup={!grouped}\n                animateEntrance={animateEntrance}\n                highlighted={highlighted}\n                onAnimationComplete={onAnimationComplete}\n                className={cn(lifted && \"invisible\", bubbleProps?.className)}\n              >\n                {typeof children === \"string\" ? (\n                  <ChatMessageText text={children} />\n                ) : (\n                  children\n                )}\n              </ChatMessageBubble>\n            </div>\n            {actions}\n          </div>\n        ) : (\n          children\n        )}\n        {reactions && <ChatBubbleReactions>{reactions}</ChatBubbleReactions>}\n        {footer && <ChatMessageFooter>{footer}</ChatMessageFooter>}\n      </ChatMessageContent>\n    </ChatMessageLayout>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-message.tsx"
    },
    {
      "path": "registry/default/chat/chat-reaction-chip.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n  Popover,\n  PopoverPopup,\n  PopoverTrigger,\n} from \"@/registry/default/ui/popover\";\nimport { Toggle } from \"@/registry/default/ui/toggle\";\nimport { useChatPopupDismissal } from \"./hooks/use-chat-popup-dismissal\";\n\nexport type ChatReactionChipProps = {\n  animateEntrance?: boolean;\n  emoji: string;\n  count: number;\n  selected: boolean;\n  label: string;\n  onClick: () => void;\n  reactorContent?: ReactNode;\n  reactorLabel?: string;\n  popupAlign?: ComponentProps<typeof PopoverPopup>[\"align\"];\n  className?: string;\n};\n\nexport function ChatReactionChip({\n  animateEntrance = false,\n  emoji,\n  count,\n  selected,\n  label,\n  onClick,\n  className,\n  reactorContent,\n  reactorLabel,\n  popupAlign,\n}: ChatReactionChipProps): ReactElement {\n  const reduceMotion = useReducedMotion();\n  const [animateReaction, setAnimateReaction] = useState(false);\n  const previouslySelected = useRef(selected);\n  useEffect(() => {\n    setAnimateReaction(selected && !previouslySelected.current);\n    previouslySelected.current = selected;\n  }, [selected]);\n\n  const [open, setOpen] = useState(false);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const anchorRef = useRef<HTMLSpanElement>(null);\n  const popupRef = useRef<HTMLDivElement>(null);\n  const holdTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const touchOrigin = useRef<{ x: number; y: number } | null>(null);\n  const suppressClick = useRef(false);\n  const clearHold = useCallback((): void => {\n    if (holdTimer.current !== null) clearTimeout(holdTimer.current);\n    holdTimer.current = null;\n  }, []);\n\n  useEffect(() => clearHold, [clearHold]);\n\n  useChatPopupDismissal(open, triggerRef, () => {\n    clearHold();\n    if (touchOrigin.current) suppressClick.current = true;\n    touchOrigin.current = null;\n    setOpen(false);\n  });\n\n  const chip = (\n    <Toggle\n      data-slot=\"chat-reaction-chip\"\n      variant=\"outline\"\n      size=\"sm\"\n      aria-label={label}\n      pressed={selected}\n      className={cn(\n        \"h-6 min-w-0 gap-1 rounded-full px-2 text-xs shadow-none transition-transform duration-150 [-webkit-touch-callout:none] before:rounded-full active:scale-95 data-pressed:border-primary/48 data-pressed:bg-primary/12 motion-reduce:scale-100 motion-reduce:transition-none sm:h-6 sm:min-w-0 sm:text-xs dark:data-pressed:bg-primary/8\",\n        className,\n      )}\n      onPressedChange={(_, details) => {\n        if (suppressClick.current) {\n          suppressClick.current = false;\n          details.cancel();\n          return;\n        }\n        onClick();\n      }}\n    >\n      <motion.span\n        className=\"inline-flex\"\n        initial={false}\n        animate={\n          animateReaction && !reduceMotion\n            ? {\n                transform: [\n                  \"scale(1) rotate(0deg)\",\n                  \"scale(1.55) rotate(-12deg)\",\n                  \"scale(1.12) rotate(6deg)\",\n                  \"scale(1) rotate(0deg)\",\n                ],\n              }\n            : { transform: \"scale(1) rotate(0deg)\" }\n        }\n        transition={{\n          duration: reduceMotion ? 0 : 0.44,\n          times: [0, 0.35, 0.65, 1],\n          ease: \"easeOut\",\n        }}\n      >\n        {emoji}\n      </motion.span>\n      <span className=\"tabular-nums\">{count}</span>\n    </Toggle>\n  );\n\n  const content = !reactorContent ? (\n    chip\n  ) : (\n    <Popover\n      open={open}\n      onOpenChange={(nextOpen, details) => {\n        if (details.reason === \"trigger-press\") {\n          details.cancel();\n          return;\n        }\n        if (\n          details.reason === \"trigger-hover\" &&\n          !window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches\n        ) {\n          details.cancel();\n          return;\n        }\n        setOpen(nextOpen);\n      }}\n    >\n      <span ref={anchorRef} className=\"inline-flex\">\n        <PopoverTrigger\n          ref={triggerRef}\n          openOnHover\n          delay={300}\n          closeDelay={150}\n          render={chip}\n          onFocus={(event) => {\n            if (\n              event.currentTarget.matches(\":focus-visible\") &&\n              !touchOrigin.current\n            )\n              setOpen(true);\n          }}\n          onBlur={(event) => {\n            if (!popupRef.current?.contains(event.relatedTarget))\n              setOpen(false);\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\" || event.key === \" \")\n              suppressClick.current = false;\n          }}\n          onPointerDown={(event) => {\n            suppressClick.current = false;\n            if (event.pointerType !== \"touch\" || !event.isPrimary) return;\n            touchOrigin.current = { x: event.clientX, y: event.clientY };\n            clearHold();\n            holdTimer.current = setTimeout(() => {\n              holdTimer.current = null;\n              suppressClick.current = true;\n              setOpen(true);\n            }, 500);\n          }}\n          onPointerMove={(event) => {\n            const origin = touchOrigin.current;\n            if (\n              !origin ||\n              Math.hypot(event.clientX - origin.x, event.clientY - origin.y) <=\n                8\n            )\n              return;\n            clearHold();\n            suppressClick.current = true;\n            touchOrigin.current = null;\n            setOpen(false);\n          }}\n          onPointerUp={() => {\n            clearHold();\n            touchOrigin.current = null;\n          }}\n          onPointerCancel={() => {\n            clearHold();\n            touchOrigin.current = null;\n            suppressClick.current = true;\n            setOpen(false);\n          }}\n          onContextMenu={(event) => {\n            if (touchOrigin.current || suppressClick.current)\n              event.preventDefault();\n          }}\n        />\n      </span>\n      <PopoverPopup\n        anchor={anchorRef}\n        align={popupAlign}\n        ref={popupRef}\n        initialFocus={false}\n        finalFocus={false}\n        aria-label={reactorLabel}\n        className=\"w-52 **:data-[slot=popover-viewport]:p-1.5\"\n      >\n        {reactorContent}\n      </PopoverPopup>\n    </Popover>\n  );\n\n  return (\n    <motion.span\n      className={cn(\n        \"inline-flex origin-top\",\n        animateEntrance && !reduceMotion && \"will-change-transform\",\n      )}\n      initial={\n        animateEntrance && !reduceMotion\n          ? { opacity: 0, transform: \"scale(0.55)\" }\n          : false\n      }\n      animate={{ opacity: 1, transform: \"scale(1)\" }}\n      transition={\n        reduceMotion\n          ? { duration: 0 }\n          : {\n              transform: { type: \"spring\", duration: 0.44, bounce: 0.45 },\n              opacity: { duration: 0.12 },\n            }\n      }\n    >\n      {content}\n    </motion.span>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-reaction-chip.tsx"
    },
    {
      "path": "registry/default/chat/chat-reaction-picker.tsx",
      "content": "\"use client\";\n\nimport { HeartIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { useRef, useState } from \"react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  Popover,\n  PopoverPopup,\n  PopoverTrigger,\n} from \"@/registry/default/ui/popover\";\nimport { ToolbarButton } from \"@/registry/default/ui/toolbar\";\nimport {\n  Tooltip,\n  TooltipPopup,\n  TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\nimport { useChatPopupDismissal } from \"./hooks/use-chat-popup-dismissal\";\n\nexport type ChatReactionPickerProps = {\n  inToolbar?: boolean;\n  options: readonly { emoji: string; label: string }[];\n  selectedEmojis: readonly string[];\n  triggerLabel: string;\n  triggerTooltip: string;\n  popupLabel: string;\n  popupAlign?: ComponentProps<typeof PopoverPopup>[\"align\"];\n  popupAnchor?: ComponentProps<typeof PopoverPopup>[\"anchor\"];\n  onSelect: (emoji: string) => void;\n};\n\nexport function ChatReactionPicker({\n  inToolbar = false,\n  options,\n  selectedEmojis,\n  triggerLabel,\n  triggerTooltip,\n  popupLabel,\n  popupAlign,\n  popupAnchor,\n  onSelect,\n}: ChatReactionPickerProps): ReactElement {\n  const [open, setOpen] = useState(false);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n\n  useChatPopupDismissal(open, triggerRef, () => setOpen(false));\n\n  const trigger = (\n    <PopoverTrigger\n      ref={triggerRef}\n      render={\n        <Button\n          size=\"icon-xs\"\n          variant=\"ghost\"\n          className=\"pointer-coarse:size-8 rounded-full before:rounded-full\"\n          aria-label={triggerLabel}\n          onMouseDown={(event) => {\n            if (event.button === 0) event.preventDefault();\n          }}\n        />\n      }\n    />\n  );\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <Tooltip>\n        <TooltipTrigger\n          render={inToolbar ? <ToolbarButton render={trigger} /> : trigger}\n        >\n          <HeartIcon aria-hidden=\"true\" className=\"size-3.5\" />\n        </TooltipTrigger>\n        <TooltipPopup>{triggerTooltip}</TooltipPopup>\n      </Tooltip>\n      <PopoverPopup\n        anchor={popupAnchor}\n        side=\"top\"\n        align={popupAlign}\n        aria-label={popupLabel}\n        initialFocus={(interaction) => interaction === \"keyboard\"}\n        finalFocus={(interaction) => interaction === \"keyboard\"}\n        className=\"transform-gpu rounded-full before:rounded-full **:data-[slot=popover-viewport]:p-1\"\n      >\n        <div className=\"flex\">\n          {options.map(({ emoji, label }) => (\n            <Button\n              key={emoji}\n              aria-label={label}\n              aria-pressed={selectedEmojis.includes(emoji)}\n              variant=\"ghost\"\n              size=\"icon-lg\"\n              className=\"rounded-full before:rounded-full\"\n              onMouseDown={(event) => {\n                if (event.button === 0) event.preventDefault();\n              }}\n              onClick={() => {\n                onSelect(emoji);\n                setOpen(false);\n              }}\n            >\n              <span aria-hidden=\"true\" className=\"text-lg leading-none\">\n                {emoji}\n              </span>\n            </Button>\n          ))}\n        </div>\n      </PopoverPopup>\n    </Popover>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-reaction-picker.tsx"
    },
    {
      "path": "registry/default/chat/chat-reply-preview.tsx",
      "content": "\"use client\";\n\nimport { XIcon } from \"lucide-react\";\nimport { motion, usePresence, useReducedMotion } from \"motion/react\";\nimport type { ReactElement, ReactNode, RefObject } from \"react\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\";\nimport {\n  Tooltip,\n  TooltipPopup,\n  TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\nimport { ChatMessageBubble } from \"./chat-message-bubble\";\n\nexport type ChatReplyTarget = {\n  source: HTMLDivElement;\n  width: number;\n  firstInGroup: boolean;\n};\n\nexport type ChatReplyPreviewProps = {\n  target: ChatReplyTarget;\n  side: \"incoming\" | \"outgoing\";\n  avatar?: ReactNode;\n  author: ReactNode;\n  replyLabel: ReactNode;\n  cancelLabel: string;\n  children: ReactNode;\n  dismissalRef: RefObject<\"cancel\" | \"send\">;\n  onCancel: () => void;\n};\n\nexport function ChatReplyPreview({\n  target,\n  side,\n  avatar,\n  author,\n  replyLabel,\n  cancelLabel,\n  children,\n  dismissalRef,\n  onCancel,\n}: ChatReplyPreviewProps): ReactElement {\n  const anchorRef = useRef<HTMLDivElement>(null);\n  const bubbleRef = useRef<HTMLDivElement>(null);\n  const [origin, setOrigin] = useState<number | null>(null);\n  const reduceMotion = useReducedMotion();\n  const [isPresent, safeToRemove] = usePresence();\n  const own = side === \"outgoing\";\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: Remeasure the source on exit after the viewport or keyboard may have moved.\n  useLayoutEffect(() => {\n    const anchor = anchorRef.current;\n    const bubble = bubbleRef.current;\n    if (!anchor || !bubble) return;\n    const destination = anchor.getBoundingClientRect();\n    const source = target.source.getBoundingClientRect();\n    setOrigin(source.top - destination.top - bubble.offsetTop);\n  }, [isPresent, target]);\n  const returning =\n    dismissalRef.current === \"cancel\" && target.source.isConnected;\n\n  return (\n    <>\n      <motion.button\n        type=\"button\"\n        tabIndex={-1}\n        aria-label={cancelLabel}\n        className=\"absolute inset-0 z-40 cursor-default touch-none bg-popover/32 backdrop-blur-sm before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-6 before:bg-linear-to-b before:from-popover before:to-transparent before:content-['']\"\n        initial={{ opacity: 0 }}\n        animate={{ opacity: isPresent ? 1 : 0 }}\n        transition={{ duration: reduceMotion ? 0 : 0.22 }}\n        onClick={onCancel}\n        onMouseDown={(event) => {\n          if (event.button === 0) event.preventDefault();\n        }}\n        onWheel={(event) => event.preventDefault()}\n      />\n      <div className=\"pointer-events-none absolute inset-0 z-50 overflow-clip\">\n        <ScrollArea\n          overscrollContain\n          className=\"**:data-[slot=scroll-area-content]:flex **:data-[slot=scroll-area-content]:min-h-full **:data-[slot=scroll-area-content]:flex-col **:data-[slot=scroll-area-content]:py-4 [&_[data-slot=scroll-area-scrollbar]]:pointer-events-auto\"\n        >\n          <div\n            ref={anchorRef}\n            data-slot=\"chat-reply-focus\"\n            className=\"relative mx-4 mt-auto shrink-0\"\n          >\n            <motion.div\n              aria-hidden=\"true\"\n              className=\"absolute -inset-x-4 -top-20 -bottom-4 bg-linear-to-t from-[calc(100%-4rem)] from-popover to-transparent\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: isPresent ? 1 : 0 }}\n              transition={{ duration: reduceMotion ? 0 : 0.22 }}\n            />\n            <motion.div\n              key={origin === null ? \"measuring\" : \"positioned\"}\n              initial={\n                reduceMotion\n                  ? false\n                  : { transform: `translateY(${origin ?? 0}px)`, opacity: 1 }\n              }\n              animate={\n                isPresent\n                  ? { transform: \"translateY(0px)\", opacity: 1 }\n                  : {\n                      transform: returning\n                        ? `translateY(${origin ?? 0}px)`\n                        : \"translateY(6px)\",\n                      opacity: returning ? 1 : 0,\n                    }\n              }\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : isPresent || returning\n                    ? {\n                        type: \"tween\",\n                        duration: 0.35,\n                        ease: [0.3, 0.84, 0.24, 1],\n                      }\n                    : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }\n              }\n              onAnimationComplete={() => {\n                if (!isPresent) safeToRemove?.();\n              }}\n              className={cn(\n                \"relative grid items-start gap-y-1\",\n                own\n                  ? \"grid-cols-1\"\n                  : \"grid-cols-[1.75rem_minmax(0,1fr)] gap-x-2\",\n                origin === null && \"invisible\",\n              )}\n            >\n              {!own && (\n                <motion.div\n                  className=\"col-start-1 row-start-2 w-7\"\n                  initial={{ opacity: target.firstInGroup ? 1 : 0 }}\n                  animate={{\n                    opacity: isPresent || target.firstInGroup ? 1 : 0,\n                  }}\n                  transition={{ duration: reduceMotion ? 0 : 0.18 }}\n                >\n                  {avatar}\n                </motion.div>\n              )}\n              <motion.div\n                className={cn(\n                  \"flex h-lh min-w-0 max-w-full items-center gap-2 px-1 font-medium text-muted-foreground text-xs\",\n                  own ? \"justify-self-end\" : \"col-start-2 row-start-1\",\n                )}\n                initial={{ opacity: !own && target.firstInGroup ? 1 : 0 }}\n                animate={{\n                  opacity: isPresent || (!own && target.firstInGroup) ? 1 : 0,\n                }}\n                transition={{ duration: reduceMotion ? 0 : 0.18 }}\n              >\n                <span role=\"status\" className=\"min-w-0 truncate\">\n                  {isPresent ? replyLabel : author}\n                </span>\n              </motion.div>\n              <div\n                ref={bubbleRef}\n                style={{ width: target.width }}\n                className={cn(\n                  \"pointer-events-auto relative row-start-2 max-w-full rounded-2xl bg-popover\",\n                  own ? \"justify-self-end\" : \"col-start-2\",\n                  target.firstInGroup &&\n                    (own ? \"rounded-se-md\" : \"rounded-ss-md\"),\n                )}\n              >\n                <ChatMessageBubble\n                  side={own ? \"outgoing\" : \"incoming\"}\n                  firstInGroup={target.firstInGroup}\n                >\n                  {children}\n                </ChatMessageBubble>\n                <motion.div\n                  className={cn(\n                    \"absolute -top-2.5 z-10 flex rounded-full bg-popover\",\n                    own ? \"-start-2.5\" : \"-end-2.5\",\n                  )}\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: isPresent ? 1 : 0 }}\n                  transition={{ duration: reduceMotion ? 0 : 0.18 }}\n                >\n                  <Tooltip>\n                    <TooltipTrigger\n                      render={\n                        <Button\n                          type=\"button\"\n                          size=\"icon-xs\"\n                          variant=\"outline\"\n                          aria-label={cancelLabel}\n                          className=\"size-6 rounded-full before:rounded-full\"\n                          onMouseDown={(event) => {\n                            if (event.button === 0) event.preventDefault();\n                          }}\n                          onClick={onCancel}\n                        />\n                      }\n                    >\n                      <XIcon aria-hidden=\"true\" />\n                    </TooltipTrigger>\n                    <TooltipPopup>{cancelLabel}</TooltipPopup>\n                  </Tooltip>\n                </motion.div>\n              </div>\n            </motion.div>\n          </div>\n        </ScrollArea>\n      </div>\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-reply-preview.tsx"
    },
    {
      "path": "registry/default/chat/chat-reply-reference.tsx",
      "content": "\"use client\";\n\nimport { CornerDownRightIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatReplyReferenceProps = Omit<\n  ComponentProps<\"button\">,\n  \"children\"\n> & {\n  author: string;\n  preview: string;\n};\n\nexport function ChatReplyReference({\n  author,\n  preview,\n  className,\n  ...props\n}: ChatReplyReferenceProps): ReactElement {\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"chat-reply-reference\"\n      className={cn(\n        \"group/reply-reference flex min-w-0 items-start gap-1.5 text-start text-xs outline-none transition-colors focus-visible:rounded-sm focus-visible:text-primary focus-visible:ring-2 focus-visible:ring-current enabled:hover:text-primary motion-reduce:transition-none\",\n        className,\n      )}\n      {...props}\n    >\n      <CornerDownRightIcon\n        aria-hidden=\"true\"\n        className=\"mt-0.5 size-3.5 shrink-0 opacity-64 transition-opacity group-hover/reply-reference:opacity-100 group-focus-visible/reply-reference:opacity-100\"\n      />\n      <span className=\"flex min-w-0 flex-col gap-0.5\">\n        <span className=\"font-medium\">{author}</span>\n        <span className=\"line-clamp-2 text-foreground/80\">{preview}</span>\n      </span>\n    </button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-reply-reference.tsx"
    },
    {
      "path": "registry/default/chat/chat-states.tsx",
      "content": "\"use client\";\n\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Children } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n  Empty,\n  EmptyContent,\n  EmptyDescription,\n  EmptyHeader,\n  EmptyMedia,\n  EmptyTitle,\n} from \"@/registry/default/ui/empty\";\nimport { Skeleton } from \"@/registry/default/ui/skeleton\";\nimport {\n  ChatMessageAvatar,\n  ChatMessageContent,\n  ChatMessageHeader,\n  ChatMessageLayout,\n} from \"./chat-message-layout\";\n\nexport type ChatEmptyStateProps = {\n  title: ReactNode;\n  description: ReactNode;\n  icon: ReactNode;\n  children?: ReactNode;\n};\n\nexport function ChatEmptyState({\n  title,\n  description,\n  icon,\n  children,\n}: ChatEmptyStateProps): ReactElement {\n  return (\n    <Empty role=\"status\" className=\"min-h-64 gap-4 py-10 md:py-10\">\n      <EmptyHeader>\n        <EmptyMedia variant=\"icon\" className=\"mb-2 bg-popover\">\n          {icon}\n        </EmptyMedia>\n        <EmptyTitle>{title}</EmptyTitle>\n        <EmptyDescription>{description}</EmptyDescription>\n      </EmptyHeader>\n      {Children.toArray(children).length > 0 && (\n        <EmptyContent>{children}</EmptyContent>\n      )}\n    </Empty>\n  );\n}\n\nconst LOADING_ROWS = [\n  { id: \"first\", side: \"incoming\" },\n  { id: \"second\", side: \"outgoing\" },\n  { id: \"third\", side: \"incoming\" },\n  { id: \"fourth\", side: \"outgoing\" },\n] as const;\n\nexport function ChatLoadingState({\n  label,\n  children,\n}: {\n  label: string;\n  children?: ReactNode;\n}): ReactElement {\n  return (\n    <div role=\"status\" className=\"flex flex-col gap-5 py-4\">\n      <span className=\"sr-only\">{label}</span>\n      {LOADING_ROWS.map(({ side, id }) => (\n        <ChatMessageLayout\n          key={id}\n          align={side === \"outgoing\" ? \"end\" : \"start\"}\n          aria-hidden=\"true\"\n        >\n          {side === \"incoming\" && (\n            <>\n              <ChatMessageHeader>\n                <Skeleton className=\"h-3 w-20 motion-reduce:animate-none\" />\n              </ChatMessageHeader>\n              <ChatMessageAvatar>\n                <Skeleton className=\"size-7 rounded-full motion-reduce:animate-none\" />\n              </ChatMessageAvatar>\n            </>\n          )}\n          <ChatMessageContent>\n            <Skeleton\n              className={cn(\n                \"h-11 w-56 max-w-[83%] rounded-2xl motion-reduce:animate-none\",\n                side === \"incoming\" ? \"rounded-ss-md\" : \"rounded-se-md\",\n              )}\n            />\n            <Skeleton className=\"mx-1 h-2 w-9 motion-reduce:animate-none\" />\n          </ChatMessageContent>\n        </ChatMessageLayout>\n      ))}\n      {children}\n    </div>\n  );\n}\n\nexport function ChatStatusBar({\n  title,\n  description,\n  action,\n}: {\n  title: ReactNode;\n  description: ReactNode;\n  action?: ReactNode;\n}): ReactElement {\n  return (\n    <div\n      role=\"status\"\n      className=\"flex shrink-0 items-center justify-between gap-3 border-border/60 border-y bg-muted/72 px-5 py-2.5\"\n    >\n      <div className=\"text-xs\">\n        <p className=\"m-0 font-medium\">{title}</p>\n        <p className=\"m-0 mt-1 text-muted-foreground\">{description}</p>\n      </div>\n      {action}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-states.tsx"
    },
    {
      "path": "registry/default/chat/chat-typing-indicator.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  usePresence,\n  useReducedMotion,\n} from \"motion/react\";\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { Fragment } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport type ChatTypingIndicatorProps = Omit<\n  ComponentProps<\"div\">,\n  \"children\"\n> & {\n  label: string;\n  visible?: boolean;\n};\n\nexport function ChatTypingIndicator({\n  label,\n  visible = true,\n  className,\n  ...props\n}: ChatTypingIndicatorProps): ReactElement {\n  return (\n    <AnimatePresence>\n      {visible && (\n        <Fragment key=\"typing\">\n          <ChatTypingIndicatorContent\n            label={label}\n            className={className}\n            {...props}\n          />\n        </Fragment>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction ChatTypingIndicatorContent({\n  label,\n  className,\n  ...props\n}: Omit<ChatTypingIndicatorProps, \"visible\">): ReactElement {\n  const reduceMotion = useReducedMotion();\n  const [isPresent, safeToRemove] = usePresence();\n  return (\n    <motion.div\n      className=\"overflow-hidden\"\n      initial={{ height: 0, opacity: 0 }}\n      animate={\n        isPresent ? { height: \"auto\", opacity: 1 } : { height: 0, opacity: 0 }\n      }\n      transition={{\n        duration: reduceMotion ? 0 : 0.2,\n        ease: [0.22, 1, 0.36, 1],\n      }}\n      onAnimationComplete={() => {\n        if (!isPresent) safeToRemove?.();\n      }}\n    >\n      <div\n        role=\"status\"\n        data-slot=\"chat-typing-indicator\"\n        className={cn(\"flex items-center gap-2.5\", className)}\n        {...props}\n      >\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"flex origin-bottom-left items-center gap-1 rounded-2xl rounded-es-md bg-muted/72 px-3 py-3.5 rtl:origin-bottom-right\"\n          initial={{ transform: reduceMotion ? \"scale(1)\" : \"scale(0.92)\" }}\n          animate={{\n            transform: isPresent || reduceMotion ? \"scale(1)\" : \"scale(0.96)\",\n          }}\n          transition={{\n            duration: reduceMotion ? 0 : 0.2,\n            ease: [0.22, 1, 0.36, 1],\n          }}\n        >\n          {[0, 1, 2].map((dot) => (\n            <motion.span\n              key={dot}\n              className=\"block size-1.5 rounded-full bg-muted-foreground\"\n              initial={{\n                transform: \"translateY(0px) scale(0.8)\",\n                opacity: 0.4,\n              }}\n              animate={\n                reduceMotion\n                  ? { transform: \"translateY(0px) scale(1)\", opacity: 1 }\n                  : {\n                      transform: [\n                        \"translateY(0px) scale(0.8)\",\n                        \"translateY(-2px) scale(1.1)\",\n                        \"translateY(0px) scale(0.8)\",\n                        \"translateY(0px) scale(0.8)\",\n                      ],\n                      opacity: [0.4, 1, 0.4, 0.4],\n                    }\n              }\n              transition={\n                reduceMotion\n                  ? { duration: 0 }\n                  : {\n                      duration: 1.4,\n                      times: [0, 0.22, 0.44, 1],\n                      delay: dot * 0.16,\n                      repeat: Number.POSITIVE_INFINITY,\n                      ease: \"easeInOut\",\n                    }\n              }\n            />\n          ))}\n        </motion.div>\n        <span className=\"min-w-0 text-muted-foreground text-xs\">{label}</span>\n      </div>\n    </motion.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-typing-indicator.tsx"
    },
    {
      "path": "registry/default/chat/chat-unread-divider.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps, ReactElement } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\n\nexport function ChatUnreadDivider({\n  children,\n  className,\n  ...props\n}: ComponentProps<\"div\">): ReactElement {\n  return (\n    <div\n      data-slot=\"chat-unread-divider\"\n      className={cn(\n        \"my-4 flex items-center gap-3 font-medium text-foreground text-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"h-px flex-1 bg-input\" />\n      {children}\n      <span aria-hidden=\"true\" className=\"h-px flex-1 bg-input\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat/chat-unread-divider.tsx"
    },
    {
      "path": "registry/default/chat/hooks/use-chat-popup-dismissal.ts",
      "content": "\"use client\";\n\nimport { type RefObject, useEffect, useRef } from \"react\";\n\nexport function useChatPopupDismissal(\n  active: boolean,\n  triggerRef: RefObject<HTMLElement | null>,\n  onDismiss: () => void,\n): void {\n  const onDismissRef = useRef(onDismiss);\n  useEffect(() => {\n    onDismissRef.current = onDismiss;\n  }, [onDismiss]);\n\n  useEffect(() => {\n    const trigger = triggerRef.current;\n    if (!active || !trigger) return;\n    const ownerDocument = trigger.ownerDocument;\n    const onScrollInput = (event: Event): void => {\n      if (!(event.target instanceof Element)) return;\n      if (event.target.closest('[role=\"menu\"], [data-slot=\"popover-popup\"]'))\n        return;\n      if (event instanceof KeyboardEvent) {\n        if (\n          event.defaultPrevented ||\n          ![\n            \"ArrowUp\",\n            \"ArrowDown\",\n            \"ArrowLeft\",\n            \"ArrowRight\",\n            \"PageUp\",\n            \"PageDown\",\n            \"Home\",\n            \"End\",\n            \" \",\n          ].includes(event.key) ||\n          event.altKey ||\n          event.ctrlKey ||\n          event.metaKey ||\n          event.target.closest(\n            \"input, textarea, select, button, a, [contenteditable], [role=menu], [role=listbox], [role=combobox], [role=slider], [role=tablist]\",\n          )\n        )\n          return;\n      }\n      const isScrollable = (element: Element): boolean => {\n        const style = getComputedStyle(element);\n        const page = element === ownerDocument.scrollingElement;\n        return (\n          (element.scrollHeight > element.clientHeight &&\n            (page || /auto|scroll/.test(style.overflowY))) ||\n          (element.scrollWidth > element.clientWidth &&\n            (page || /auto|scroll/.test(style.overflowX)))\n        );\n      };\n      // Dismiss on input in the scroller, never on automatic scrolling or menu navigation.\n      for (\n        let element: Element | null = event.target;\n        element;\n        element = element.parentElement\n      ) {\n        if (!isScrollable(element)) continue;\n        if (element.contains(trigger)) onDismissRef.current();\n        return;\n      }\n      // Modal menus intercept wheel/touch input with a backdrop over the scroller.\n      const point =\n        event instanceof WheelEvent\n          ? event\n          : event instanceof TouchEvent\n            ? event.touches[0]\n            : undefined;\n      if (!point) return;\n      for (\n        let element = trigger.parentElement;\n        element;\n        element = element.parentElement\n      ) {\n        if (!isScrollable(element)) continue;\n        const bounds = element.getBoundingClientRect();\n        if (\n          point.clientX >= bounds.left &&\n          point.clientX <= bounds.right &&\n          point.clientY >= bounds.top &&\n          point.clientY <= bounds.bottom\n        ) {\n          onDismissRef.current();\n          return;\n        }\n      }\n    };\n    const scrollInputs = [\"wheel\", \"touchmove\", \"keydown\"] as const;\n    for (const type of scrollInputs)\n      ownerDocument.addEventListener(type, onScrollInput, { passive: true });\n    return () => {\n      for (const type of scrollInputs)\n        ownerDocument.removeEventListener(type, onScrollInput);\n    };\n  }, [active, triggerRef]);\n}\n",
      "type": "registry:component",
      "target": "@components/chat/hooks/use-chat-popup-dismissal.ts"
    },
    {
      "path": "registry/default/chat/hooks/use-chat-scroll.ts",
      "content": "\"use client\";\n\nimport {\n  type UIEvent,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type ChatScrollOptions = {\n  active?: boolean;\n  paused?: boolean;\n  itemCount: number;\n  layoutKey?: string | number;\n  onReachEnd?: () => void;\n};\n\nexport function useChatScroll({\n  active = true,\n  paused = false,\n  itemCount,\n  layoutKey,\n  onReachEnd,\n}: ChatScrollOptions) {\n  const viewportRef = useRef<HTMLDivElement>(null);\n  const atBottomRef = useRef(true);\n  const [showJump, setShowJump] = useState(false);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: Follow content changes before scroll events can misinterpret the new height.\n  useLayoutEffect(() => {\n    const viewport = viewportRef.current;\n    if (!active || paused || !viewport) return;\n    // Follow before a scroll event can interpret newly added height as scrolling away.\n    if (atBottomRef.current) {\n      viewport.scrollTop = viewport.scrollHeight;\n      setShowJump(false);\n    } else {\n      setShowJump(\n        viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight > 64,\n      );\n    }\n  }, [active, paused, itemCount, layoutKey]);\n\n  useEffect(() => {\n    if (!active || paused) return;\n    let observer: ResizeObserver | undefined;\n    const frame = requestAnimationFrame(() => {\n      const viewport = viewportRef.current;\n      if (!viewport) return;\n      observer = new ResizeObserver(() => {\n        if (atBottomRef.current) viewport.scrollTop = viewport.scrollHeight;\n      });\n      observer.observe(viewport);\n      if (viewport.firstElementChild)\n        observer.observe(viewport.firstElementChild);\n    });\n    return () => {\n      cancelAnimationFrame(frame);\n      observer?.disconnect();\n    };\n  }, [active, paused]);\n\n  const updatePosition = (): void => {\n    const viewport = viewportRef.current;\n    atBottomRef.current = Boolean(\n      viewport &&\n        viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight < 64,\n    );\n  };\n\n  const onScroll = (_event: UIEvent<HTMLDivElement>): void => {\n    if (paused) return;\n    updatePosition();\n    setShowJump(!atBottomRef.current);\n    if (atBottomRef.current) onReachEnd?.();\n  };\n\n  const resetScroll = (atBottom = true): void => {\n    atBottomRef.current = atBottom;\n    setShowJump(!atBottom);\n    requestAnimationFrame(() => {\n      const viewport = viewportRef.current;\n      if (viewport) viewport.scrollTop = atBottom ? viewport.scrollHeight : 0;\n    });\n  };\n\n  const scrollToLatest = (): void => {\n    atBottomRef.current = true;\n    const viewport = viewportRef.current;\n    viewport?.scrollTo({\n      top: viewport.scrollHeight,\n      behavior: window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n        ? \"auto\"\n        : \"smooth\",\n    });\n  };\n\n  return {\n    viewportRef,\n    atBottomRef,\n    showJump,\n    onScroll,\n    resetScroll,\n    updatePosition,\n    scrollToLatest,\n  };\n}\n\nexport type ChatScrollController = ReturnType<typeof useChatScroll>;\n",
      "type": "registry:component",
      "target": "@components/chat/hooks/use-chat-scroll.ts"
    },
    {
      "path": "registry/default/chat/lib/split-chat-message-preview.ts",
      "content": "export function splitChatMessagePreview(\n  text: string,\n  maxPreviewLength = 500,\n): { preview: string; remainder: string | null } {\n  if (text.length <= maxPreviewLength)\n    return { preview: text, remainder: null };\n\n  const limit = Math.max(1, Math.floor(maxPreviewLength));\n  const paragraphBreak = text.lastIndexOf(\"\\n\\n\", limit);\n  let splitAt = limit;\n  if (paragraphBreak > limit / 2) {\n    splitAt = paragraphBreak;\n  } else {\n    for (let index = limit; index > 0; index--) {\n      if (/\\s/u.test(text[index] ?? \"\")) {\n        splitAt = index;\n        break;\n      }\n    }\n  }\n\n  if (typeof Intl.Segmenter === \"function\") {\n    const segments = new Intl.Segmenter(undefined, {\n      granularity: \"grapheme\",\n    }).segment(text);\n    for (const { index, segment } of segments) {\n      if (index + segment.length > splitAt) {\n        splitAt = index || segment.length;\n        break;\n      }\n    }\n  } else if (/^[\\uDC00-\\uDFFF]$/u.test(text[splitAt] ?? \"\")) {\n    splitAt = splitAt === 1 ? 2 : splitAt - 1;\n  }\n\n  return {\n    preview: text.slice(0, splitAt).trimEnd(),\n    remainder: text.slice(splitAt).trimStart() || null,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/chat/lib/split-chat-message-preview.ts"
    }
  ],
  "type": "registry:ui"
}