{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-demo",
  "description": "An interactive group chat with a responsive drawer and conversation states.",
  "dependencies": [
    "motion@14.0.0",
    "lucide-react"
  ],
  "registryDependencies": [
    "@coss/alert-dialog",
    "@coss/avatar",
    "@coss/badge",
    "@coss/button",
    "@coss/composer",
    "@coss/drawer",
    "@coss/menu",
    "@coss/tooltip",
    "@coss/use-media-query",
    "@coss/utils",
    "@coss/chat"
  ],
  "files": [
    {
      "path": "registry/default/chat-demo/chat-demo-composer.tsx",
      "content": "\"use client\";\n\nimport { ArrowUpIcon } from \"lucide-react\";\nimport type { ReactElement, ReactNode, RefObject } from \"react\";\nimport { useId, useMemo } from \"react\";\nimport { useMediaQuery } from \"@/registry/default/hooks/use-media-query\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  Composer,\n  ComposerActionGroup,\n  ComposerActions,\n  ComposerInput,\n  ComposerItem,\n  type ComposerItemValue,\n  ComposerList,\n  ComposerPopup,\n  ComposerSubmit,\n  type ComposerTrigger,\n  type ComposerValue,\n} from \"@/registry/default/ui/composer\";\nimport {\n  Tooltip,\n  TooltipPopup,\n  TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\";\nimport { ChatMemberAvatar } from \"./chat-demo-message\";\nimport type { MemberId } from \"./chat-demo-state\";\nimport { CHAT_MEMBERS } from \"./chat-demo-state\";\n\nfunction isMemberId(id: string): id is MemberId {\n  return Object.hasOwn(CHAT_MEMBERS, id);\n}\n\nfunction mentionMatchesHandle(item: ComposerItemValue, query: string): boolean {\n  return item.label.toLocaleLowerCase().startsWith(query.toLocaleLowerCase());\n}\n\nexport function ChatDemoComposer({\n  className,\n  value,\n  onValueChange,\n  replyAuthor,\n  placeholder = \"Message the group…\",\n  showKeyboardHint = true,\n  startAction,\n  mentionMembers,\n  onSend,\n  inputRef,\n  notice,\n  disabled = false,\n}: {\n  className?: string;\n  value: ComposerValue;\n  onValueChange: (value: ComposerValue) => void;\n  replyAuthor?: string;\n  placeholder?: string;\n  showKeyboardHint?: boolean;\n  startAction?: ReactNode;\n  mentionMembers: readonly MemberId[];\n  onSend: () => void;\n  inputRef: RefObject<HTMLDivElement | null>;\n  notice: string;\n  disabled?: boolean;\n}): ReactElement {\n  const hintId = useId();\n  const touchInput = useMediaQuery({ pointer: \"coarse\" });\n  const triggers = useMemo<ComposerTrigger[]>(\n    () => [\n      {\n        character: \"@\",\n        filter: mentionMatchesHandle,\n        items: mentionMembers.map((id) => ({\n          id,\n          label: CHAT_MEMBERS[id].handle,\n        })),\n      },\n    ],\n    [mentionMembers],\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative z-20 flex shrink-0 flex-col gap-2.5 px-4 pb-[max(1rem,env(safe-area-inset-bottom,0px))]\",\n        className,\n      )}\n    >\n      <Composer\n        className=\"[--composer-max-rows:4] data-disabled:opacity-50 max-md:data-[layout=adaptive]:not-data-multiline:px-1 max-md:[--composer-min-rows:1] max-md:[--composer-padding:--spacing(1)] max-md:[--composer-radius:calc(--spacing(5.5)+1px)] sm:max-md:[--composer-radius:calc(--spacing(5)+1px)] md:[--composer-max-rows:6] md:[--composer-min-rows:2] pointer-coarse:[&_[data-slot=composer-scroll-area]_[data-slot=scroll-area-scrollbar]]:hidden\"\n        disabled={disabled}\n        layout=\"adaptive\"\n        submitOnEnter=\"desktop\"\n        triggers={triggers}\n        value={value}\n        onSubmit={onSend}\n        onValueChange={onValueChange}\n      >\n        <ComposerInput\n          ref={inputRef}\n          aria-describedby={\n            showKeyboardHint && !touchInput ? hintId : undefined\n          }\n          aria-label=\"Message\"\n          className=\"max-md:px-2\"\n          placeholder={replyAuthor ? `Reply to ${replyAuthor}…` : placeholder}\n        />\n        <ComposerPopup>\n          <ComposerList>\n            {(item: ComposerItemValue) => {\n              if (!isMemberId(item.id)) return null;\n              return (\n                <ComposerItem key={item.id} value={item}>\n                  <ChatMemberAvatar\n                    className=\"size-4 text-[0.5rem]\"\n                    member={item.id}\n                  />\n                  <span className=\"min-w-0 truncate\">\n                    {CHAT_MEMBERS[item.id].name}\n                  </span>\n                </ComposerItem>\n              );\n            }}\n          </ComposerList>\n        </ComposerPopup>\n        <ComposerActions className=\"max-md:mx-1\">\n          {startAction && (\n            <ComposerActionGroup\n              align=\"start\"\n              className=\"in-[[data-layout=adaptive]:not([data-multiline])]:me-0\"\n            >\n              {startAction}\n            </ComposerActionGroup>\n          )}\n          <ComposerActionGroup align=\"end\">\n            <Tooltip>\n              <TooltipTrigger\n                render={\n                  <ComposerSubmit\n                    aria-label=\"Send message\"\n                    render={\n                      <Button\n                        aria-label=\"Send message\"\n                        className=\"rounded-full before:rounded-full\"\n                        size=\"icon\"\n                      >\n                        <ArrowUpIcon\n                          aria-hidden=\"true\"\n                          className=\"size-4 md:size-4.5\"\n                        />\n                      </Button>\n                    }\n                  />\n                }\n              />\n              <TooltipPopup side=\"bottom\">Send message</TooltipPopup>\n            </Tooltip>\n          </ComposerActionGroup>\n        </ComposerActions>\n      </Composer>\n      <div\n        className={cn(\n          (touchInput || !showKeyboardHint) && !notice\n            ? \"sr-only\"\n            : \"flex min-h-4 items-center justify-between px-1 text-muted-foreground text-xs\",\n          disabled && \"opacity-50\",\n        )}\n      >\n        <span aria-live=\"polite\" role=\"status\">\n          {notice}\n        </span>\n        {showKeyboardHint && !touchInput && (\n          <span\n            className={notice ? \"sr-only\" : \"ms-auto pointer-coarse:hidden\"}\n            id={hintId}\n          >\n            Enter to send <span className=\"px-1.5 opacity-40\">/</span> Shift +\n            Enter for a new line\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-composer.tsx"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-message-info.tsx",
      "content": "\"use client\";\n\nimport { ArrowLeftIcon, CheckCheckIcon, ClockIcon } from \"lucide-react\";\nimport type { ReactElement, RefObject } from \"react\";\nimport { useRef } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerDescription,\n  DrawerHeader,\n  DrawerPanel,\n  DrawerPopup,\n  DrawerTitle,\n} from \"@/registry/default/ui/drawer\";\nimport { ChatDeliveryIndicator } from \"../chat/chat-delivery-indicator\";\nimport { ChatMessageBubble } from \"../chat/chat-message-bubble\";\nimport { ChatMessageInfo as ChatMessageInfoView } from \"../chat/chat-message-info\";\nimport { ChatDemoMessageBody, ChatMemberAvatar } from \"./chat-demo-message\";\nimport {\n  DELIVERY_LABELS,\n  getMessageDelivery,\n  groupMessageReceipts,\n} from \"./chat-demo-receipts\";\nimport type { ChatMessage, MemberId } from \"./chat-demo-state\";\nimport { CHAT_MEMBERS } from \"./chat-demo-state\";\n\nconst RECEIPT_SECTIONS = [\n  { key: \"read\", label: \"Read by\", icon: CheckCheckIcon },\n  {\n    key: \"delivered\",\n    label: \"Delivered to\",\n    icon: CheckCheckIcon,\n  },\n  {\n    key: \"pending\",\n    label: \"Pending delivery\",\n    icon: ClockIcon,\n  },\n] as const;\n\nexport function ChatMessageInfo({\n  message,\n  recipients,\n  replyMessage,\n  position,\n  open,\n  onOpenChange,\n  onClosed,\n  returnFocusRef,\n}: {\n  message?: ChatMessage;\n  recipients: readonly MemberId[];\n  replyMessage?: ChatMessage;\n  position: \"bottom\" | \"right\";\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onClosed: () => void;\n  returnFocusRef: RefObject<HTMLElement | null>;\n}): ReactElement {\n  const backRef = useRef<HTMLButtonElement>(null);\n  const groups = message\n    ? groupMessageReceipts(message, recipients)\n    : { read: [], delivered: [], pending: [] };\n  const visibleSections = RECEIPT_SECTIONS.filter(\n    ({ key }) => groups[key].length > 0,\n  );\n  const delivery = message ? getMessageDelivery(message, recipients) : \"sent\";\n  return (\n    <Drawer\n      position={position}\n      open={open}\n      onOpenChange={onOpenChange}\n      onOpenChangeComplete={(isOpen) => {\n        if (!isOpen) onClosed();\n      }}\n    >\n      {message && (\n        <DrawerPopup\n          variant=\"inset\"\n          initialFocus={backRef}\n          finalFocus={returnFocusRef}\n          className={\n            position === \"bottom\" ? \"[--drawer-height:92dvh]\" : undefined\n          }\n        >\n          <DrawerHeader>\n            <div className=\"flex items-start gap-2\">\n              <DrawerClose\n                render={\n                  <Button\n                    ref={backRef}\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label=\"Back to conversation\"\n                  />\n                }\n              >\n                <ArrowLeftIcon />\n              </DrawerClose>\n              <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n                <DrawerTitle>Message info</DrawerTitle>\n                <DrawerDescription>The studio</DrawerDescription>\n              </div>\n            </div>\n          </DrawerHeader>\n          <DrawerPanel>\n            <ChatMessageInfoView\n              message={\n                <ChatMessageBubble side=\"outgoing\">\n                  <ChatDemoMessageBody\n                    message={message}\n                    replyMessage={replyMessage}\n                  />\n                </ChatMessageBubble>\n              }\n              metadata={\n                <>\n                  <time>\n                    {message.dateLabel}, {message.time}\n                  </time>\n                  <ChatDeliveryIndicator\n                    status={delivery}\n                    label={DELIVERY_LABELS[delivery]}\n                  />\n                </>\n              }\n              groups={visibleSections.map(({ key, label, icon: Icon }) => ({\n                id: key,\n                label,\n                icon: (\n                  <Icon\n                    aria-hidden=\"true\"\n                    className={cn(\n                      \"size-3.5\",\n                      key === \"read\"\n                        ? \"text-sky-600 dark:text-sky-400\"\n                        : \"text-muted-foreground\",\n                    )}\n                  />\n                ),\n                members: groups[key].map((receipt) => ({\n                  id: receipt.member,\n                  name: CHAT_MEMBERS[receipt.member].name,\n                  avatar: <ChatMemberAvatar member={receipt.member} />,\n                  action: (\n                    <span className=\"text-muted-foreground text-xs tabular-nums\">\n                      {receipt.readAt ?? receipt.deliveredAt ?? \"Pending\"}\n                    </span>\n                  ),\n                })),\n              }))}\n            />\n          </DrawerPanel>\n        </DrawerPopup>\n      )}\n    </Drawer>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-message-info.tsx"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-message.tsx",
      "content": "\"use client\";\n\nimport { CopyIcon, InfoIcon, PinIcon, Trash2Icon } from \"lucide-react\";\nimport type { ReactElement } from \"react\";\nimport { useRef, useState } from \"react\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport { Avatar, AvatarFallback } from \"@/registry/default/ui/avatar\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { MenuItem, MenuSeparator } from \"@/registry/default/ui/menu\";\nimport { ChatCollapsibleMessage } from \"../chat/chat-collapsible-message\";\nimport { ChatDeliveryIndicator } from \"../chat/chat-delivery-indicator\";\nimport { ChatMessage as ChatMessageView } from \"../chat/chat-message\";\nimport { ChatMessageActionToolbar } from \"../chat/chat-message-actions\";\nimport { ChatMessageText } from \"../chat/chat-message-text\";\nimport { ChatReactionChip } from \"../chat/chat-reaction-chip\";\nimport { ChatReactionPicker } from \"../chat/chat-reaction-picker\";\nimport { ChatReplyReference } from \"../chat/chat-reply-reference\";\nimport { DELIVERY_LABELS, getMessageDelivery } from \"./chat-demo-receipts\";\nimport type { ChatMessage, MemberId } from \"./chat-demo-state\";\nimport {\n  CHAT_EMOJI,\n  CHAT_MEMBERS,\n  getMessageDeletionReason,\n  getMessagePreview,\n} from \"./chat-demo-state\";\n\nexport function ChatMemberAvatar({\n  member,\n  className,\n}: {\n  member: MemberId;\n  className?: string;\n}): ReactElement {\n  return (\n    <Avatar\n      aria-hidden=\"true\"\n      className={cn(\"size-7 text-[.625rem]\", className)}\n    >\n      <AvatarFallback className={CHAT_MEMBERS[member].color}>\n        {CHAT_MEMBERS[member].initials}\n      </AvatarFallback>\n    </Avatar>\n  );\n}\n\nexport function ChatDemoMessageBody({\n  message,\n  replyMessage,\n  onJump,\n  defaultOpen,\n  open,\n  onOpenChange,\n}: {\n  message: ChatMessage;\n  replyMessage?: ChatMessage;\n  onJump?: (id: number) => void;\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n}): ReactElement {\n  const own = message.author === \"you\";\n  return (\n    <>\n      {message.forwarded && (\n        <span className=\"text-xs italic opacity-65\">Forwarded</span>\n      )}\n      {message.replyTo && (\n        <ChatReplyReference\n          author={\n            replyMessage\n              ? CHAT_MEMBERS[replyMessage.author].name\n              : \"Original message\"\n          }\n          preview={\n            replyMessage\n              ? getMessagePreview(replyMessage)\n              : \"Message unavailable\"\n          }\n          onClick={() => onJump?.(message.replyTo ?? message.id)}\n          disabled={!replyMessage || !onJump}\n        />\n      )}\n      <ChatCollapsibleMessage\n        defaultOpen={defaultOpen}\n        open={open}\n        onOpenChange={onOpenChange}\n        text={message.text}\n        side={own ? \"outgoing\" : \"incoming\"}\n        showMoreLabel=\"Show more\"\n        showLessLabel=\"Show less\"\n        renderText={(text) => (\n          <ChatMessageText\n            text={text}\n            mentions={Object.values(CHAT_MEMBERS).map(\n              (member) => member.handle,\n            )}\n          />\n        )}\n      />\n    </>\n  );\n}\n\nexport function ChatDemoMessage({\n  message,\n  recipients,\n  entering,\n  onEntranceEnd,\n  replyMessage,\n  grouped,\n  groupEnd,\n  pinned,\n  highlighted,\n  replyLifted,\n  domId,\n  onReply,\n  onReact,\n  onPin,\n  onDelete,\n  onCopy,\n  onJump,\n  onRetry,\n  onInfo,\n}: {\n  message: ChatMessage;\n  recipients: readonly MemberId[];\n  entering: boolean;\n  onEntranceEnd: () => void;\n  replyMessage?: ChatMessage;\n  grouped: boolean;\n  groupEnd: boolean;\n  pinned: boolean;\n  highlighted: boolean;\n  replyLifted: boolean;\n  domId: string;\n  onReply: (\n    id: number,\n    source: HTMLDivElement,\n    firstInGroup: boolean,\n    expanded: boolean,\n  ) => void;\n  onReact: (id: number, emoji: string) => void;\n  onPin: (id: number) => void;\n  onDelete: (id: number, trigger: HTMLElement | null) => void;\n  onCopy: (text: string) => void;\n  onJump: (id: number) => void;\n  onRetry: (id: number) => void;\n  onInfo: (id: number, trigger: HTMLElement | null) => void;\n}): ReactElement {\n  const [expanded, setExpanded] = useState(false);\n  const moreTriggerRef = useRef<HTMLButtonElement>(null);\n  const suppressMenuFocusRef = useRef(false);\n  const bubbleRef = useRef<HTMLDivElement>(null);\n  const replyToMessage = (): void => {\n    if (bubbleRef.current)\n      onReply(message.id, bubbleRef.current, !grouped, expanded);\n  };\n  const initialReactions = useRef(\n    new Set(message.reactions.map((reaction) => reaction.emoji)),\n  );\n  const delivery = getMessageDelivery(message, recipients);\n  const own = message.author === \"you\";\n  const member = CHAT_MEMBERS[message.author];\n  return (\n    <ChatMessageView\n      id={domId}\n      tabIndex={-1}\n      aria-label={`${member.name}, ${message.dateLabel} at ${message.time}`}\n      side={own ? \"outgoing\" : \"incoming\"}\n      author={member.name}\n      avatar={<ChatMemberAvatar member={message.author} />}\n      grouped={grouped}\n      lifted={replyLifted}\n      bubble={!message.deletedReason}\n      bubbleProps={{ ref: bubbleRef }}\n      animateEntrance={entering}\n      onAnimationComplete={entering ? onEntranceEnd : undefined}\n      highlighted={highlighted}\n      actions={\n        <ChatMessageActionToolbar\n          menuTriggerRef={moreTriggerRef}\n          side={own ? \"outgoing\" : \"incoming\"}\n          toolbarLabel=\"Message actions\"\n          reactionPicker={(toolbarRef) => (\n            <ChatReactionPicker\n              inToolbar\n              options={CHAT_EMOJI}\n              selectedEmojis={message.reactions\n                .filter((reaction) => reaction.members.includes(\"you\"))\n                .map((reaction) => reaction.emoji)}\n              triggerLabel=\"React to message\"\n              triggerTooltip=\"React\"\n              popupLabel=\"Choose a reaction\"\n              popupAlign={own ? \"start\" : \"end\"}\n              popupAnchor={toolbarRef}\n              onSelect={(emoji) => onReact(message.id, emoji)}\n            />\n          )}\n          replyLabel={`Reply to ${member.name}`}\n          replyTooltip=\"Reply\"\n          onReply={replyToMessage}\n          moreLabel={`More actions for ${member.name}'s message`}\n          moreTooltip=\"More actions\"\n          menuFinalFocus={() => {\n            if (!suppressMenuFocusRef.current) return true;\n            suppressMenuFocusRef.current = false;\n            return false;\n          }}\n          menuChildren={\n            <>\n              <MenuItem onClick={() => onCopy(message.text)}>\n                <CopyIcon /> Copy text\n              </MenuItem>\n              <MenuItem onClick={() => onPin(message.id)}>\n                <PinIcon /> {pinned ? \"Unpin message\" : \"Pin message\"}\n              </MenuItem>\n              {own && (\n                <MenuItem\n                  onClick={() => {\n                    suppressMenuFocusRef.current = true;\n                    onInfo(message.id, moreTriggerRef.current);\n                  }}\n                >\n                  <InfoIcon /> Message info\n                </MenuItem>\n              )}\n              {getMessageDeletionReason(message) && (\n                <>\n                  <MenuSeparator />\n                  <MenuItem\n                    variant=\"destructive\"\n                    onClick={() => {\n                      suppressMenuFocusRef.current = true;\n                      onDelete(message.id, moreTriggerRef.current);\n                    }}\n                  >\n                    <Trash2Icon /> Delete message\n                  </MenuItem>\n                </>\n              )}\n            </>\n          }\n        />\n      }\n      reactions={\n        message.reactions.length > 0 && !message.deletedReason\n          ? message.reactions.map((reaction) => (\n              <ChatReactionChip\n                key={reaction.emoji}\n                animateEntrance={!initialReactions.current.has(reaction.emoji)}\n                emoji={reaction.emoji}\n                count={reaction.members.length}\n                reactorLabel={`${reaction.members.length} ${reaction.members.length === 1 ? \"reaction\" : \"reactions\"}`}\n                popupAlign={own ? \"end\" : \"start\"}\n                selected={reaction.members.includes(\"you\")}\n                label={`${reaction.members.includes(\"you\") ? \"Remove\" : \"Add\"} ${reaction.emoji} reaction, ${reaction.members.length} ${reaction.members.length === 1 ? \"person\" : \"people\"}`}\n                reactorContent={\n                  <ul\n                    aria-label=\"People who reacted\"\n                    className=\"flex max-h-48 flex-col overflow-y-auto overscroll-contain\"\n                  >\n                    {reaction.members.map((member) => (\n                      <li\n                        key={member}\n                        className=\"flex items-center gap-2 px-1.5 py-1 text-xs leading-5\"\n                      >\n                        <ChatMemberAvatar\n                          member={member}\n                          className=\"size-5 text-[.5625rem]\"\n                        />\n                        <span className=\"min-w-0 flex-1 truncate\">\n                          {CHAT_MEMBERS[member].name}\n                        </span>\n                        {member === \"you\" && (\n                          <Badge variant=\"secondary\" size=\"sm\">\n                            You\n                          </Badge>\n                        )}\n                      </li>\n                    ))}\n                  </ul>\n                }\n                onClick={() => onReact(message.id, reaction.emoji)}\n              />\n            ))\n          : undefined\n      }\n      footer={\n        groupEnd || message.delivery ? (\n          <>\n            {pinned && !message.deletedReason && (\n              <PinIcon className=\"size-2.5\" aria-label=\"Pinned\" />\n            )}\n            <time>{message.time}</time>\n            {own && !message.deletedReason && (\n              <>\n                <ChatDeliveryIndicator\n                  status={delivery}\n                  label={DELIVERY_LABELS[delivery]}\n                />\n                {message.delivery === \"failed\" && (\n                  <Button\n                    variant=\"ghost\"\n                    size=\"xs\"\n                    onClick={() => onRetry(message.id)}\n                  >\n                    Retry\n                  </Button>\n                )}\n              </>\n            )}\n          </>\n        ) : undefined\n      }\n    >\n      {message.deletedReason ? (\n        <div className=\"flex items-center gap-1 px-1 py-2 text-muted-foreground text-xs\">\n          <Trash2Icon className=\"size-3 opacity-80\" aria-hidden=\"true\" />{\" \"}\n          {getMessagePreview(message)}\n        </div>\n      ) : (\n        <ChatDemoMessageBody\n          message={message}\n          replyMessage={replyMessage}\n          onJump={onJump}\n          open={expanded}\n          onOpenChange={setExpanded}\n        />\n      )}\n    </ChatMessageView>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-message.tsx"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-receipts.ts",
      "content": "import type { ChatMessage, ChatReceipt, MemberId } from \"./chat-demo-state\";\nimport { OTHER_MEMBERS } from \"./chat-demo-state\";\n\nexport type MessageDelivery = NonNullable<ChatMessage[\"delivery\"]>;\n\nexport const DELIVERY_LABELS: Record<MessageDelivery, string> = {\n  sending: \"Sending\",\n  sent: \"Sent\",\n  delivered: \"Delivered to everyone\",\n  read: \"Read by everyone\",\n  failed: \"Not sent\",\n};\n\nexport function getMessageReceipts(\n  message: ChatMessage,\n  recipients: readonly MemberId[] = OTHER_MEMBERS,\n): ChatReceipt[] {\n  if (message.receipts) {\n    const receiptsByMember = new Map(\n      message.receipts.map((receipt) => [receipt.member, receipt]),\n    );\n    return recipients.map(\n      (member) => receiptsByMember.get(member) ?? { member },\n    );\n  }\n  return recipients.map((member) => ({\n    member,\n    deliveredAt:\n      message.delivery === \"delivered\" || message.delivery === \"read\"\n        ? message.time\n        : undefined,\n    readAt: message.delivery === \"read\" ? message.time : undefined,\n  }));\n}\n\nexport function getMessageDelivery(\n  message: ChatMessage,\n  recipients?: readonly MemberId[],\n): MessageDelivery {\n  if (message.delivery === \"sending\" || message.delivery === \"failed\")\n    return message.delivery;\n  const receipts = getMessageReceipts(message, recipients);\n  if (receipts.length && receipts.every((receipt) => receipt.readAt))\n    return \"read\";\n  if (\n    receipts.length &&\n    receipts.every((receipt) => receipt.deliveredAt || receipt.readAt)\n  )\n    return \"delivered\";\n  return \"sent\";\n}\n\nexport function groupMessageReceipts(\n  message: ChatMessage,\n  recipients?: readonly MemberId[],\n): {\n  read: ChatReceipt[];\n  delivered: ChatReceipt[];\n  pending: ChatReceipt[];\n} {\n  const groups: ReturnType<typeof groupMessageReceipts> = {\n    read: [],\n    delivered: [],\n    pending: [],\n  };\n  for (const receipt of getMessageReceipts(message, recipients)) {\n    if (receipt.readAt) groups.read.push(receipt);\n    else if (receipt.deliveredAt) groups.delivered.push(receipt);\n    else groups.pending.push(receipt);\n  }\n  return groups;\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-receipts.ts"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-reply-focus.tsx",
      "content": "\"use client\";\n\nimport type { ReactElement, RefObject } from \"react\";\nimport {\n  ChatReplyPreview,\n  type ChatReplyTarget,\n} from \"../chat/chat-reply-preview\";\nimport { ChatDemoMessageBody, ChatMemberAvatar } from \"./chat-demo-message\";\nimport type { ChatMessage } from \"./chat-demo-state\";\nimport { CHAT_MEMBERS } from \"./chat-demo-state\";\n\nexport type ChatReplyFocusTarget = ChatReplyTarget & {\n  messageId: number;\n  expanded: boolean;\n};\n\nexport function ChatDemoReplyFocus({\n  target,\n  message,\n  replyMessage,\n  dismissalRef,\n  onCancel,\n}: {\n  target: ChatReplyFocusTarget;\n  message: ChatMessage;\n  replyMessage?: ChatMessage;\n  dismissalRef: RefObject<\"cancel\" | \"send\">;\n  onCancel: () => void;\n}): ReactElement {\n  const own = message.author === \"you\";\n  return (\n    <ChatReplyPreview\n      target={target}\n      side={own ? \"outgoing\" : \"incoming\"}\n      avatar={<ChatMemberAvatar member={message.author} />}\n      author={own ? \"your message\" : CHAT_MEMBERS[message.author].name}\n      replyLabel={\n        own\n          ? \"Reply to your message\"\n          : `Reply to ${CHAT_MEMBERS[message.author].name}`\n      }\n      cancelLabel=\"Cancel reply\"\n      dismissalRef={dismissalRef}\n      onCancel={onCancel}\n    >\n      <ChatDemoMessageBody\n        defaultOpen={target.expanded}\n        message={message}\n        replyMessage={replyMessage}\n      />\n    </ChatReplyPreview>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-reply-focus.tsx"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-scenarios.tsx",
      "content": "\"use client\";\n\nimport {\n  BanIcon,\n  CircleAlertIcon,\n  LockIcon,\n  LogOutIcon,\n  MessageCircleIcon,\n  UserXIcon,\n} from \"lucide-react\";\nimport type { ReactElement } from \"react\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport { ChatEmptyState, ChatLoadingState } from \"../chat/chat-states\";\nimport type { ChatScenario } from \"./chat-demo-state\";\n\nexport function ChatTimelineState({\n  scenario,\n  onRecover,\n  memberCount,\n  joined,\n  onJoin,\n}: {\n  scenario: ChatScenario;\n  onRecover: () => void;\n  memberCount: number;\n  joined: boolean;\n  onJoin: () => void;\n}): ReactElement | null {\n  if (scenario === \"loading\")\n    return (\n      <ChatLoadingState label=\"Loading conversation\">\n        <Button variant=\"outline\" size=\"sm\" onClick={onRecover}>\n          Finish loading preview\n        </Button>\n      </ChatLoadingState>\n    );\n  const content = {\n    empty: {\n      title: joined ? \"A little hello goes a long way\" : \"Join the event chat\",\n      description: joined\n        ? \"Start the conversation. Everyone in the group can join in.\"\n        : \"Talk with the host and other guests who are going. Only people who join can read or send messages.\",\n      Icon: MessageCircleIcon,\n    },\n    error: {\n      title: \"Couldn’t load the conversation\",\n      description: \"Your draft is safe. Try loading the messages again.\",\n      Icon: CircleAlertIcon,\n    },\n    \"access-lost\": {\n      title: \"This conversation is no longer available\",\n      description:\n        \"You no longer have access. Contact the group admin if this seems wrong.\",\n      Icon: LockIcon,\n    },\n    removed: {\n      title: \"You were removed from this chat\",\n      description:\n        \"A host removed you. You can rejoin through your ticket invitation. Your ticket for the event is still valid.\",\n      Icon: UserXIcon,\n    },\n    banned: {\n      title: \"You were banned from this chat\",\n      description:\n        \"You can’t rejoin unless a host lifts the ban. Your ticket for the event is still valid.\",\n      Icon: BanIcon,\n    },\n    left: {\n      title: \"You left this chat\",\n      description:\n        \"You’ll no longer receive messages from this group. Your ticket for the event is still valid.\",\n      Icon: LogOutIcon,\n    },\n  };\n  if (\n    scenario !== \"empty\" &&\n    scenario !== \"error\" &&\n    scenario !== \"access-lost\" &&\n    scenario !== \"removed\" &&\n    scenario !== \"banned\" &&\n    scenario !== \"left\"\n  )\n    return null;\n  const { title, description, Icon } = content[scenario];\n  return (\n    <ChatEmptyState\n      title={title}\n      description={description}\n      icon={<Icon aria-hidden=\"true\" />}\n    >\n      {scenario === \"empty\" && !joined && (\n        <div className=\"flex flex-col items-center gap-2\">\n          <Button onClick={onJoin}>Join chat</Button>\n          <p className=\"text-muted-foreground text-xs\">\n            {memberCount} {memberCount === 1 ? \"person is\" : \"people are\"}{\" \"}\n            already in the chat\n          </p>\n        </div>\n      )}\n      {scenario === \"error\" && (\n        <Button variant=\"outline\" size=\"sm\" onClick={onRecover}>\n          Try again\n        </Button>\n      )}\n      {(scenario === \"left\" || scenario === \"removed\") && (\n        <Button onClick={onRecover}>Rejoin chat</Button>\n      )}\n    </ChatEmptyState>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-scenarios.tsx"
    },
    {
      "path": "registry/default/chat-demo/chat-demo-state.ts",
      "content": "export type MemberId = \"maya\" | \"jordan\" | \"alex\" | \"you\";\n\nexport const CHAT_MEMBERS = {\n  maya: {\n    role: \"host\",\n    name: \"Maya Chen\",\n    handle: \"Maya\",\n    initials: \"MC\",\n    color:\n      \"bg-orange-100 text-orange-800 dark:bg-orange-950 dark:text-orange-200\",\n  },\n  jordan: {\n    role: \"co-host\",\n    name: \"Jordan Lee\",\n    handle: \"Jordan\",\n    initials: \"JL\",\n    color:\n      \"bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-200\",\n  },\n  alex: {\n    role: \"guest\",\n    name: \"Alex Morgan\",\n    handle: \"Alex\",\n    initials: \"AM\",\n    color: \"bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-200\",\n  },\n  you: {\n    role: \"co-host\",\n    name: \"Sam Rivera\",\n    handle: \"You\",\n    initials: \"SR\",\n    color: \"bg-primary text-primary-foreground\",\n  },\n} satisfies Record<\n  MemberId,\n  {\n    role: \"host\" | \"co-host\" | \"guest\";\n    name: string;\n    handle: string;\n    initials: string;\n    color: string;\n  }\n>;\n\nexport const OTHER_MEMBERS: MemberId[] = [\"maya\", \"jordan\", \"alex\"];\n\nexport function getTypingLabel(members: readonly MemberId[]): string {\n  const [first, second] = members;\n  if (!first) return \"\";\n  const firstName = CHAT_MEMBERS[first].handle;\n  if (members.length === 1) return `${firstName} is typing`;\n  if (members.length === 2 && second)\n    return `${firstName} and ${CHAT_MEMBERS[second].handle} are typing`;\n  return `${firstName} and ${members.length - 1} others are typing`;\n}\n\nexport const CHAT_EMOJI = [\n  { emoji: \"👍\", label: \"Thumbs up\" },\n  { emoji: \"❤️\", label: \"Heart\" },\n  { emoji: \"😂\", label: \"Laugh\" },\n  { emoji: \"😮\", label: \"Surprised\" },\n  { emoji: \"😢\", label: \"Sad\" },\n  { emoji: \"🙏\", label: \"Thanks\" },\n];\n\nexport type ChatReaction = { emoji: string; members: MemberId[] };\nexport type ChatReceipt = {\n  member: MemberId;\n  deliveredAt?: string;\n  readAt?: string;\n};\nexport type ChatMessage = {\n  id: number;\n  author: MemberId;\n  text: string;\n  dateLabel: string;\n  time: string;\n  replyTo?: number;\n  reactions: ChatReaction[];\n  deletedReason?: \"sender\" | \"moderator\";\n  forwarded?: boolean;\n  receipts?: ChatReceipt[];\n  delivery?: \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n};\n\nexport function getMessageDeletionReason(\n  message: ChatMessage,\n  viewer: MemberId = \"you\",\n): ChatMessage[\"deletedReason\"] {\n  if (message.deletedReason) return undefined;\n  if (message.author === viewer) return \"sender\";\n  if (\n    CHAT_MEMBERS[viewer].role !== \"guest\" &&\n    CHAT_MEMBERS[message.author].role === \"guest\"\n  ) {\n    return \"moderator\";\n  }\n  return undefined;\n}\n\nexport function deleteChatMessage(\n  message: ChatMessage,\n  viewer: MemberId = \"you\",\n): ChatMessage {\n  const deletedReason = getMessageDeletionReason(message, viewer);\n  if (!deletedReason) return message;\n  return { ...message, text: \"\", reactions: [], deletedReason };\n}\n\nexport function getMessagePreview(message: ChatMessage): string {\n  if (message.deletedReason === \"moderator\") return \"Removed by a host\";\n  if (message.deletedReason === \"sender\") return \"Message deleted\";\n  return message.text;\n}\n\nexport const INITIAL_CHAT_MESSAGES: ChatMessage[] = [\n  {\n    id: 1,\n    author: \"maya\",\n    text: \"The new direction is ready for a first look.\",\n    dateLabel: \"Thursday, September 10\",\n    time: \"10:42\",\n    reactions: [],\n    forwarded: true,\n  },\n  {\n    id: 2,\n    author: \"maya\",\n    text: \"Would love your eyes on the little details before we share it.\",\n    dateLabel: \"Friday, September 11\",\n    time: \"10:42\",\n    reactions: [],\n  },\n  {\n    id: 3,\n    author: \"jordan\",\n    text: \"Much calmer. The spacing makes all the difference ✨\",\n    dateLabel: \"Friday, September 11\",\n    time: \"10:44\",\n    reactions: [{ emoji: \"❤️\", members: [\"maya\", \"alex\"] }],\n  },\n  {\n    id: 4,\n    author: \"you\",\n    text: \"Agreed. Everything has a little more room to breathe.\",\n    dateLabel: \"Today\",\n    time: \"10:46\",\n    reactions: [],\n    delivery: \"read\",\n  },\n  {\n    id: 5,\n    author: \"alex\",\n    text: \"Quick walkthrough at 2? @You would love your take on the final flow.\",\n    dateLabel: \"Today\",\n    time: \"10:48\",\n    reactions: [],\n  },\n  {\n    id: 6,\n    author: \"you\",\n    text: \"Sounds good — see you then.\",\n    dateLabel: \"Today\",\n    time: \"10:49\",\n    replyTo: 5,\n    reactions: [{ emoji: \"👍\", members: [\"jordan\"] }],\n    receipts: [\n      { member: \"maya\", deliveredAt: \"10:49\", readAt: \"10:50\" },\n      { member: \"jordan\", deliveredAt: \"10:49\" },\n      { member: \"alex\" },\n    ],\n  },\n  {\n    id: 7,\n    author: \"maya\",\n    text: \"Perfect. I’ll bring the coffee ☕\",\n    dateLabel: \"Today\",\n    time: \"10:50\",\n    reactions: [],\n  },\n  {\n    id: 9,\n    author: \"jordan\",\n    text: \"See you at two. I’ll bring the notes.\",\n    dateLabel: \"Today\",\n    time: \"10:51\",\n    replyTo: 6,\n    reactions: [],\n  },\n];\n\nconst LONG_CHAT_MESSAGE: ChatMessage = {\n  id: 8,\n  author: \"maya\",\n  text: [\n    \"I wrote down the full walkthrough so everyone can review it before we meet. First, we’ll look at the event page as a guest and make sure the date, location, and group details are easy to find. Then we’ll open the chat and check that the conversation feels useful without taking over the page.\",\n    \"After that, let’s try a few everyday situations: catching up on older messages, replying to a specific note, mentioning someone, and finding a pinned update. I’d also like us to check what happens when several people are typing and when a message takes a moment to send.\",\n    \"Finally, we should look at the narrow layout together. The drawer needs to leave enough room for actions beside each bubble, keep the composer comfortable to use, and make long notes like this one easy to skim. If anything feels crowded, we can simplify it before sharing the pattern with other teams.\",\n    \"One more thing for the review: please try the same conversation with a few messages arriving while the drawer is open. We should check whether the reading position stays predictable, whether the latest message control is easy to find, and whether the date markers still make sense as we move through older parts of the conversation. If you notice any confusing transitions, leave a note so we can refine the behavior before this pattern is reused elsewhere in the app.\",\n  ].join(\"\\n\\n\"),\n  dateLabel: \"Today\",\n  time: \"10:52\",\n  reactions: [],\n};\n\nexport function toggleReaction(\n  reactions: ChatReaction[],\n  emoji: string,\n): ChatReaction[] {\n  const existing = reactions.find((reaction) => reaction.emoji === emoji);\n  if (!existing) return [...reactions, { emoji, members: [\"you\"] }];\n  return reactions.flatMap((reaction) => {\n    if (reaction.emoji !== emoji) return [reaction];\n    const members: MemberId[] = reaction.members.includes(\"you\")\n      ? reaction.members.filter((member) => member !== \"you\")\n      : [...reaction.members, \"you\"];\n    if (members.length === 0) return [];\n    return [{ ...reaction, members }];\n  });\n}\n\nexport const CHAT_SCENARIOS = [\n  { value: \"conversation\", label: \"Conversation\" },\n  { value: \"long-message\", label: \"Long message\" },\n  { value: \"pinned\", label: \"Pinned message\" },\n  { value: \"typing\", label: \"Someone typing\" },\n  { value: \"typing-multiple\", label: \"Multiple people typing\" },\n  { value: \"empty\", label: \"First message\" },\n  { value: \"loading\", label: \"Loading conversation\" },\n  { value: \"error\", label: \"History error\" },\n  { value: \"offline\", label: \"Disconnected\" },\n  { value: \"access-lost\", label: \"Access lost\" },\n  { value: \"removed\", label: \"Removed from chat\" },\n  { value: \"banned\", label: \"Banned from chat\" },\n  { value: \"left\", label: \"Left chat\" },\n  { value: \"unread\", label: \"Unread messages\" },\n  { value: \"receipts\", label: \"Delivery & retry\" },\n] as const;\n\nexport type ChatScenario =\n  | (typeof CHAT_SCENARIOS)[number][\"value\"]\n  | \"reconnecting\";\n\nexport function getScenarioMessages(scenario: ChatScenario): ChatMessage[] {\n  if (\n    [\n      \"empty\",\n      \"loading\",\n      \"error\",\n      \"access-lost\",\n      \"removed\",\n      \"banned\",\n      \"left\",\n    ].includes(scenario)\n  )\n    return [];\n  if (scenario === \"long-message\")\n    return [...INITIAL_CHAT_MESSAGES, LONG_CHAT_MESSAGE];\n  if (scenario !== \"receipts\") return INITIAL_CHAT_MESSAGES;\n  return [\n    ...INITIAL_CHAT_MESSAGES.slice(0, 1),\n    ...([\"sending\", \"sent\", \"delivered\", \"read\", \"failed\"] as const).map(\n      (delivery, index) => ({\n        id: 20 + index,\n        author: \"you\" as const,\n        text:\n          [\n            \"Sending the latest update…\",\n            \"The notes are on their way.\",\n            \"The group has received the notes.\",\n            \"Ready for our next review.\",\n            \"Shall we meet at two?\",\n          ][index] ?? \"A new update.\",\n        dateLabel: \"Today\",\n        time: \"10:51\",\n        reactions: [],\n        delivery,\n      }),\n    ),\n  ];\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo-state.ts"
    },
    {
      "path": "registry/default/chat-demo/chat-demo.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowLeftIcon,\n  BanIcon,\n  ChevronRightIcon,\n  EllipsisIcon,\n  LockIcon,\n  LogOutIcon,\n  MicIcon,\n  MicOffIcon,\n  PinIcon,\n  UserXIcon,\n} from \"lucide-react\";\nimport { AnimatePresence } from \"motion/react\";\nimport type { ReactElement, ReactNode, RefObject } from \"react\";\nimport { Fragment, useEffect, useId, useRef, useState } from \"react\";\nimport { useMediaQuery } from \"@/registry/default/hooks/use-media-query\";\nimport { cn } from \"@/registry/default/lib/utils\";\nimport {\n  AlertDialog,\n  AlertDialogClose,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogPopup,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/registry/default/ui/alert-dialog\";\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n  type ComposerValue,\n  getComposerText,\n} from \"@/registry/default/ui/composer\";\nimport {\n  Drawer,\n  DrawerClose,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerPanel,\n  DrawerPopup,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/registry/default/ui/drawer\";\nimport {\n  Menu,\n  MenuItem,\n  MenuPopup,\n  MenuTrigger,\n} from \"@/registry/default/ui/menu\";\nimport { TooltipProvider } from \"@/registry/default/ui/tooltip\";\nimport { ChatConversation } from \"../chat/chat-conversation\";\nimport { ChatDatePill } from \"../chat/chat-date-pill\";\nimport { ChatMemberSection } from \"../chat/chat-members\";\nimport { ChatStatusBar } from \"../chat/chat-states\";\nimport { ChatTypingIndicator } from \"../chat/chat-typing-indicator\";\nimport { ChatUnreadDivider } from \"../chat/chat-unread-divider\";\nimport { useChatScroll } from \"../chat/hooks/use-chat-scroll\";\nimport { ChatDemoComposer } from \"./chat-demo-composer\";\nimport { ChatDemoMessage, ChatMemberAvatar } from \"./chat-demo-message\";\nimport { ChatMessageInfo } from \"./chat-demo-message-info\";\nimport { getMessageReceipts } from \"./chat-demo-receipts\";\nimport type { ChatReplyFocusTarget } from \"./chat-demo-reply-focus\";\nimport { ChatDemoReplyFocus } from \"./chat-demo-reply-focus\";\nimport { ChatTimelineState } from \"./chat-demo-scenarios\";\nimport type { ChatMessage, ChatScenario, MemberId } from \"./chat-demo-state\";\nimport {\n  CHAT_MEMBERS,\n  deleteChatMessage,\n  getMessageDeletionReason,\n  getMessagePreview,\n  getScenarioMessages,\n  getTypingLabel,\n  INITIAL_CHAT_MESSAGES,\n  OTHER_MEMBERS,\n  toggleReaction,\n} from \"./chat-demo-state\";\n\nconst DEMO_REPLIES = [\n  \"Thanks for sharing! I’ll take a look ✨\",\n  \"Love that. Let’s pick it up in the walkthrough.\",\n  \"Noted — I’ll bring this back to the group.\",\n];\n\n// `focus()` on a contenteditable puts the caret at the start of an existing draft.\nfunction focusAtEnd(input: HTMLElement | null): void {\n  if (!input) return;\n  input.focus({ preventScroll: true });\n  const selection = window.getSelection();\n  selection?.selectAllChildren(input);\n  selection?.collapseToEnd();\n}\n\nfunction GroupDetails({\n  open,\n  onOpenChange,\n  onClosed,\n  position,\n  activeMembers,\n  mutedMembers,\n  bannedMembers,\n  onToggleMute,\n  onRemove,\n  onBan,\n  onUnban,\n  canLeave,\n  onLeave,\n  conversationFocusRef,\n}: {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  onClosed: () => void;\n  position: \"bottom\" | \"right\";\n  activeMembers: readonly MemberId[];\n  mutedMembers: readonly MemberId[];\n  bannedMembers: readonly MemberId[];\n  onToggleMute: (member: MemberId) => void;\n  onRemove: (member: MemberId) => void;\n  onBan: (member: MemberId) => void;\n  onUnban: (member: MemberId) => void;\n  canLeave: boolean;\n  onLeave: () => void;\n  conversationFocusRef: RefObject<HTMLDivElement | null>;\n}): ReactElement {\n  const memberNames = activeMembers\n    .map((member) => CHAT_MEMBERS[member].name)\n    .join(\", \");\n  const previewNames = activeMembers\n    .slice(0, 3)\n    .map((member) => CHAT_MEMBERS[member].name)\n    .join(\", \");\n  const [pendingRemoval, setPendingRemoval] = useState<MemberId | null>(null);\n  const [moderationAction, setModerationAction] = useState<\"remove\" | \"ban\">(\n    \"remove\",\n  );\n  const [confirmOpen, setConfirmOpen] = useState(false);\n  const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false);\n  const [restoredMember, setRestoredMember] = useState<MemberId | null>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const backRef = useRef<HTMLButtonElement>(null);\n  const leaveTriggerRef = useRef<HTMLButtonElement>(null);\n  const leaveRequestedRef = useRef(false);\n  const editRefs = useRef<Partial<Record<MemberId, HTMLButtonElement>>>({});\n  const unbanRefs = useRef<Partial<Record<MemberId, HTMLButtonElement>>>({});\n  useEffect(() => {\n    if (!restoredMember || !activeMembers.includes(restoredMember)) return;\n    editRefs.current[restoredMember]?.focus();\n    setRestoredMember(null);\n  }, [activeMembers, restoredMember]);\n  return (\n    <Drawer\n      position={position}\n      open={open}\n      onOpenChange={onOpenChange}\n      onOpenChangeComplete={(isOpen) => {\n        if (!isOpen) onClosed();\n      }}\n    >\n      <DrawerTrigger\n        ref={triggerRef}\n        className=\"block max-w-full truncate text-start hover:text-foreground focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2\"\n        aria-label={`View group members: ${memberNames}`}\n      >\n        {previewNames}\n        {activeMembers.length > 3 ? \" and others\" : \"\"}\n      </DrawerTrigger>\n      <DrawerPopup\n        variant=\"inset\"\n        initialFocus={backRef}\n        finalFocus={triggerRef}\n        className={\n          position === \"bottom\" ? \"[--drawer-height:92dvh]\" : undefined\n        }\n      >\n        <DrawerHeader>\n          <div className=\"flex items-start gap-2\">\n            <DrawerClose\n              render={\n                <Button\n                  ref={backRef}\n                  variant=\"ghost\"\n                  size=\"icon-sm\"\n                  aria-label=\"Back to conversation\"\n                />\n              }\n            >\n              <ArrowLeftIcon />\n            </DrawerClose>\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n              <DrawerTitle>Group members</DrawerTitle>\n              <DrawerDescription>\n                The studio · {activeMembers.length} members\n              </DrawerDescription>\n            </div>\n          </div>\n        </DrawerHeader>\n        <DrawerPanel>\n          <div className=\"flex flex-col gap-7\">\n            <ChatMemberSection\n              label=\"Current members\"\n              members={activeMembers.map((member) => ({\n                id: member,\n                name: CHAT_MEMBERS[member].name,\n                avatar: <ChatMemberAvatar member={member} />,\n                badges: (\n                  <>\n                    {member === \"you\" && <Badge variant=\"secondary\">You</Badge>}\n                    {CHAT_MEMBERS[member].role === \"host\" && (\n                      <Badge variant=\"default\">Host</Badge>\n                    )}\n                    {CHAT_MEMBERS[member].role === \"co-host\" && (\n                      <Badge variant=\"default\">Co-host</Badge>\n                    )}\n                    {mutedMembers.includes(member) && (\n                      <Badge variant=\"error\">\n                        <MicOffIcon aria-hidden=\"true\" /> Muted\n                      </Badge>\n                    )}\n                  </>\n                ),\n                action:\n                  CHAT_MEMBERS[member].role === \"guest\" ? (\n                    <Menu>\n                      <MenuTrigger\n                        render={\n                          <Button\n                            ref={(node) => {\n                              editRefs.current[member] = node ?? undefined;\n                            }}\n                            variant=\"ghost\"\n                            size=\"icon-sm\"\n                            aria-label={`More actions for ${CHAT_MEMBERS[member].name}`}\n                          />\n                        }\n                      >\n                        <EllipsisIcon className=\"size-4\" aria-hidden=\"true\" />\n                      </MenuTrigger>\n                      <MenuPopup align=\"end\">\n                        <MenuItem onClick={() => onToggleMute(member)}>\n                          {mutedMembers.includes(member) ? (\n                            <>\n                              <MicIcon aria-hidden=\"true\" /> Unmute\n                            </>\n                          ) : (\n                            <>\n                              <MicOffIcon aria-hidden=\"true\" /> Mute\n                            </>\n                          )}\n                        </MenuItem>\n                        <MenuItem\n                          variant=\"destructive\"\n                          onClick={() => {\n                            setPendingRemoval(member);\n                            setModerationAction(\"remove\");\n                            setConfirmOpen(true);\n                          }}\n                        >\n                          <UserXIcon aria-hidden=\"true\" /> Remove from chat\n                        </MenuItem>\n                        <MenuItem\n                          variant=\"destructive\"\n                          onClick={() => {\n                            setPendingRemoval(member);\n                            setModerationAction(\"ban\");\n                            setConfirmOpen(true);\n                          }}\n                        >\n                          <BanIcon aria-hidden=\"true\" /> Ban from chat\n                        </MenuItem>\n                      </MenuPopup>\n                    </Menu>\n                  ) : undefined,\n              }))}\n            />\n            <ChatMemberSection\n              label=\"Banned users\"\n              heading=\"Banned users\"\n              members={bannedMembers.map((member) => ({\n                id: member,\n                name: CHAT_MEMBERS[member].name,\n                avatar: <ChatMemberAvatar member={member} />,\n                dimmed: true,\n                action: (\n                  <Button\n                    ref={(node) => {\n                      unbanRefs.current[member] = node ?? undefined;\n                    }}\n                    variant=\"outline\"\n                    size=\"xs\"\n                    aria-label={`Unban ${CHAT_MEMBERS[member].name}`}\n                    onClick={() => {\n                      onUnban(member);\n                      setRestoredMember(member);\n                    }}\n                  >\n                    Unban\n                  </Button>\n                ),\n              }))}\n            />\n          </div>\n        </DrawerPanel>\n        {canLeave && (\n          <AlertDialog\n            open={leaveConfirmOpen}\n            onOpenChange={(nextOpen) => {\n              if (nextOpen) leaveRequestedRef.current = false;\n              setLeaveConfirmOpen(nextOpen);\n            }}\n            onOpenChangeComplete={(isOpen) => {\n              if (!isOpen && leaveRequestedRef.current) onLeave();\n            }}\n          >\n            <DrawerFooter>\n              <AlertDialogTrigger\n                render={\n                  <Button\n                    ref={leaveTriggerRef}\n                    variant=\"destructive-outline\"\n                    size=\"sm\"\n                  />\n                }\n              >\n                <LogOutIcon aria-hidden=\"true\" />\n                Leave chat\n              </AlertDialogTrigger>\n            </DrawerFooter>\n            <AlertDialogPopup\n              finalFocus={() =>\n                leaveRequestedRef.current\n                  ? conversationFocusRef.current\n                  : leaveTriggerRef.current\n              }\n            >\n              <AlertDialogHeader>\n                <AlertDialogTitle>Leave this chat?</AlertDialogTitle>\n                <AlertDialogDescription>\n                  You’ll stop receiving messages from this group. Your ticket\n                  for the event stays valid.\n                </AlertDialogDescription>\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogClose render={<Button variant=\"ghost\" />}>\n                  Cancel\n                </AlertDialogClose>\n                <AlertDialogClose\n                  render={<Button variant=\"destructive\" />}\n                  onClick={() => {\n                    leaveRequestedRef.current = true;\n                  }}\n                >\n                  Leave chat\n                </AlertDialogClose>\n              </AlertDialogFooter>\n            </AlertDialogPopup>\n          </AlertDialog>\n        )}\n        <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>\n          <AlertDialogPopup\n            finalFocus={() => {\n              if (!pendingRemoval) return backRef.current;\n              return (\n                (bannedMembers.includes(pendingRemoval)\n                  ? unbanRefs.current[pendingRemoval]\n                  : editRefs.current[pendingRemoval]) ?? backRef.current\n              );\n            }}\n          >\n            <AlertDialogHeader>\n              <AlertDialogTitle>\n                {moderationAction === \"ban\" ? \"Ban\" : \"Remove\"}{\" \"}\n                {pendingRemoval ? CHAT_MEMBERS[pendingRemoval].name : \"member\"}{\" \"}\n                from the chat?\n              </AlertDialogTitle>\n              <AlertDialogDescription>\n                {moderationAction === \"ban\"\n                  ? \"They won’t be able to rejoin until you lift the ban. Their ticket stays valid.\"\n                  : \"They’ll leave the chat but can rejoin through their ticket invitation. Their ticket stays valid.\"}\n              </AlertDialogDescription>\n            </AlertDialogHeader>\n            <AlertDialogFooter>\n              <AlertDialogClose render={<Button variant=\"ghost\" />}>\n                Cancel\n              </AlertDialogClose>\n              <Button\n                variant=\"destructive\"\n                onClick={() => {\n                  if (!pendingRemoval) return;\n                  if (moderationAction === \"ban\") onBan(pendingRemoval);\n                  else onRemove(pendingRemoval);\n                  setConfirmOpen(false);\n                }}\n              >\n                {moderationAction === \"ban\"\n                  ? \"Ban from chat\"\n                  : \"Remove from chat\"}\n              </Button>\n            </AlertDialogFooter>\n          </AlertDialogPopup>\n        </AlertDialog>\n      </DrawerPopup>\n    </Drawer>\n  );\n}\n\nexport function ChatDemo({\n  renderTriggers,\n}: {\n  renderTriggers: (select: (scenario: ChatScenario) => void) => ReactNode;\n}): ReactElement {\n  const id = useId();\n  const position = useMediaQuery(\"md\") ? \"right\" : \"bottom\";\n  const [infoOpen, setInfoOpen] = useState(false);\n  const [membersOpen, setMembersOpen] = useState(false);\n  const [infoMessageId, setInfoMessageId] = useState<number | null>(null);\n  const [deleteMessageId, setDeleteMessageId] = useState<number | null>(null);\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const [scenario, setScenario] = useState<ChatScenario>(\"conversation\");\n  const [joinedChat, setJoinedChat] = useState(false);\n  const [unreadCount, setUnreadCount] = useState(0);\n  const [unreadFrom, setUnreadFrom] = useState<number | null>(null);\n  const [open, setOpen] = useState(false);\n  const [messages, setMessages] = useState<ChatMessage[]>(\n    INITIAL_CHAT_MESSAGES,\n  );\n  const [enteringMessageIds, setEnteringMessageIds] = useState<number[]>([]);\n  const [draftValue, setDraftValue] = useState<ComposerValue>([]);\n  const draft = getComposerText(draftValue);\n  const [replyTo, setReplyTo] = useState<number | null>(null);\n  const [replyFocus, setReplyFocus] = useState<ChatReplyFocusTarget | null>(\n    null,\n  );\n  const replyDismissalRef = useRef<\"cancel\" | \"send\">(\"cancel\");\n  const [pinnedId, setPinnedId] = useState<number | null>(null);\n  const [typingMembers, setTypingMembers] = useState<MemberId[]>([]);\n  const [mutedMembers, setMutedMembers] = useState<MemberId[]>([]);\n  const [members, setMembers] = useState<MemberId[]>([...OTHER_MEMBERS, \"you\"]);\n  const [bannedMembers, setBannedMembers] = useState<MemberId[]>([]);\n  const [notice, setNotice] = useState(\"\");\n  const [highlightedId, setHighlightedId] = useState<number | null>(null);\n  const infoReturnFocusRef = useRef<HTMLElement | null>(null);\n  const deleteReturnFocusRef = useRef<HTMLElement | null>(null);\n  const composerRef = useRef<HTMLDivElement>(null);\n  const focusComposerAfterRestoreRef = useRef(false);\n  const scroll = useChatScroll({\n    active: open,\n    paused: infoOpen || membersOpen || deleteOpen || replyFocus !== null,\n    itemCount: messages.length,\n    layoutKey: `${typingMembers.length}-${scenario}`,\n    onReachEnd: () => setUnreadCount(0),\n  });\n  const { viewportRef: timelineRef, atBottomRef } = scroll;\n  const nextIdRef = useRef(30);\n  const replyIndexRef = useRef(0);\n  const retryTimerRef = useRef<number | undefined>(undefined);\n  const reconnectTimerRef = useRef<number | undefined>(undefined);\n  const replyTimerRef = useRef<number | undefined>(undefined);\n  const noticeTimerRef = useRef<number | undefined>(undefined);\n  const highlightTimerRef = useRef<number | undefined>(undefined);\n  const cancelJumpRef = useRef<(() => void) | undefined>(undefined);\n  const messagesById = new Map(\n    messages.map((message) => [message.id, message]),\n  );\n  const dateGroups = messages.reduce<\n    Array<{\n      dateLabel: string;\n      entries: Array<{ message: ChatMessage; index: number }>;\n    }>\n  >((groups, message, index) => {\n    const currentGroup = groups.at(-1);\n    if (currentGroup?.dateLabel === message.dateLabel) {\n      currentGroup.entries.push({ message, index });\n    } else {\n      groups.push({\n        dateLabel: message.dateLabel,\n        entries: [{ message, index }],\n      });\n    }\n    return groups;\n  }, []);\n  const infoMessage =\n    infoMessageId === null ? undefined : messagesById.get(infoMessageId);\n  const replyMessage = replyTo === null ? undefined : messagesById.get(replyTo);\n  const pinnedMessage =\n    pinnedId === null ? undefined : messagesById.get(pinnedId);\n  const activeOtherMembers = members.filter((member) => member !== \"you\");\n  const composerDisabled = [\n    \"loading\",\n    \"error\",\n    \"offline\",\n    \"reconnecting\",\n    \"access-lost\",\n    \"removed\",\n    \"banned\",\n    \"left\",\n  ].includes(scenario);\n\n  useEffect(() => {\n    if (open && scenario === \"empty\" && joinedChat)\n      focusAtEnd(composerRef.current);\n    if (\n      open &&\n      scenario === \"conversation\" &&\n      focusComposerAfterRestoreRef.current\n    ) {\n      focusComposerAfterRestoreRef.current = false;\n      focusAtEnd(composerRef.current);\n    }\n  }, [open, scenario, joinedChat]);\n\n  const openMessageInfo = (\n    messageId: number,\n    trigger: HTMLElement | null,\n  ): void => {\n    infoReturnFocusRef.current = trigger;\n    atBottomRef.current = false;\n    setInfoMessageId(messageId);\n    setInfoOpen(true);\n  };\n\n  const finishNestedDrawerClose = scroll.updatePosition;\n\n  const changeScenario = (next: ChatScenario): void => {\n    window.clearTimeout(replyTimerRef.current);\n    window.clearTimeout(retryTimerRef.current);\n    window.clearTimeout(reconnectTimerRef.current);\n    window.clearTimeout(noticeTimerRef.current);\n    window.clearTimeout(highlightTimerRef.current);\n    cancelJumpRef.current?.();\n    setInfoOpen(false);\n    setMembersOpen(false);\n    setInfoMessageId(null);\n    setDeleteOpen(false);\n    setDeleteMessageId(null);\n    setScenario(next);\n    setJoinedChat(false);\n    setMessages(getScenarioMessages(next));\n    setEnteringMessageIds([]);\n    setTypingMembers(\n      next === \"typing\"\n        ? activeOtherMembers.filter((member) => member === \"maya\")\n        : next === \"typing-multiple\"\n          ? activeOtherMembers\n          : [],\n    );\n    setReplyTo(null);\n    setReplyFocus(null);\n    setHighlightedId(null);\n    setNotice(\"\");\n    setPinnedId(next === \"pinned\" ? 5 : null);\n    nextIdRef.current = 30;\n    setUnreadCount(next === \"unread\" ? 3 : 0);\n    setUnreadFrom(next === \"unread\" ? 5 : null);\n    scroll.resetScroll(next !== \"unread\");\n  };\n\n  const retryMessage = (messageId: number): void => {\n    setMessages((current) =>\n      current.map((message) =>\n        message.id === messageId\n          ? { ...message, delivery: \"sending\" }\n          : message,\n      ),\n    );\n    window.clearTimeout(retryTimerRef.current);\n    retryTimerRef.current = window.setTimeout(() => {\n      setMessages((current) =>\n        current.map((message) =>\n          message.id === messageId\n            ? { ...message, delivery: \"delivered\", receipts: undefined }\n            : message,\n        ),\n      );\n      announce(\"Message delivered\");\n    }, 1200);\n  };\n\n  useEffect(\n    () => () => {\n      window.clearTimeout(replyTimerRef.current);\n      window.clearTimeout(retryTimerRef.current);\n      window.clearTimeout(reconnectTimerRef.current);\n      window.clearTimeout(noticeTimerRef.current);\n      window.clearTimeout(highlightTimerRef.current);\n      cancelJumpRef.current?.();\n    },\n    [],\n  );\n\n  const announce = (text: string): void => {\n    setNotice(text);\n    window.clearTimeout(noticeTimerRef.current);\n    noticeTimerRef.current = window.setTimeout(() => setNotice(\"\"), 3000);\n  };\n\n  const sendMessage = (): void => {\n    const text = draft.trim();\n    if (!text || composerDisabled) return;\n    if (\n      scenario === \"empty\" ||\n      scenario === \"typing\" ||\n      scenario === \"typing-multiple\"\n    )\n      setScenario(\"conversation\");\n    const time = new Intl.DateTimeFormat(\"en\", {\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      hourCycle: \"h23\",\n    }).format(new Date());\n    const message: ChatMessage = {\n      id: nextIdRef.current++,\n      author: \"you\",\n      text,\n      dateLabel: \"Today\",\n      time,\n      replyTo: replyTo ?? undefined,\n      reactions: [],\n    };\n    atBottomRef.current = true;\n    setEnteringMessageIds((current) => [...current, message.id]);\n    setMessages((current) => [...current, message]);\n    setDraftValue([]);\n    replyDismissalRef.current = \"send\";\n    setReplyTo(null);\n    setTypingMembers([\"maya\"]);\n    window.clearTimeout(replyTimerRef.current);\n    replyTimerRef.current = window.setTimeout(() => {\n      const response: ChatMessage = {\n        id: nextIdRef.current++,\n        author: \"maya\",\n        text:\n          DEMO_REPLIES[replyIndexRef.current++ % DEMO_REPLIES.length] ??\n          \"Thanks for sharing!\",\n        dateLabel: \"Today\",\n        time,\n        reactions: [],\n      };\n      if (!atBottomRef.current) {\n        setUnreadCount((count) => count + 1);\n        setUnreadFrom((first) => first ?? response.id);\n      }\n      if (timelineRef.current && atBottomRef.current) {\n        setEnteringMessageIds((current) => [...current, response.id]);\n      }\n      setMessages((current) => [\n        ...current.map((item) =>\n          item.author === \"you\" &&\n          item.delivery !== \"failed\" &&\n          item.delivery !== \"sending\"\n            ? { ...item, delivery: \"read\" as const, receipts: undefined }\n            : item,\n        ),\n        response,\n      ]);\n      setTypingMembers([]);\n    }, 1800);\n  };\n\n  const onReply = (\n    messageId: number,\n    source: HTMLDivElement,\n    firstInGroup: boolean,\n    expanded: boolean,\n  ): void => {\n    cancelJumpRef.current?.();\n    setHighlightedId(null);\n    atBottomRef.current = false;\n    replyDismissalRef.current = \"cancel\";\n    setReplyFocus({\n      messageId,\n      source,\n      width: source.getBoundingClientRect().width,\n      firstInGroup,\n      expanded,\n    });\n    setReplyTo(messageId);\n    focusAtEnd(composerRef.current);\n  };\n\n  const cancelReply = (): void => {\n    replyDismissalRef.current = \"cancel\";\n    setReplyTo(null);\n  };\n\n  const onReact = (messageId: number, emoji: string): void => {\n    setMessages((current) =>\n      current.map((message) =>\n        message.id === messageId\n          ? { ...message, reactions: toggleReaction(message.reactions, emoji) }\n          : message,\n      ),\n    );\n  };\n\n  const requestDelete = (\n    messageId: number,\n    trigger: HTMLElement | null,\n  ): void => {\n    const message = messagesById.get(messageId);\n    if (!message || !getMessageDeletionReason(message)) return;\n    deleteReturnFocusRef.current = trigger;\n    setDeleteMessageId(messageId);\n    setDeleteOpen(true);\n  };\n\n  const confirmDelete = (): void => {\n    if (deleteMessageId === null) return;\n    const message = messagesById.get(deleteMessageId);\n    if (!message || !getMessageDeletionReason(message)) return;\n    setMessages((current) =>\n      current.map((message) =>\n        message.id === deleteMessageId ? deleteChatMessage(message) : message,\n      ),\n    );\n    if (pinnedId === deleteMessageId) setPinnedId(null);\n    if (replyTo === deleteMessageId) setReplyTo(null);\n    announce(getMessagePreview(deleteChatMessage(message)));\n  };\n\n  const onCopy = async (text: string): Promise<void> => {\n    try {\n      await navigator.clipboard.writeText(text);\n      announce(\"Message copied\");\n    } catch {\n      announce(\"Couldn’t copy. Select the message text to copy it.\");\n    }\n  };\n\n  const removeMember = (member: MemberId): void => {\n    setMembers((current) => current.filter((id) => id !== member));\n    setTypingMembers((current) => current.filter((id) => id !== member));\n    setMessages((current) =>\n      current.map((message) => ({\n        ...message,\n        receipts: getMessageReceipts(message, activeOtherMembers).filter(\n          (receipt) => receipt.member !== member,\n        ),\n      })),\n    );\n  };\n\n  const onPin = (messageId: number): void => {\n    setPinnedId((current) => (current === messageId ? null : messageId));\n    announce(pinnedId === messageId ? \"Message unpinned\" : \"Message pinned\");\n  };\n\n  const onJump = (messageId: number): void => {\n    const target = document.getElementById(`${id}-message-${messageId}`);\n    if (!target) return;\n    atBottomRef.current = false;\n    cancelJumpRef.current?.();\n    window.clearTimeout(highlightTimerRef.current);\n    setHighlightedId(null);\n    const timeline = timelineRef.current;\n    const reveal = (): void => {\n      cancelJumpRef.current?.();\n      setHighlightedId(messageId);\n      highlightTimerRef.current = window.setTimeout(\n        () => setHighlightedId(null),\n        420,\n      );\n    };\n    const waitForScroll = (): void => {\n      window.clearTimeout(highlightTimerRef.current);\n      highlightTimerRef.current = window.setTimeout(reveal, 100);\n    };\n    timeline?.addEventListener(\"scroll\", waitForScroll, { passive: true });\n    cancelJumpRef.current = () => {\n      timeline?.removeEventListener(\"scroll\", waitForScroll);\n      window.clearTimeout(highlightTimerRef.current);\n    };\n    waitForScroll();\n    target.scrollIntoView({\n      block: \"center\",\n      behavior: window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n        ? \"auto\"\n        : \"smooth\",\n    });\n    target.focus({ preventScroll: true });\n  };\n\n  return (\n    <TooltipProvider delay={300}>\n      <Drawer\n        position={position}\n        open={open}\n        onOpenChange={(nextOpen, details) => {\n          if (\n            !nextOpen &&\n            details.reason === \"escape-key\" &&\n            replyTo !== null\n          ) {\n            details.cancel();\n            cancelReply();\n            return;\n          }\n          setOpen(nextOpen);\n          setEnteringMessageIds([]);\n          if (!nextOpen) {\n            cancelJumpRef.current?.();\n            setHighlightedId(null);\n            setInfoOpen(false);\n            setMembersOpen(false);\n          }\n        }}\n        onOpenChangeComplete={(isOpen) => {\n          if (isOpen) return;\n          setReplyTo(null);\n          setReplyFocus(null);\n        }}\n      >\n        {renderTriggers(changeScenario)}\n        <DrawerPopup\n          variant=\"inset\"\n          showCloseButton\n          portalProps={{\n            className: \"max-md:[&_[data-slot=drawer-viewport]]:pt-2\",\n          }}\n          className={\n            position === \"bottom\"\n              ? \"pb-[max(var(--drawer-keyboard-inset,0px),env(safe-area-inset-bottom,0px))] [--drawer-height:100dvh]\"\n              : undefined\n          }\n        >\n          <DrawerHeader>\n            <DrawerTitle>The studio</DrawerTitle>\n            <DrawerDescription>\n              {scenario === \"removed\" ? (\n                \"You were removed from this chat\"\n              ) : scenario === \"banned\" ? (\n                \"You were banned from this chat\"\n              ) : scenario === \"left\" ? (\n                \"You left this chat\"\n              ) : (\n                <GroupDetails\n                  open={membersOpen}\n                  onOpenChange={(nextOpen) => {\n                    if (nextOpen) atBottomRef.current = false;\n                    setMembersOpen(nextOpen);\n                  }}\n                  onClosed={finishNestedDrawerClose}\n                  position={position}\n                  activeMembers={members}\n                  mutedMembers={mutedMembers}\n                  bannedMembers={bannedMembers}\n                  onToggleMute={(member) =>\n                    setMutedMembers((current) =>\n                      current.includes(member)\n                        ? current.filter((id) => id !== member)\n                        : [...current, member],\n                    )\n                  }\n                  onRemove={removeMember}\n                  onBan={(member) => {\n                    setBannedMembers((current) => [...current, member]);\n                    removeMember(member);\n                  }}\n                  onUnban={(member) => {\n                    setBannedMembers((current) =>\n                      current.filter((id) => id !== member),\n                    );\n                    setMembers((current) =>\n                      current.includes(member) ? current : [...current, member],\n                    );\n                  }}\n                  canLeave={scenario !== \"access-lost\"}\n                  onLeave={() => {\n                    changeScenario(\"left\");\n                    setDraftValue([]);\n                  }}\n                  conversationFocusRef={timelineRef}\n                />\n              )}\n            </DrawerDescription>\n          </DrawerHeader>\n          {pinnedMessage && !pinnedMessage.deletedReason && (\n            <Button\n              variant=\"ghost\"\n              className=\"h-auto shrink-0 justify-start gap-3 rounded-none border-border/60 border-x-0 border-y bg-muted/72 px-5 py-2.5 text-start sm:h-auto\"\n              onClick={() => onJump(pinnedMessage.id)}\n              aria-label=\"Jump to pinned message\"\n            >\n              <PinIcon\n                aria-hidden=\"true\"\n                className=\"size-3.5 text-muted-foreground\"\n              />\n              <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                <span className=\"font-medium text-[.625rem] text-muted-foreground\">\n                  Pinned message\n                </span>\n                <span className=\"truncate font-normal text-xs\">\n                  {pinnedMessage.text}\n                </span>\n              </span>\n              <ChevronRightIcon\n                aria-hidden=\"true\"\n                className=\"size-3 text-muted-foreground\"\n              />\n            </Button>\n          )}\n          {(scenario === \"offline\" || scenario === \"reconnecting\") && (\n            <ChatStatusBar\n              title={\n                scenario === \"offline\" ? \"You’re offline\" : \"Reconnecting…\"\n              }\n              description=\"Your draft is saved here. Sending will resume when connected.\"\n              action={\n                <Button\n                  variant=\"outline\"\n                  size=\"xs\"\n                  loading={scenario === \"reconnecting\"}\n                  onClick={() => {\n                    setScenario(\"reconnecting\");\n                    window.clearTimeout(reconnectTimerRef.current);\n                    reconnectTimerRef.current = window.setTimeout(() => {\n                      focusComposerAfterRestoreRef.current = true;\n                      setScenario(\"conversation\");\n                      announce(\"Connected\");\n                    }, 1200);\n                  }}\n                >\n                  Reconnect\n                </Button>\n              }\n            />\n          )}\n          <DrawerPanel\n            scrollable={false}\n            className=\"relative flex min-h-0 flex-1 flex-col overflow-clip p-0 pt-0!\"\n          >\n            <ChatConversation\n              scroll={scroll}\n              label=\"Conversation messages\"\n              inactive={replyFocus !== null}\n              jumpLabel={\n                unreadCount\n                  ? `${unreadCount} new ${unreadCount === 1 ? \"message\" : \"messages\"}`\n                  : \"Jump to latest\"\n              }\n              contentClassName={cn(\n                [\n                  \"empty\",\n                  \"error\",\n                  \"access-lost\",\n                  \"removed\",\n                  \"banned\",\n                  \"left\",\n                ].includes(scenario) && \"flex min-h-full flex-col\",\n                (scenario === \"removed\" ||\n                  scenario === \"banned\" ||\n                  scenario === \"left\") &&\n                  \"justify-center\",\n              )}\n              overlay={\n                <AnimatePresence\n                  initial={false}\n                  onExitComplete={() => {\n                    setReplyFocus(null);\n                    finishNestedDrawerClose();\n                  }}\n                >\n                  {replyMessage && replyFocus && (\n                    <ChatDemoReplyFocus\n                      key={replyMessage.id}\n                      target={replyFocus}\n                      message={replyMessage}\n                      replyMessage={messagesById.get(\n                        replyMessage.replyTo ?? -1,\n                      )}\n                      dismissalRef={replyDismissalRef}\n                      onCancel={cancelReply}\n                    />\n                  )}\n                </AnimatePresence>\n              }\n            >\n              <ChatTimelineState\n                scenario={scenario}\n                onRecover={() => {\n                  if (scenario === \"left\" || scenario === \"removed\")\n                    focusComposerAfterRestoreRef.current = true;\n                  changeScenario(\"conversation\");\n                }}\n                memberCount={activeOtherMembers.length}\n                joined={joinedChat}\n                onJoin={() => setJoinedChat(true)}\n              />\n              <div>\n                {dateGroups.map((group) => (\n                  <div\n                    key={group.entries[0]?.message.id}\n                    className=\"not-first:mt-4 flex flex-col gap-4 first:pt-4\"\n                  >\n                    {group.dateLabel && (\n                      <ChatDatePill>{group.dateLabel}</ChatDatePill>\n                    )}\n                    <div className=\"flex flex-col\">\n                      {group.entries.map(({ message, index }, entryIndex) => {\n                        const previousMessage = messages[index - 1];\n                        const grouped =\n                          previousMessage?.author === message.author &&\n                          previousMessage.dateLabel === message.dateLabel &&\n                          !message.deletedReason &&\n                          !previousMessage.deletedReason;\n                        const followsSeparator =\n                          entryIndex === 0 || message.id === unreadFrom;\n                        return (\n                          <Fragment key={message.id}>\n                            {message.id === unreadFrom && (\n                              <ChatUnreadDivider className=\"first:mt-0\">\n                                New messages\n                              </ChatUnreadDivider>\n                            )}\n                            <div\n                              className={cn(\n                                !followsSeparator &&\n                                  (grouped\n                                    ? previousMessage.delivery\n                                      ? \"pt-3\"\n                                      : \"pt-1\"\n                                    : \"pt-5\"),\n                              )}\n                            >\n                              <ChatDemoMessage\n                                message={message}\n                                recipients={activeOtherMembers}\n                                entering={enteringMessageIds.includes(\n                                  message.id,\n                                )}\n                                onEntranceEnd={() =>\n                                  setEnteringMessageIds((current) =>\n                                    current.filter((id) => id !== message.id),\n                                  )\n                                }\n                                replyMessage={messagesById.get(\n                                  message.replyTo ?? -1,\n                                )}\n                                grouped={grouped}\n                                groupEnd={\n                                  messages[index + 1]?.author !==\n                                    message.author ||\n                                  messages[index + 1]?.dateLabel !==\n                                    message.dateLabel ||\n                                  Boolean(\n                                    message.deletedReason ||\n                                      messages[index + 1]?.deletedReason,\n                                  )\n                                }\n                                pinned={pinnedId === message.id}\n                                highlighted={highlightedId === message.id}\n                                replyLifted={\n                                  replyFocus?.messageId === message.id\n                                }\n                                domId={`${id}-message-${message.id}`}\n                                onReply={onReply}\n                                onReact={onReact}\n                                onPin={onPin}\n                                onDelete={requestDelete}\n                                onCopy={onCopy}\n                                onJump={onJump}\n                                onRetry={retryMessage}\n                                onInfo={openMessageInfo}\n                              />\n                            </div>\n                          </Fragment>\n                        );\n                      })}\n                    </div>\n                  </div>\n                ))}\n              </div>\n              <ChatTypingIndicator\n                visible={typingMembers.length > 0}\n                label={getTypingLabel(typingMembers)}\n                className=\"ps-9 pt-5\"\n              />\n            </ChatConversation>\n          </DrawerPanel>\n          {scenario === \"empty\" && !joinedChat && (\n            <DrawerFooter\n              variant=\"bare\"\n              className=\"items-center text-muted-foreground text-xs sm:justify-center\"\n            >\n              <div className=\"flex flex-col gap-1.5\">\n                <span className=\"flex items-start gap-2\">\n                  <LockIcon\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 size-3.5 shrink-0\"\n                  />\n                  Only members can see this chat and who’s in it.\n                </span>\n                <span className=\"flex items-start gap-2\">\n                  <LogOutIcon\n                    aria-hidden=\"true\"\n                    className=\"mt-0.5 size-3.5 shrink-0\"\n                  />\n                  Leave any time — your ticket stays valid.\n                </span>\n              </div>\n            </DrawerFooter>\n          )}\n          {scenario !== \"removed\" &&\n            scenario !== \"banned\" &&\n            scenario !== \"left\" &&\n            scenario !== \"error\" &&\n            scenario !== \"access-lost\" &&\n            (scenario !== \"empty\" || joinedChat) && (\n              <ChatDemoComposer\n                value={draftValue}\n                onValueChange={setDraftValue}\n                replyAuthor={\n                  replyMessage\n                    ? CHAT_MEMBERS[replyMessage.author].name\n                    : undefined\n                }\n                mentionMembers={activeOtherMembers}\n                onSend={sendMessage}\n                inputRef={composerRef}\n                notice={notice}\n                disabled={composerDisabled}\n              />\n            )}\n          <ChatMessageInfo\n            message={infoMessage}\n            recipients={activeOtherMembers}\n            replyMessage={messagesById.get(infoMessage?.replyTo ?? -1)}\n            position={position}\n            open={infoOpen}\n            onOpenChange={setInfoOpen}\n            onClosed={finishNestedDrawerClose}\n            returnFocusRef={infoReturnFocusRef}\n          />\n          <AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>\n            <AlertDialogPopup\n              finalFocus={() => {\n                if (deleteReturnFocusRef.current?.isConnected)\n                  return deleteReturnFocusRef.current;\n                return (\n                  document.getElementById(`${id}-message-${deleteMessageId}`) ??\n                  timelineRef.current\n                );\n              }}\n            >\n              <AlertDialogHeader>\n                <AlertDialogTitle>Delete this message?</AlertDialogTitle>\n                <AlertDialogDescription>\n                  This message will be deleted for everyone. This can’t be\n                  undone.\n                </AlertDialogDescription>\n              </AlertDialogHeader>\n              <AlertDialogFooter>\n                <AlertDialogClose render={<Button variant=\"ghost\" />}>\n                  Cancel\n                </AlertDialogClose>\n                <AlertDialogClose\n                  render={<Button variant=\"destructive\" />}\n                  onClick={confirmDelete}\n                >\n                  Delete message\n                </AlertDialogClose>\n              </AlertDialogFooter>\n            </AlertDialogPopup>\n          </AlertDialog>\n        </DrawerPopup>\n      </Drawer>\n    </TooltipProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/chat-demo/chat-demo.tsx"
    }
  ],
  "type": "registry:block"
}