# Mailbox

> A responsive mailbox block with folders, message lists, compose flow, confirmations, and toast feedback.

A shadcn/ui block from the [@corr](https://ui.corr.sh/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/corr/mailbox).

- Page, with a live preview: https://shadcn.exchange/r/corr/mailbox
- Screenshot: https://shadcn.exchange/shot/corr/mailbox/light.png
- Registry item: https://ui.corr.sh/r/mailbox.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/corr/mailbox.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `sonner`
- Registry items: `alert-dialog`, `avatar`, `badge`, `button`, `card`, `combobox`, `context-menu`, `dialog`, `dropdown-menu`, `https://ui.corr.sh/r/file-upload-dropzone.json`, `input`, `menubar`, `resizable`, `https://ui.corr.sh/r/scroll-blur.json`, `separator`, `sidebar`, `sonner`, `textarea`, `toggle-group`

## Files

### `registry/corr/components/mailbox.tsx`

```tsx
"use client"

import * as React from "react"
import { toast } from "sonner"
import {
  ArchiveIcon,
  ArrowLeftIcon,
  BoldIcon,
  Edit3Icon,
  ForwardIcon,
  FolderIcon,
  InboxIcon,
  ItalicIcon,
  MailIcon,
  MoreHorizontalIcon,
  PaperclipIcon,
  PanelLeftIcon,
  ReplyIcon,
  RefreshCcwIcon,
  SearchIcon,
  SendIcon,
  StarIcon,
  Trash2Icon,
  UnderlineIcon,
  XIcon,
} from "lucide-react"

import { FileUploadDropzone, type FileUploadDropzoneFile } from "./file-upload-dropzone"
import { ScrollBlur } from "./scroll-blur"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  useComboboxAnchor,
} from "@/components/ui/combobox"
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import {
  Menubar,
  MenubarContent,
  MenubarItem,
  MenubarMenu,
  MenubarSeparator,
  MenubarTrigger,
} from "@/components/ui/menubar"
import {
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
} from "@/components/ui/resizable"
import { Separator } from "@/components/ui/separator"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
} from "@/components/ui/sidebar"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { cn } from "@/lib/utils"

export type MailboxFolder = {
  id: string
  label: string
  count?: number
  icon?: React.ComponentType<{ className?: string }>
}

export type MailboxContact = {
  id: string
  name: string
  email: string
  avatar?: string
}

export type MailboxMessage = {
  id: string
  folderId: string
  from: MailboxContact
  to?: MailboxContact[]
  cc?: MailboxContact[]
  bcc?: MailboxContact[]
  subject: string
  preview: string
  body: string
  timestamp: string
  unread?: boolean
  starred?: boolean
  attachments?: number
  thread?: MailboxThreadReply[]
}

export type MailboxThreadReply = {
  id: string
  from: MailboxContact
  to?: MailboxContact[]
  cc?: MailboxContact[]
  body: string
  timestamp: string
}

type MailboxPane = "folders" | "list" | "detail" | "compose"
const STARRED_FOLDER_ID = "starred"

type MailboxComposeDraft = {
  to?: string[]
  cc?: string[]
  bcc?: string[]
  subject?: string
  body?: string
}

export const defaultMailboxFolders: MailboxFolder[] = [
  { id: "inbox", label: "Inbox", count: 6, icon: InboxIcon },
  { id: "drafts", label: "Drafts", count: 2, icon: Edit3Icon },
  { id: "sent", label: "Sent", icon: SendIcon },
  { id: "archive", label: "Archive", icon: ArchiveIcon },
  { id: "trash", label: "Trash", icon: Trash2Icon },
]

export const defaultMailboxContacts: MailboxContact[] = [
  { id: "mina", name: "Mina Patel", email: "mina@corr.sh" },
  { id: "avery", name: "Avery Stone", email: "avery@corr.sh" },
  { id: "nora", name: "Nora West", email: "nora@corr.sh" },
  { id: "theo", name: "Theo Brooks", email: "theo@corr.sh" },
]

export const defaultMailboxMessages: MailboxMessage[] = [
  {
    id: "msg-1",
    folderId: "inbox",
    from: defaultMailboxContacts[0],
    to: [defaultMailboxContacts[1]],
    cc: [defaultMailboxContacts[2]],
    subject: "Registry publish checklist",
    preview: "The install payloads are ready for review before release.",
    body: "I checked the registry payloads, preview screenshots, and install commands. The only remaining item is confirming the chart state examples before we publish.",
    timestamp: "09:42",
    unread: true,
    starred: true,
    attachments: 2,
    thread: [
      {
        id: "msg-1-thread-1",
        from: defaultMailboxContacts[1],
        to: [defaultMailboxContacts[0]],
        body: "The install command examples are good. Can we include one screenshot of the generated JSON as well?",
        timestamp: "08:58",
      },
      {
        id: "msg-1-thread-2",
        from: defaultMailboxContacts[0],
        to: [defaultMailboxContacts[1]],
        cc: [defaultMailboxContacts[2]],
        body: "Yes, I added the JSON preview and checked the responsive shell captures. I am just doing one last pass on chart states.",
        timestamp: "09:17",
      },
      {
        id: "msg-1-thread-3",
        from: defaultMailboxContacts[2],
        to: [defaultMailboxContacts[0], defaultMailboxContacts[1]],
        body: "Chart states are in. Loading, empty, and error examples now all use the shared wrapper.",
        timestamp: "09:31",
      },
    ],
  },
  {
    id: "msg-2",
    folderId: "inbox",
    from: defaultMailboxContacts[1],
    subject: "Preview shell screenshots",
    preview: "Mobile and tablet frames are looking cleaner after the shell update.",
    body: "The responsive preview shell now gives us a much better way to inspect the same component across breakpoints without duplicating examples.",
    timestamp: "Yesterday",
  },
  {
    id: "msg-3",
    folderId: "inbox",
    from: defaultMailboxContacts[2],
    subject: "Component metrics",
    preview: "Copy and install events are now visible in the dashboard.",
    body: "The component metrics are flowing through the install card. We can use this to compare page views, source copies, and registry JSON requests.",
    timestamp: "May 20",
    unread: true,
  },
  {
    id: "msg-4",
    folderId: "inbox",
    from: defaultMailboxContacts[3],
    subject: "Form builder drag preview",
    preview: "The drag overlay now follows the cursor more closely.",
    body: "The form builder drag preview is behaving much better after moving the overlay into a body-level portal and using the grip handle for drag activation.",
    timestamp: "May 19",
    attachments: 1,
  },
  {
    id: "msg-5",
    folderId: "inbox",
    from: defaultMailboxContacts[1],
    subject: "Timeline mobile agenda",
    preview: "The mobile agenda can expose slot creation between items.",
    body: "The timeline mobile mode should keep the agenda readable while still making slot creation available through the inline add affordance.",
    timestamp: "May 18",
  },
  {
    id: "msg-6",
    folderId: "inbox",
    from: defaultMailboxContacts[2],
    subject: "Kanban tab behaviour",
    preview: "Tablet and mobile now use bucket tabs instead of squeezing columns.",
    body: "The kanban board should adapt into bucket tabs at constrained widths and keep move actions available from each card menu.",
    timestamp: "May 17",
    unread: true,
  },
  {
    id: "msg-7",
    folderId: "inbox",
    from: defaultMailboxContacts[0],
    subject: "Mailbox block polish",
    preview: "The toolbar, compose flow, and scroll areas are getting closer.",
    body: "The mailbox block now has a top toolbar, independent scroll areas, context menus, and a compose workflow with attachments.",
    timestamp: "May 16",
  },
  {
    id: "msg-8",
    folderId: "drafts",
    from: defaultMailboxContacts[3],
    subject: "Draft: launch notes",
    preview: "Short release notes for the latest registry additions.",
    body: "Need to mention the mailbox block, chart state utility, and responsive previews in the release notes.",
    timestamp: "Draft",
  },
  {
    id: "msg-9",
    folderId: "sent",
    from: defaultMailboxContacts[0],
    subject: "Re: Design review",
    preview: "Sent over the final screenshots and implementation notes.",
    body: "Thanks, I sent the final screenshots and noted the mobile behaviour in the implementation summary.",
    timestamp: "May 18",
  },
]

export function MailboxBlock({
  folders = defaultMailboxFolders,
  messages: initialMessages = defaultMailboxMessages,
  contacts = defaultMailboxContacts,
  initialFolderId = "inbox",
  initialMessageId,
  onSend,
  onDelete,
  onArchive,
  onMarkRead,
  className,
}: {
  folders?: MailboxFolder[]
  messages?: MailboxMessage[]
  contacts?: MailboxContact[]
  initialFolderId?: string
  initialMessageId?: string
  onSend?: (message: MailboxMessage) => void
  onDelete?: (message: MailboxMessage) => void
  onArchive?: (message: MailboxMessage) => void
  onMarkRead?: (message: MailboxMessage) => void
  className?: string
}) {
  const rootRef = React.useRef<HTMLDivElement | null>(null)
  const [width, setWidth] = React.useState(1024)
  const [messages, setMessages] = React.useState(initialMessages)
  const [folderId, setFolderId] = React.useState(initialFolderId)
  const [selectedId, setSelectedId] = React.useState(
    initialMessageId ?? initialMessages.find((message) => message.folderId === initialFolderId)?.id
  )
  const [pane, setPane] = React.useState<MailboxPane>("list")
  const [deleteTarget, setDeleteTarget] = React.useState<MailboxMessage | null>(null)
  const [query, setQuery] = React.useState("")
  const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false)
  const [composeDraft, setComposeDraft] = React.useState<MailboxComposeDraft | undefined>()
  const [refreshing, setRefreshing] = React.useState(false)
  const [unreadFirst, setUnreadFirst] = React.useState(false)
  const [compactList, setCompactList] = React.useState(false)
  const compact = width < 760

  React.useEffect(() => {
    const element = rootRef.current
    if (!element) return

    const observer = new ResizeObserver(([entry]) => {
      setWidth(entry.contentRect.width)
    })
    observer.observe(element)
    return () => observer.disconnect()
  }, [])

  const selectedFolder =
    folderId === STARRED_FOLDER_ID
      ? { id: STARRED_FOLDER_ID, label: "Starred", icon: StarIcon }
      : folders.find((folder) => folder.id === folderId) ?? folders[0]
  const folderMessages = messages
    .filter(
      (message) =>
        (folderId === STARRED_FOLDER_ID ? message.starred : message.folderId === folderId) &&
        `${message.subject} ${message.from.name} ${message.preview}`
          .toLowerCase()
          .includes(query.toLowerCase())
    )
    .sort((first, second) => {
      if (!unreadFirst) return 0
      return Number(Boolean(second.unread)) - Number(Boolean(first.unread))
    })
  const selectedMessage =
    refreshing ? undefined : messages.find((message) => message.id === selectedId) ?? folderMessages[0]

  React.useEffect(() => {
    if (refreshing) return
    if (selectedId || !folderMessages[0]) return
    setSelectedId(folderMessages[0].id)
  }, [folderMessages, refreshing, selectedId])

  function selectFolder(nextFolderId: string) {
    setFolderId(nextFolderId)
    const nextMessage =
      nextFolderId === STARRED_FOLDER_ID
        ? messages.find((message) => message.starred)
        : messages.find((message) => message.folderId === nextFolderId)
    setSelectedId(nextMessage?.id)
    setPane("list")
  }

  function markAllAsRead() {
    setMessages((current) =>
      current.map((message) =>
        folderId === STARRED_FOLDER_ID
          ? message.starred
            ? { ...message, unread: false }
            : message
          : message.folderId === folderId
            ? { ...message, unread: false }
            : message
      )
    )
    toast.success("Mailbox marked as read", {
      description: `${selectedFolder.label} has no unread messages.`,
    })
  }

  function refreshMailbox() {
    setRefreshing(true)
    setSelectedId(undefined)
    window.setTimeout(() => {
      setRefreshing(false)
      setSelectedId(folderMessages[0]?.id)
      toast.success("Mailbox refreshed", {
        description: "Latest messages are up to date.",
      })
    }, 900)
  }

  function toggleUnreadFirst() {
    setUnreadFirst((current) => !current)
  }

  function toggleCompactList() {
    setCompactList((current) => !current)
  }

  function selectMessage(message: MailboxMessage) {
    setSelectedId(message.id)
    if (message.unread) {
      const readMessage = { ...message, unread: false }
      setMessages((current) =>
        current.map((item) => (item.id === message.id ? readMessage : item))
      )
      onMarkRead?.(readMessage)
    }
    if (compact) setPane("detail")
  }

  function archiveMessage(message: MailboxMessage) {
    const archived = { ...message, folderId: "archive", unread: false }
    setMessages((current) =>
      current.map((item) => (item.id === message.id ? archived : item))
    )
    onArchive?.(archived)
    toast.success("Email archived", { description: message.subject })
    if (compact) setPane("list")
  }

  function deleteMessage(message: MailboxMessage) {
    setMessages((current) => current.filter((item) => item.id !== message.id))
    onDelete?.(message)
    setDeleteTarget(null)
    setSelectedId(folderMessages.find((item) => item.id !== message.id)?.id)
    toast.error("Email deleted", { description: message.subject })
    if (compact) setPane("list")
  }

  function moveMessage(message: MailboxMessage, nextFolderId: string) {
    const moved = { ...message, folderId: nextFolderId, unread: false }
    setMessages((current) =>
      current.map((item) => (item.id === message.id ? moved : item))
    )
    setSelectedId(folderMessages.find((item) => item.id !== message.id)?.id)
    const nextFolder = folders.find((folder) => folder.id === nextFolderId)
    toast.success("Email moved", {
      description: `${message.subject} moved to ${nextFolder?.label ?? "folder"}.`,
    })
  }

  function toggleStarMessage(message: MailboxMessage) {
    const nextMessage = { ...message, starred: !message.starred }
    setMessages((current) =>
      current.map((item) => (item.id === message.id ? nextMessage : item))
    )
    if (folderId === STARRED_FOLDER_ID && message.starred) {
      setSelectedId(messages.find((item) => item.id !== message.id && item.starred)?.id)
    }
  }

  function openCompose(draft?: MailboxComposeDraft) {
    setComposeDraft(draft)
    setPane("compose")
  }

  function replyToMessage(message: MailboxMessage) {
    setSelectedId(message.id)
    openCompose({
      to: [message.from.email],
      subject: withSubjectPrefix(message.subject, "Re:"),
      body: buildThreadQuote(message, "reply"),
    })
  }

  function replyAllToMessage(message: MailboxMessage) {
    const currentUserEmail = contacts[0]?.email
    const emails = uniqueEmails([
      message.from.email,
      ...(message.to ?? []).map((contact) => contact.email),
    ]).filter((email) => email !== currentUserEmail)
    const ccEmails = uniqueEmails((message.cc ?? []).map((contact) => contact.email)).filter(
      (email) => email !== currentUserEmail && !emails.includes(email)
    )

    setSelectedId(message.id)
    openCompose({
      to: emails,
      cc: ccEmails,
      subject: withSubjectPrefix(message.subject, "Re:"),
      body: buildThreadQuote(message, "reply"),
    })
  }

  function forwardMessage(message: MailboxMessage) {
    setSelectedId(message.id)
    openCompose({
      subject: withSubjectPrefix(message.subject, "Fwd:"),
      body: buildThreadQuote(message, "forward"),
    })
  }

  return (
    <Card
      ref={rootRef}
      data-slot="mailbox-block"
      className={cn("relative h-[34rem] min-w-0 overflow-hidden p-0", className)}
    >
      <SidebarProvider
        className="h-full min-h-0"
        style={
          {
            "--sidebar-width": compact ? "15rem" : sidebarCollapsed ? "4.25rem" : "15rem",
          } as React.CSSProperties
        }
      >
        {compact ? (
          <div className="flex h-full min-w-0 flex-1 flex-col">
            <MailboxToolbar
              compact
              pane={pane}
              query={query}
              onQueryChange={setQuery}
              showSearch={width > 560}
              iconOnlyCompose={width < 640}
              selectedMessage={selectedMessage}
              unreadFirst={unreadFirst}
              compactList={compactList}
              onMarkAllRead={markAllAsRead}
              onRefresh={refreshMailbox}
              onShowArchived={() => selectFolder("archive")}
              onReply={() => selectedMessage && replyToMessage(selectedMessage)}
              onForward={() => selectedMessage && forwardMessage(selectedMessage)}
              onDelete={() => selectedMessage && setDeleteTarget(selectedMessage)}
              onShowStarred={() => selectFolder(STARRED_FOLDER_ID)}
              onToggleUnreadFirst={toggleUnreadFirst}
              onToggleCompactList={toggleCompactList}
              onBack={() => setPane(pane === "detail" || pane === "compose" ? "list" : "folders")}
              onFolders={() => setPane("folders")}
              onCompose={() => openCompose()}
            />
            <div className="grid min-h-0 flex-1 grid-cols-1">
              {pane === "folders" ? (
                <FolderPane
                  folders={folders}
                  messages={messages}
                  folderId={folderId}
                  onSelect={selectFolder}
                  onCompose={() => openCompose()}
                />
              ) : null}
              {pane === "list" ? (
                <MessageListPane
                  folder={selectedFolder}
                  folders={folders}
                  messages={folderMessages}
                  selectedId={selectedMessage?.id}
                  refreshing={refreshing}
                  compactList={compactList}
                  query={query}
                  onQueryChange={setQuery}
                  onSelect={selectMessage}
                  onReply={replyToMessage}
                  onReplyAll={replyAllToMessage}
                  onForward={forwardMessage}
                  onArchive={archiveMessage}
                  onDelete={setDeleteTarget}
                  onMove={moveMessage}
                  showHeader={width <= 560}
                />
              ) : null}
              {pane === "detail" && selectedMessage ? (
                <MessageDetailPane
                  message={selectedMessage}
                  onReply={() => replyToMessage(selectedMessage)}
                  onReplyAll={() => replyAllToMessage(selectedMessage)}
                  onForward={() => forwardMessage(selectedMessage)}
                  onToggleStar={() => toggleStarMessage(selectedMessage)}
                  onArchive={() => archiveMessage(selectedMessage)}
                  onDelete={() => setDeleteTarget(selectedMessage)}
                />
              ) : null}
              {pane === "compose" ? (
                <ComposePane
                  contacts={contacts}
                  draft={composeDraft}
                  onCancel={() => setPane(selectedMessage ? "detail" : "list")}
                  onSend={(message) => {
                    setMessages((current) => [message, ...current])
                    onSend?.(message)
                    setComposeDraft(undefined)
                    toast.success("Email sent", { description: message.subject })
                    selectFolder("sent")
                  }}
                />
              ) : null}
            </div>
          </div>
        ) : (
          <>
            <MailboxDesktopToolbar
              onCompose={() => openCompose()}
              query={query}
              onQueryChange={setQuery}
              selectedMessage={selectedMessage}
              unreadFirst={unreadFirst}
              compactList={compactList}
              onMarkAllRead={markAllAsRead}
              onRefresh={refreshMailbox}
              onShowArchived={() => selectFolder("archive")}
              onReply={() => selectedMessage && replyToMessage(selectedMessage)}
              onForward={() => selectedMessage && forwardMessage(selectedMessage)}
              onDelete={() => selectedMessage && setDeleteTarget(selectedMessage)}
              onShowStarred={() => selectFolder(STARRED_FOLDER_ID)}
              onToggleUnreadFirst={toggleUnreadFirst}
              onToggleCompactList={toggleCompactList}
            />
            <Sidebar collapsible="none" className="min-h-0 border-r pt-10">
              <FolderPane
                folders={folders}
                messages={messages}
                folderId={folderId}
                onSelect={selectFolder}
                onCompose={() => openCompose()}
                collapsed={sidebarCollapsed}
                onToggleCollapsed={() => setSidebarCollapsed((collapsed) => !collapsed)}
              />
            </Sidebar>
            <SidebarInset className="min-w-0 bg-background pt-10">
              {pane === "compose" ? (
                <ComposePane
                  contacts={contacts}
                  draft={composeDraft}
                  onCancel={() => setPane(selectedMessage ? "detail" : "list")}
                  onSend={(message) => {
                    setMessages((current) => [message, ...current])
                    onSend?.(message)
                    setComposeDraft(undefined)
                    toast.success("Email sent", { description: message.subject })
                    selectFolder("sent")
                  }}
                />
              ) : (
                <ResizablePanelGroup orientation="horizontal" className="min-h-0 min-w-0 flex-1">
                  <ResizablePanel defaultSize="42%" minSize="5rem" maxSize="82%" className="min-w-0 overflow-hidden">
                    <MessageListPane
                      folder={selectedFolder}
                      folders={folders}
                      messages={folderMessages}
                      selectedId={selectedMessage?.id}
                      refreshing={refreshing}
                      compactList={compactList}
                      query={query}
                      onQueryChange={setQuery}
                      onSelect={selectMessage}
                      onReply={replyToMessage}
                      onReplyAll={replyAllToMessage}
                      onForward={forwardMessage}
                      onArchive={archiveMessage}
                      onDelete={setDeleteTarget}
                      onMove={moveMessage}
                      showHeader={false}
                    />
                  </ResizablePanel>
                  <ResizableHandle withHandle />
                  <ResizablePanel defaultSize="58%" minSize="7rem" className="min-w-0 overflow-hidden">
                    {selectedMessage ? (
                      <MessageDetailPane
                        message={selectedMessage}
                        onReply={() => replyToMessage(selectedMessage)}
                        onReplyAll={() => replyAllToMessage(selectedMessage)}
                        onForward={() => forwardMessage(selectedMessage)}
                        onToggleStar={() => toggleStarMessage(selectedMessage)}
                        onArchive={() => archiveMessage(selectedMessage)}
                        onDelete={() => setDeleteTarget(selectedMessage)}
                      />
                    ) : null}
                  </ResizablePanel>
                </ResizablePanelGroup>
              )}
            </SidebarInset>
          </>
        )}
      </SidebarProvider>
      <AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Delete this email?</AlertDialogTitle>
            <AlertDialogDescription>
              This removes “{deleteTarget?.subject}” from the mailbox demo.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
              onClick={() => deleteTarget && deleteMessage(deleteTarget)}
            >
              Delete
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </Card>
  )
}

function MailboxDesktopToolbar({
  query,
  onQueryChange,
  onCompose,
  selectedMessage,
  unreadFirst,
  compactList,
  onMarkAllRead,
  onRefresh,
  onShowArchived,
  onReply,
  onForward,
  onDelete,
  onShowStarred,
  onToggleUnreadFirst,
  onToggleCompactList,
}: {
  query: string
  onQueryChange: (query: string) => void
  onCompose: () => void
  selectedMessage?: MailboxMessage
  unreadFirst: boolean
  compactList: boolean
  onMarkAllRead: () => void
  onRefresh: () => void
  onShowArchived: () => void
  onReply: () => void
  onForward: () => void
  onDelete: () => void
  onShowStarred: () => void
  onToggleUnreadFirst: () => void
  onToggleCompactList: () => void
}) {
  return (
    <div className="absolute inset-x-0 top-0 z-20 flex h-10 items-center gap-2 border-b bg-background/95 px-3 backdrop-blur">
      <Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
        <MenubarMenu>
          <MenubarTrigger>Mailbox</MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onMarkAllRead}>Mark all as read</MenubarItem>
            <MenubarItem onSelect={onRefresh}>
              <RefreshCcwIcon />
              Refresh mailbox
            </MenubarItem>
            <MenubarSeparator />
            <MenubarItem onSelect={onShowArchived}>Show archived</MenubarItem>
          </MenubarContent>
        </MenubarMenu>
        <MenubarMenu>
          <MenubarTrigger>Message</MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onCompose}>
              <Edit3Icon />
              Compose
            </MenubarItem>
            <MenubarItem disabled={!selectedMessage} onSelect={onReply}>
              <ReplyIcon />
              Reply
            </MenubarItem>
            <MenubarItem disabled={!selectedMessage} onSelect={onForward}>
              <ForwardIcon />
              Forward
            </MenubarItem>
            <MenubarSeparator />
            <MenubarItem disabled={!selectedMessage} variant="destructive" onSelect={onDelete}>
              <Trash2Icon />
              Delete
            </MenubarItem>
          </MenubarContent>
        </MenubarMenu>
        <MenubarMenu>
          <MenubarTrigger>View</MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onToggleUnreadFirst}>
              {unreadFirst ? "Default order" : "Unread first"}
            </MenubarItem>
            <MenubarItem onSelect={onShowStarred}>Show starred</MenubarItem>
            <MenubarItem onSelect={onToggleCompactList}>
              {compactList ? "Comfortable list" : "Compact list"}
            </MenubarItem>
          </MenubarContent>
        </MenubarMenu>
      </Menubar>
      <div className="relative ml-auto w-52 shrink-0">
        <SearchIcon className="absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
        <Input
          value={query}
          onChange={(event) => onQueryChange(event.target.value)}
          placeholder="Search mail"
          className="h-8 pl-7"
        />
      </div>
      <Button size="sm" onClick={onCompose}>
        <Edit3Icon className="size-3.5" />
        Compose
      </Button>
    </div>
  )
}

function MailboxToolbar({
  compact,
  pane,
  query,
  onQueryChange,
  showSearch,
  onBack,
  onFolders,
  onCompose,
  iconOnlyCompose = false,
  selectedMessage,
  unreadFirst,
  compactList,
  onMarkAllRead,
  onRefresh,
  onShowArchived,
  onReply,
  onForward,
  onDelete,
  onShowStarred,
  onToggleUnreadFirst,
  onToggleCompactList,
}: {
  compact: boolean
  pane: MailboxPane
  query: string
  onQueryChange: (query: string) => void
  showSearch: boolean
  onBack: () => void
  onFolders: () => void
  onCompose: () => void
  iconOnlyCompose?: boolean
  selectedMessage?: MailboxMessage
  unreadFirst: boolean
  compactList: boolean
  onMarkAllRead: () => void
  onRefresh: () => void
  onShowArchived: () => void
  onReply: () => void
  onForward: () => void
  onDelete: () => void
  onShowStarred: () => void
  onToggleUnreadFirst: () => void
  onToggleCompactList: () => void
}) {
  return (
    <div className="scrollbar-none flex h-10 shrink-0 items-center gap-2 overflow-x-auto border-b px-2">
      {compact && pane !== "folders" ? (
        <Button
          variant="ghost"
          size="icon-sm"
          onClick={onBack}
          aria-label="Back"
          className="shrink-0"
        >
          <ArrowLeftIcon />
        </Button>
      ) : null}
      {compact ? (
        <Button
          variant="outline"
          size={compact ? "icon-sm" : "sm"}
          onClick={onFolders}
          aria-label="Folders"
          className="h-8 shrink-0"
        >
          <FolderIcon className="size-3.5" />
          {!compact ? <span>Folders</span> : null}
        </Button>
      ) : null}
      <Menubar className="h-8 shrink-0 border-0 bg-transparent p-0 shadow-none">
        <MenubarMenu>
          <MenubarTrigger
            aria-label="Mailbox actions"
            className={cn(compact && "h-8 w-8 justify-center px-0")}
          >
            {compact ? (
              <>
                <InboxIcon className="size-3.5" />
                <span className="sr-only">Mailbox</span>
              </>
            ) : (
              "Mailbox"
            )}
          </MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onMarkAllRead}>Mark all as read</MenubarItem>
            <MenubarItem onSelect={onRefresh}>
              <RefreshCcwIcon />
              Refresh mailbox
            </MenubarItem>
            <MenubarSeparator />
            <MenubarItem onSelect={onShowArchived}>Show archived</MenubarItem>
          </MenubarContent>
        </MenubarMenu>
        <MenubarMenu>
          <MenubarTrigger
            aria-label="Message actions"
            className={cn(compact && "h-8 w-8 justify-center px-0")}
          >
            {compact ? (
              <>
                <MailIcon className="size-3.5" />
                <span className="sr-only">Message</span>
              </>
            ) : (
              "Message"
            )}
          </MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onCompose}>
              <Edit3Icon />
              Compose
            </MenubarItem>
            <MenubarItem disabled={!selectedMessage} onSelect={onReply}>
              <ReplyIcon />
              Reply
            </MenubarItem>
            <MenubarItem disabled={!selectedMessage} onSelect={onForward}>
              <ForwardIcon />
              Forward
            </MenubarItem>
            <MenubarSeparator />
            <MenubarItem disabled={!selectedMessage} variant="destructive" onSelect={onDelete}>
              <Trash2Icon />
              Delete
            </MenubarItem>
          </MenubarContent>
        </MenubarMenu>
        <MenubarMenu>
          <MenubarTrigger
            aria-label="View options"
            className={cn(compact && "h-8 w-8 justify-center px-0")}
          >
            {compact ? (
              <>
                <PanelLeftIcon className="size-3.5" />
                <span className="sr-only">View</span>
              </>
            ) : (
              "View"
            )}
          </MenubarTrigger>
          <MenubarContent>
            <MenubarItem onSelect={onToggleUnreadFirst}>
              {unreadFirst ? "Default order" : "Unread first"}
            </MenubarItem>
            <MenubarItem onSelect={onShowStarred}>Show starred</MenubarItem>
            <MenubarItem onSelect={onToggleCompactList}>
              {compactList ? "Comfortable list" : "Compact list"}
            </MenubarItem>
          </MenubarContent>
        </MenubarMenu>
      </Menubar>
      {showSearch ? (
        <div className="relative ml-auto w-44 shrink-0">
          <SearchIcon className="absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
          <Input
            value={query}
            onChange={(event) => onQueryChange(event.target.value)}
            placeholder="Search mail"
            className="h-8 pl-7"
          />
        </div>
      ) : <span className="min-w-2 flex-1" />}
      <Button
        size="icon-sm"
        onClick={onCompose}
        aria-label="Compose"
        className={cn(
          "h-8 shrink-0",
          compact || iconOnlyCompose ? "w-8" : "w-auto px-3"
        )}
      >
        <Edit3Icon className="size-3.5" />
        <span className={compact || iconOnlyCompose ? "sr-only" : undefined}>Compose</span>
      </Button>
    </div>
  )
}

function FolderPane({
  folders,
  messages,
  folderId,
  onSelect,
  onCompose,
  collapsed = false,
  onToggleCollapsed,
}: {
  folders: MailboxFolder[]
  messages: MailboxMessage[]
  folderId: string
  onSelect: (folderId: string) => void
  onCompose: () => void
  collapsed?: boolean
  onToggleCollapsed?: () => void
}) {
  return (
    <>
      <SidebarHeader className="border-b p-3">
        <SidebarMenu>
          <SidebarMenuItem>
            <div className={cn("flex items-center gap-3", collapsed && "justify-center")}>
              {!collapsed ? (
                <SidebarMenuButton size="lg" className="min-w-0 flex-1 gap-3">
                  <span className="flex size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                    <MailIcon className="size-4" />
                  </span>
                  <span className="grid min-w-0 flex-1 text-left text-sm leading-tight">
                    <span className="truncate font-semibold">Acme Mail</span>
                    <span className="truncate text-xs text-muted-foreground">Shared inbox</span>
                  </span>
                </SidebarMenuButton>
              ) : null}
              {onToggleCollapsed ? (
                <Button
                  type="button"
                  variant="ghost"
                  size="icon-sm"
                  className="shrink-0"
                  onClick={onToggleCollapsed}
                  aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
                >
                  <PanelLeftIcon className="size-4" />
                </Button>
              ) : null}
            </div>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarHeader>
      <SidebarContent className="min-h-0 flex-1 overflow-hidden">
        <ScrollBlur className="h-full min-h-0" edgeSize={24}>
          <SidebarGroup>
            {!collapsed ? <SidebarGroupLabel>Mailboxes</SidebarGroupLabel> : null}
            <SidebarGroupContent>
              <SidebarMenu>
                {folders.map((folder) => {
                  const Icon = folder.icon ?? MailIcon
                  const count = messages.filter((message) => message.folderId === folder.id).length
                  return (
                    <SidebarMenuItem key={folder.id}>
                      <SidebarMenuButton
                        isActive={folderId === folder.id}
                        onClick={() => onSelect(folder.id)}
                        tooltip={collapsed ? folder.label : undefined}
                        className={cn(collapsed && "justify-center px-0")}
                      >
                        <Icon />
                        {!collapsed ? <span>{folder.label}</span> : null}
                        {count && !collapsed ? (
                          <Badge
                            variant="secondary"
                            className="ml-auto h-5 rounded-full px-1.5 text-[10px]"
                          >
                            {count}
                          </Badge>
                        ) : null}
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  )
                })}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarGroup>
            {!collapsed ? <SidebarGroupLabel>Actions</SidebarGroupLabel> : null}
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton
                    onClick={onCompose}
                    tooltip={collapsed ? "Compose" : undefined}
                    className={cn(collapsed && "justify-center px-0")}
                  >
                    <Edit3Icon />
                    {!collapsed ? <span>Compose</span> : null}
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton
                    isActive={folderId === STARRED_FOLDER_ID}
                    onClick={() => onSelect(STARRED_FOLDER_ID)}
                    tooltip={collapsed ? "Starred" : undefined}
                    className={cn(collapsed && "justify-center px-0")}
                  >
                    <StarIcon />
                    {!collapsed ? <span>Starred</span> : null}
                    {messages.filter((message) => message.starred).length && !collapsed ? (
                      <Badge
                        variant="secondary"
                        className="ml-auto h-5 rounded-full px-1.5 text-[10px]"
                      >
                        {messages.filter((message) => message.starred).length}
                      </Badge>
                    ) : null}
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </ScrollBlur>
      </SidebarContent>
      {!collapsed ? (
        <SidebarFooter className="border-t">
        <SidebarMenu>
          <SidebarMenuItem>
            <SidebarMenuButton size="lg" className="gap-3">
              <Avatar className="size-8 rounded-lg after:hidden">
                <AvatarImage className="rounded-lg" alt="" />
                <AvatarFallback className="rounded-lg">LC</AvatarFallback>
              </Avatar>
              <span className="grid min-w-0 flex-1 text-left text-sm leading-tight">
                <span className="truncate font-medium">Liam Corrigan</span>
                <span className="truncate text-xs text-muted-foreground">liam@corr.sh</span>
              </span>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
        </SidebarFooter>
      ) : null}
    </>
  )
}

function MessageListPane({
  folder,
  folders,
  messages,
  selectedId,
  refreshing = false,
  compactList = false,
  query,
  onQueryChange,
  onSelect,
  onReply,
  onReplyAll,
  onForward,
  onArchive,
  onDelete,
  onMove,
  showHeader = true,
}: {
  folder: MailboxFolder
  folders: MailboxFolder[]
  messages: MailboxMessage[]
  selectedId?: string
  refreshing?: boolean
  compactList?: boolean
  query: string
  onQueryChange: (query: string) => void
  onSelect: (message: MailboxMessage) => void
  onReply: (message: MailboxMessage) => void
  onReplyAll: (message: MailboxMessage) => void
  onForward: (message: MailboxMessage) => void
  onArchive: (message: MailboxMessage) => void
  onDelete: (message: MailboxMessage) => void
  onMove: (message: MailboxMessage, folderId: string) => void
  showHeader?: boolean
}) {
  return (
    <section className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
      {showHeader ? (
        <div className="h-14 min-w-0 overflow-hidden border-b p-3">
          <div className="relative">
            <SearchIcon className="absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
            <Input
              value={query}
              onChange={(event) => onQueryChange(event.target.value)}
              placeholder={`Search ${folder.label.toLowerCase()}`}
              className="h-8 pl-7"
            />
          </div>
        </div>
      ) : null}
      <ScrollBlur className="min-h-0 flex-1" edgeSize={28}>
        <div className="space-y-1 p-2">
          {refreshing ? (
            <MessageListSkeleton />
          ) : (
            messages.map((message) => (
              <ContextMenu key={message.id}>
                <ContextMenuTrigger asChild>
                  <button
                    type="button"
                    onClick={() => onSelect(message)}
                    className={cn(
                      "w-full rounded-md border text-left transition-colors hover:bg-muted/40",
                      compactList ? "p-2" : "p-3",
                      selectedId === message.id && "border-foreground/20 bg-muted/30"
                    )}
                  >
                    <MessageListItem message={message} compact={compactList} />
                  </button>
                </ContextMenuTrigger>
                <MessageContextMenu
                  message={message}
                  folders={folders}
                  currentFolderId={folder.id}
                  onReply={onReply}
                  onReplyAll={onReplyAll}
                  onForward={onForward}
                  onArchive={onArchive}
                  onDelete={onDelete}
                  onMove={onMove}
                />
              </ContextMenu>
            ))
          )}
        </div>
      </ScrollBlur>
      <div className="flex h-8 shrink-0 items-center border-t px-3 text-xs text-muted-foreground">
        {refreshing ? "Refreshing..." : `${messages.length} message${messages.length === 1 ? "" : "s"}`}
      </div>
    </section>
  )
}

function MessageListItem({ message, compact = false }: { message: MailboxMessage; compact?: boolean }) {
  return (
    <div className="flex min-w-0 items-start gap-2">
      <Avatar className={cn("shrink-0 rounded-md after:hidden", compact ? "size-7" : "size-8")}>
        <AvatarImage src={message.from.avatar} alt="" className="rounded-md" />
        <AvatarFallback className="rounded-md">
          {initials(message.from.name)}
        </AvatarFallback>
      </Avatar>
      <span className="min-w-0 flex-1 overflow-hidden">
        <span className="flex items-center gap-2">
          <span className={cn("truncate text-sm", message.unread && "font-semibold")}>
            {message.from.name}
          </span>
          <span className="ml-auto shrink-0 text-[11px] text-muted-foreground">
            {message.timestamp}
          </span>
        </span>
        <span className={cn("block truncate text-xs font-medium", compact ? "mt-0.5" : "mt-1")}>
          {message.subject}
        </span>
        {!compact ? (
          <span className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
            {message.preview}
          </span>
        ) : null}
      </span>
    </div>
  )
}

function MessageListSkeleton() {
  return (
    <>
      {Array.from({ length: 4 }).map((_, index) => (
        <div key={index} className="flex gap-2 rounded-md border p-3">
          <div className="size-8 shrink-0 animate-pulse rounded-md bg-muted" />
          <div className="min-w-0 flex-1 space-y-2">
            <div className="h-3 w-2/5 animate-pulse rounded bg-muted" />
            <div className="h-3 w-3/5 animate-pulse rounded bg-muted" />
            <div className="h-3 w-4/5 animate-pulse rounded bg-muted" />
          </div>
        </div>
      ))}
    </>
  )
}

function MessageContextMenu({
  message,
  folders,
  currentFolderId,
  onReply,
  onReplyAll,
  onForward,
  onArchive,
  onDelete,
  onMove,
}: {
  message: MailboxMessage
  folders: MailboxFolder[]
  currentFolderId: string
  onReply: (message: MailboxMessage) => void
  onReplyAll: (message: MailboxMessage) => void
  onForward: (message: MailboxMessage) => void
  onArchive: (message: MailboxMessage) => void
  onDelete: (message: MailboxMessage) => void
  onMove: (message: MailboxMessage, folderId: string) => void
}) {
  return (
    <ContextMenuContent className="w-52">
      <ContextMenuItem onSelect={() => onReply(message)}>
        <ReplyIcon />
        Reply
      </ContextMenuItem>
      <ContextMenuItem onSelect={() => onReplyAll(message)}>
        <ReplyIcon />
        Reply all
      </ContextMenuItem>
      <ContextMenuItem onSelect={() => onForward(message)}>
        <ForwardIcon />
        Forward
      </ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuSub>
        <ContextMenuSubTrigger>
          <FolderIcon />
          Move to folder
        </ContextMenuSubTrigger>
        <ContextMenuSubContent className="w-40">
          {folders.map((folder) => {
            const Icon = folder.icon ?? FolderIcon
            return (
              <ContextMenuItem
                key={folder.id}
                disabled={folder.id === currentFolderId}
                onSelect={() => onMove(message, folder.id)}
              >
                <Icon />
                {folder.label}
              </ContextMenuItem>
            )
          })}
        </ContextMenuSubContent>
      </ContextMenuSub>
      <ContextMenuItem onSelect={() => onArchive(message)}>
        <ArchiveIcon />
        Archive
      </ContextMenuItem>
      <ContextMenuSeparator />
      <ContextMenuItem variant="destructive" onSelect={() => onDelete(message)}>
        <Trash2Icon />
        Delete
      </ContextMenuItem>
    </ContextMenuContent>
  )
}

function MessageDetailPane({
  message,
  onReply,
  onReplyAll,
  onForward,
  onToggleStar,
  onArchive,
  onDelete,
}: {
  message: MailboxMessage
  onReply: () => void
  onReplyAll: () => void
  onForward: () => void
  onToggleStar: () => void
  onArchive: () => void
  onDelete: () => void
}) {
  return (
    <section className="min-h-0 min-w-0 flex-1 overflow-hidden">
      <ScrollBlur className="h-full min-h-0" edgeSize={32}>
        <article className="min-w-0 space-y-4 p-4">
          <div className="flex items-start gap-3">
            <Avatar className="size-10 rounded-md after:hidden">
              <AvatarImage src={message.from.avatar} alt="" className="rounded-md" />
              <AvatarFallback className="rounded-md">
                {initials(message.from.name)}
              </AvatarFallback>
            </Avatar>
            <div className="min-w-0 flex-1">
              <div className="flex items-start justify-between gap-3">
                <div className="min-w-0">
                  <h2 className="break-words text-lg font-semibold tracking-tight">
                    {message.subject}
                  </h2>
                  <p className="break-words text-sm text-muted-foreground">
                    {message.from.name} · {message.from.email}
                  </p>
                </div>
                <DropdownMenu>
                  <DropdownMenuTrigger asChild>
                    <Button variant="ghost" size="icon-sm">
                      <MoreHorizontalIcon />
                    </Button>
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="end">
                    <DropdownMenuItem onClick={onArchive}>
                      <ArchiveIcon />
                      Archive
                    </DropdownMenuItem>
                    <DropdownMenuItem variant="destructive" onClick={onDelete}>
                      <Trash2Icon />
                      Delete
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              </div>
            </div>
          </div>
          <Separator />
          <p className="break-words text-sm leading-7 text-muted-foreground">{message.body}</p>
          {message.thread?.length ? (
            <div className="space-y-2 border-t pt-4">
              <div className="text-xs font-medium text-muted-foreground">
                {message.thread.length} earlier repl{message.thread.length === 1 ? "y" : "ies"}
              </div>
              {message.thread.map((reply) => (
                <ThreadReplyCard key={reply.id} reply={reply} />
              ))}
            </div>
          ) : null}
          <div className="flex flex-wrap items-center gap-2">
            <Button size="sm" onClick={onReply}>
              <ReplyIcon className="size-3.5" />
              Reply
            </Button>
            <Button size="sm" variant="outline" onClick={onReplyAll}>
              <ReplyIcon className="size-3.5" />
              Reply all
            </Button>
            <Button size="sm" variant="outline" onClick={onForward}>
              <ForwardIcon className="size-3.5" />
              Forward
            </Button>
            <Button
              size="icon-sm"
              variant={message.starred ? "default" : "outline"}
              aria-label={message.starred ? "Remove star" : "Star"}
              aria-pressed={message.starred}
              onClick={onToggleStar}
            >
              <StarIcon className={cn(message.starred && "fill-current")} />
            </Button>
          </div>
        </article>
      </ScrollBlur>
    </section>
  )
}

function ThreadReplyCard({ reply }: { reply: MailboxThreadReply }) {
  return (
    <section className="rounded-lg border bg-muted/20 p-3">
      <div className="flex items-start gap-3">
        <Avatar className="size-8 rounded-md after:hidden">
          <AvatarImage src={reply.from.avatar} alt="" className="rounded-md" />
          <AvatarFallback className="rounded-md">
            {initials(reply.from.name)}
          </AvatarFallback>
        </Avatar>
        <div className="min-w-0 flex-1">
          <div className="flex items-start justify-between gap-3">
            <div className="min-w-0">
              <div className="truncate text-sm font-medium">{reply.from.name}</div>
              <div className="truncate text-xs text-muted-foreground">
                to {formatReplyRecipients(reply.to)}
                {reply.cc?.length ? ` · cc ${formatReplyRecipients(reply.cc)}` : ""}
              </div>
            </div>
            <span className="shrink-0 text-xs text-muted-foreground">{reply.timestamp}</span>
          </div>
          <p className="mt-3 text-sm leading-6 text-muted-foreground">{reply.body}</p>
        </div>
      </div>
    </section>
  )
}

function ComposePane({
  contacts,
  draft,
  onCancel,
  onSend,
}: {
  contacts: MailboxContact[]
  draft?: MailboxComposeDraft
  onCancel: () => void
  onSend: (message: MailboxMessage) => void
}) {
  const [recipients, setRecipients] = React.useState<string[]>(draft?.to ?? [])
  const [cc, setCc] = React.useState<string[]>(draft?.cc ?? [])
  const [bcc, setBcc] = React.useState<string[]>(draft?.bcc ?? [])
  const [subject, setSubject] = React.useState(draft?.subject ?? "")
  const [body, setBody] = React.useState(draft?.body ?? "")
  const [attachments, setAttachments] = React.useState<FileUploadDropzoneFile[]>([])
  const [attachmentDialogOpen, setAttachmentDialogOpen] = React.useState(false)
  const editorRef = React.useRef<HTMLDivElement | null>(null)

  React.useEffect(() => {
    const nextBody = draft?.body ?? ""

    setRecipients(draft?.to ?? [])
    setCc(draft?.cc ?? [])
    setBcc(draft?.bcc ?? [])
    setSubject(draft?.subject ?? "")
    setBody(nextBody)
    setAttachments([])
    setAttachmentDialogOpen(false)

    window.requestAnimationFrame(() => {
      if (editorRef.current) {
        editorRef.current.innerHTML = formatComposerBody(nextBody)
      }
    })
  }, [draft])

  React.useEffect(() => {
    if (!attachmentDialogOpen) return
    if (!attachments.length) return
    if (attachments.some((item) => item.status === "uploading")) return
    if (!attachments.every((item) => item.status === "success")) return

    const timer = window.setTimeout(() => {
      setAttachmentDialogOpen(false)
      toast.success("Attachments ready", {
        description: `${attachments.length} file${attachments.length === 1 ? "" : "s"} attached.`,
      })
    }, 250)

    return () => window.clearTimeout(timer)
  }, [attachmentDialogOpen, attachments])

  function syncBodyFromEditor() {
    setBody(editorRef.current?.innerText ?? "")
  }

  function getComposerBody() {
    return editorRef.current?.innerText.trim() ?? body.trim()
  }

  function applyFormat(command: "bold" | "italic" | "underline") {
    editorRef.current?.focus()
    document.execCommand(command)
    syncBodyFromEditor()
  }

  return (
    <section className="min-h-0 min-w-0 flex-1 overflow-hidden bg-background">
      <div className="flex h-full min-h-0 flex-col">
        <ScrollBlur className="min-h-0 flex-1" edgeSize={32}>
          <div className="space-y-3 border-b p-4">
          <div className="flex items-center justify-between gap-3">
            <div>
              <h2 className="text-sm font-semibold">Compose</h2>
              <p className="text-xs text-muted-foreground">Draft a new email</p>
            </div>
            <div className="flex items-center gap-2">
              <Button variant="outline" size="sm" onClick={onCancel}>
                Cancel
              </Button>
              <Button
                size="sm"
                onClick={() => {
                  const nextBody = getComposerBody()

                  onSend({
                    id: `sent-${Date.now()}`,
                    folderId: "sent",
                    from: contacts[0],
                    to: contacts.filter((contact) => recipients.includes(contact.email)),
                    cc: contacts.filter((contact) => cc.includes(contact.email)),
                    bcc: contacts.filter((contact) => bcc.includes(contact.email)),
                    subject,
                    preview: nextBody,
                    body: nextBody,
                    timestamp: "Now",
                  })
                }}
              >
                <SendIcon className="size-3.5" />
                Send
              </Button>
            </div>
          </div>
          <div className="grid gap-2">
            <RecipientCombobox
              label="To"
              contacts={contacts}
              value={recipients}
              onValueChange={setRecipients}
            />
            <RecipientCombobox
              label="Cc"
              contacts={contacts}
              value={cc}
              onValueChange={setCc}
            />
            <RecipientCombobox
              label="Bcc"
              contacts={contacts}
              value={bcc}
              onValueChange={setBcc}
            />
          </div>
          <Input value={subject} onChange={(event) => setSubject(event.target.value)} placeholder="Subject" />
          <div className="flex items-center justify-between gap-2">
            <ToggleGroup type="multiple" size="sm" variant="outline">
              <ToggleGroupItem
                value="bold"
                aria-label="Bold"
                onMouseDown={(event) => event.preventDefault()}
                onClick={() => applyFormat("bold")}
              >
                <BoldIcon className="size-3.5" />
              </ToggleGroupItem>
              <ToggleGroupItem
                value="italic"
                aria-label="Italic"
                onMouseDown={(event) => event.preventDefault()}
                onClick={() => applyFormat("italic")}
              >
                <ItalicIcon className="size-3.5" />
              </ToggleGroupItem>
              <ToggleGroupItem
                value="underline"
                aria-label="Underline"
                onMouseDown={(event) => event.preventDefault()}
                onClick={() => applyFormat("underline")}
              >
                <UnderlineIcon className="size-3.5" />
              </ToggleGroupItem>
            </ToggleGroup>
            <Dialog open={attachmentDialogOpen} onOpenChange={setAttachmentDialogOpen}>
              <DialogTrigger asChild>
                <Button variant="ghost" size="icon-sm" aria-label="Attach files">
                  <PaperclipIcon />
                  {attachments.length ? (
                    <span className="absolute -right-1 -top-1 flex size-4 items-center justify-center rounded-full bg-foreground text-[10px] font-medium text-background">
                      {attachments.length}
                    </span>
                  ) : null}
                </Button>
              </DialogTrigger>
              <DialogContent className="sm:max-w-lg">
                <DialogHeader>
                  <DialogTitle>Attach files</DialogTitle>
                  <DialogDescription>
                    Add supporting files before sending this email.
                  </DialogDescription>
                </DialogHeader>
                <FileUploadDropzone
                  files={attachments}
                  onFilesChange={setAttachments}
                  maxFiles={4}
                  label="Drop email attachments here"
                  description="or click to browse files"
                  simulateUpload
                />
                <DialogFooter showCloseButton>
                  <Button
                    type="button"
                    onClick={() =>
                      toast.success("Attachments ready", {
                        description: `${attachments.length} file${attachments.length === 1 ? "" : "s"} attached.`,
                      })
                    }
                    disabled={!attachments.length}
                  >
                    Done
                  </Button>
                </DialogFooter>
              </DialogContent>
            </Dialog>
          </div>
          </div>
          {attachments.length ? (
            <div className="flex flex-wrap gap-2 border-b px-4 py-2">
              {attachments.map((item) => (
                <Badge key={item.id} variant="secondary" className="rounded-md pr-1">
                  <PaperclipIcon className="size-3" />
                  {item.file.name}
                  <button
                    type="button"
                    onClick={() =>
                      setAttachments((current) =>
                        current.filter((attachment) => attachment.id !== item.id)
                      )
                    }
                    className="ml-1 inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
                    aria-label={`Remove ${item.file.name}`}
                  >
                    <XIcon className="size-3" />
                  </button>
                </Badge>
              ))}
            </div>
          ) : null}
          <div
            ref={editorRef}
            contentEditable
            suppressContentEditableWarning
            role="textbox"
            aria-multiline="true"
            data-placeholder="Write a message..."
            onInput={syncBodyFromEditor}
            className="min-h-80 bg-background p-4 text-sm leading-7 outline-none empty:before:text-muted-foreground empty:before:content-[attr(data-placeholder)] [&_hr]:my-4 [&_hr]:border-border"
          />
        </ScrollBlur>
      </div>
    </section>
  )
}

function RecipientCombobox({
  contacts,
  label,
  value,
  onValueChange,
}: {
  contacts: MailboxContact[]
  label: string
  value: string[]
  onValueChange: (value: string[]) => void
}) {
  const anchor = useComboboxAnchor()
  const [open, setOpen] = React.useState(false)
  const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null)
  const items = contacts.map((contact) => contact.email)

  React.useEffect(() => {
    setPortalContainer(document.body)
  }, [])

  function openCombobox() {
    setOpen(true)
  }

  return (
    <div className="grid grid-cols-[2.25rem_minmax(0,1fr)] items-start gap-2">
      <div className="pt-1.5 text-xs font-medium text-muted-foreground">{label}</div>
      <Combobox
        multiple
        open={open}
        onOpenChange={setOpen}
        autoHighlight
        openOnInputClick
        items={items}
        value={value}
        onValueChange={(nextValue) => {
          onValueChange(nextValue as string[])
          window.requestAnimationFrame(() => setOpen(true))
        }}
      >
        <ComboboxChips
          ref={anchor}
          className="relative min-h-8 w-full pr-8"
          onFocusCapture={openCombobox}
          onPointerDownCapture={openCombobox}
        >
          <ComboboxValue>
            {(values) => (
              <>
                {(values as string[]).map((item) => (
                  <ComboboxChip key={item}>{item}</ComboboxChip>
                ))}
                <ComboboxChipsInput
                  placeholder={`Search ${label.toLowerCase()} contacts`}
                  className="min-w-32"
                  onFocus={openCombobox}
                  onClick={openCombobox}
                />
              </>
            )}
          </ComboboxValue>
          <ComboboxTrigger
            className="absolute right-2 top-1.5 inline-flex size-5 items-center justify-center rounded-sm hover:bg-muted"
            onClick={openCombobox}
          />
        </ComboboxChips>
        <ComboboxContent
          anchor={anchor}
          container={portalContainer ?? undefined}
          sideOffset={4}
          className="z-[90]"
        >
          <ComboboxEmpty>No contacts found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                <span className="grid min-w-0">
                  <span className="truncate font-medium">
                    {contacts.find((contact) => contact.email === item)?.name ?? item}
                  </span>
                  <span className="truncate text-muted-foreground">{item}</span>
                </span>
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}

function initials(name: string) {
  return name
    .split(" ")
    .map((part) => part[0])
    .join("")
    .slice(0, 2)
    .toUpperCase()
}

function withSubjectPrefix(subject: string, prefix: "Re:" | "Fwd:") {
  return subject.toLowerCase().startsWith(prefix.toLowerCase())
    ? subject
    : `${prefix} ${subject}`
}

function uniqueEmails(emails: string[]) {
  return Array.from(new Set(emails.filter(Boolean)))
}

function formatReplyRecipients(recipients: MailboxContact[] = []) {
  if (!recipients.length) return "me"

  return recipients.map((contact) => contact.name).join(", ")
}

function buildThreadQuote(message: MailboxMessage, mode: "reply" | "forward") {
  const replies = message.thread ?? []
  const quotedMessages = [
    ...replies.map((reply) => ({
      from: reply.from,
      to: reply.to,
      cc: reply.cc,
      timestamp: reply.timestamp,
      subject: message.subject,
      body: reply.body,
    })),
    {
      from: message.from,
      to: message.to,
      cc: message.cc,
      timestamp: message.timestamp,
      subject: message.subject,
      body: message.body,
    },
  ]

  return quotedMessages
    .map((item, index) => {
      const header =
        mode === "forward"
          ? [
              "Forwarded message:",
              `From: ${item.from.name} <${item.from.email}>`,
              item.to?.length ? `To: ${item.to.map((contact) => contact.email).join(", ")}` : null,
              item.cc?.length ? `Cc: ${item.cc.map((contact) => contact.email).join(", ")}` : null,
              `Subject: ${item.subject}`,
              `Date: ${item.timestamp}`,
            ]
              .filter(Boolean)
              .join("\n")
          : `${formatQuoteTimestamp(item.timestamp)}, ${item.from.name} wrote:`

      return `${index === 0 ? "\n\n" : "\n"}---\n${header}\n${item.body}`
    })
    .join("\n")
}

function formatQuoteTimestamp(timestamp: string) {
  if (/^\d{1,2}:\d{2}$/.test(timestamp)) {
    return `Today @ ${timestamp}`
  }

  if (timestamp.toLowerCase() === "yesterday") {
    return "Yesterday"
  }

  if (timestamp.toLowerCase() === "now") {
    return "Just now"
  }

  if (timestamp.toLowerCase() === "draft") {
    return "Draft"
  }

  return `On ${timestamp}`
}

function formatComposerBody(body: string) {
  return body
    .split("\n")
    .map((line) => {
      if (line.trim() === "---") {
        return '<hr aria-hidden="true" />'
      }

      if (!line) {
        return "<div><br /></div>"
      }

      return `<div>${escapeHtml(line)}</div>`
    })
    .join("")
}

function escapeHtml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;")
}
```
