{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "inbox",
  "title": "Inbox",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/inbox/inbox.component.ts",
      "content": "// Three-pane mail surface. The sample `messages` below are a stub -- pass your own list through the\n// `messages` input (or edit `defaultMails`) and wire archive / send to your mail API.\nimport { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  AlertOctagon,\n  Archive,\n  ArrowLeft,\n  ArrowRight,\n  AtSign,\n  Bookmark,\n  FileText,\n  Forward,\n  Inbox as InboxIcon,\n  LucideAngularModule,\n  MoreVertical,\n  Paperclip,\n  Pencil,\n  Reply,\n  ReplyAll,\n  Search,\n  Send,\n  Smile,\n  Star,\n  Tag,\n  Trash2,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\n\nexport type ReplyMode = 'reply' | 'reply-all' | 'forward'\n\nexport type FolderId = 'inbox' | 'starred' | 'sent' | 'drafts' | 'spam' | 'trash'\n\ninterface Folder {\n  id: FolderId\n  label: string\n  icon: LucideIconData\n}\n\nexport interface LabelTag {\n  id: string\n  label: string\n  color: string\n}\n\nexport interface Mail {\n  id: string\n  folder: FolderId\n  from: { name: string; email: string; initials: string }\n  to: string\n  subject: string\n  preview: string\n  body: string\n  receivedAt: Date\n  read: boolean\n  starred: boolean\n  hasAttachment: boolean\n  labels: string[]\n}\n\nconst folders: Folder[] = [\n  { id: 'inbox', label: 'Inbox', icon: InboxIcon },\n  { id: 'starred', label: 'Starred', icon: Star },\n  { id: 'sent', label: 'Sent', icon: Send },\n  { id: 'drafts', label: 'Drafts', icon: FileText },\n  { id: 'spam', label: 'Spam', icon: AlertOctagon },\n  { id: 'trash', label: 'Trash', icon: Trash2 },\n]\n\nconst labels: LabelTag[] = [\n  { id: 'work', label: 'Work', color: 'bg-primary' },\n  { id: 'personal', label: 'Personal', color: 'bg-success' },\n  { id: 'finance', label: 'Finance', color: 'bg-info' },\n  { id: 'travel', label: 'Travel', color: 'bg-warning' },\n]\n\nconst now = new Date()\nconst todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())\nconst yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)\n\nconst defaultMails: Mail[] = [\n  {\n    id: 'e1',\n    folder: 'inbox',\n    from: { name: 'Sarah Connor', email: 'sarah@acme.com', initials: 'SC' },\n    to: 'you@acme.com',\n    subject: 'Q2 OKR draft — your sign-off needed',\n    preview:\n      'Pulled together the Q2 OKR draft from the leadership offsite. I have flagged the two areas where we need…',\n    body: `Hi,\\n\\nPulled together the Q2 OKR draft from the leadership offsite. I have flagged the two areas where we need your call before we lock it:\\n\\n• Hiring plan: 4 vs 6 ENG roles (page 3)\\n• EU rollout timing: April vs June (page 7)\\n\\nGoing to share with the rest of the leadership team Wednesday EOD, so anything from you by Tuesday is great.\\n\\nThanks,\\nSarah`,\n    receivedAt: new Date(now.getTime() - 18 * 60 * 1000),\n    read: false,\n    starred: true,\n    hasAttachment: true,\n    labels: ['work'],\n  },\n  {\n    id: 'e2',\n    folder: 'inbox',\n    from: { name: 'Marcus Rivera', email: 'marcus@acme.com', initials: 'MR' },\n    to: 'you@acme.com',\n    subject: 'Re: Dashboard review tomorrow',\n    preview:\n      'Yes! Pushing to 10:30 so we can join from the standup. I will own the slides and pull the latest kanban metrics.',\n    body: `Yes! Pushing to 10:30 so we can join from the standup. I will own the slides and pull the latest kanban metrics.\\n\\nFew things I want to cover:\\n  - Throughput delta vs last sprint\\n  - The two stories we de-scoped\\n  - Q2 capacity ask\\n\\nSee you tomorrow,\\nMarcus`,\n    receivedAt: new Date(now.getTime() - 95 * 60 * 1000),\n    read: false,\n    starred: false,\n    hasAttachment: false,\n    labels: ['work'],\n  },\n  {\n    id: 'e3',\n    folder: 'inbox',\n    from: { name: 'Stripe', email: 'receipts@stripe.com', initials: 'St' },\n    to: 'you@acme.com',\n    subject: 'Your invoice from Acme Inc. — $1,290.00',\n    preview: 'Thanks for using Stripe. Your invoice for the period Mar 1 – Mar 31 is attached. Total due: $1,290.00…',\n    body: 'Thanks for using Stripe. Your invoice for the period Mar 1 – Mar 31 is attached. Total due: $1,290.00.',\n    receivedAt: new Date(now.getTime() - 4 * 60 * 60 * 1000),\n    read: true,\n    starred: false,\n    hasAttachment: true,\n    labels: ['finance'],\n  },\n  {\n    id: 'e4',\n    folder: 'inbox',\n    from: { name: 'Linear', email: 'no-reply@linear.app', initials: 'Li' },\n    to: 'you@acme.com',\n    subject: 'Weekly summary: 14 issues closed, 22 opened',\n    preview:\n      'Good news — the team closed 14 issues this week, up 3 from last week. Top contributors: Marcus, Priya, Devon.',\n    body: 'Good news — the team closed 14 issues this week, up 3 from last week. Top contributors: Marcus, Priya, Devon.',\n    receivedAt: new Date(now.getTime() - 9 * 60 * 60 * 1000),\n    read: true,\n    starred: false,\n    hasAttachment: false,\n    labels: ['work'],\n  },\n  {\n    id: 'e5',\n    folder: 'inbox',\n    from: { name: 'Priya Shah', email: 'priya@acme.com', initials: 'PS' },\n    to: 'you@acme.com',\n    subject: 'Color tokens are live in staging',\n    preview:\n      'Pushed the new OKLCH token set to staging. All chart colors, success/warning/info, sidebar variants. Take a look…',\n    body: 'Pushed the new OKLCH token set to staging. All chart colors, success/warning/info, sidebar variants. Take a look when you have a sec.',\n    receivedAt: yesterday,\n    read: true,\n    starred: true,\n    hasAttachment: false,\n    labels: ['work'],\n  },\n  {\n    id: 'e6',\n    folder: 'inbox',\n    from: { name: 'United Airlines', email: 'reservations@united.com', initials: 'UA' },\n    to: 'you@acme.com',\n    subject: 'Your itinerary: SFO → LHR, departing Apr 12',\n    preview: 'Your reservation is confirmed. Check-in opens 24h before departure. Booking reference: 7HQ4P2.',\n    body: 'Your reservation is confirmed. Check-in opens 24h before departure. Booking reference: 7HQ4P2.',\n    receivedAt: new Date(now.getTime() - 38 * 60 * 60 * 1000),\n    read: true,\n    starred: false,\n    hasAttachment: true,\n    labels: ['travel'],\n  },\n  {\n    id: 'e7',\n    folder: 'inbox',\n    from: { name: 'Mom', email: 'mom@family.net', initials: 'M' },\n    to: 'you@acme.com',\n    subject: 'Sunday lunch?',\n    preview: 'Are you free for lunch this Sunday? Dad is making his lasagne. Let me know!',\n    body: 'Are you free for lunch this Sunday? Dad is making his lasagne. Let me know!',\n    receivedAt: new Date(now.getTime() - 55 * 60 * 60 * 1000),\n    read: true,\n    starred: false,\n    hasAttachment: false,\n    labels: ['personal'],\n  },\n]\n\nfunction byNewest(a: Mail, b: Mail): number {\n  return b.receivedAt.getTime() - a.receivedAt.getTime()\n}\n\nfunction formatMailTime(d: Date): string {\n  if (d >= todayStart) return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })\n  if (d.toDateString() === yesterday.toDateString()) return 'Yesterday'\n  const diffDays = Math.floor((now.getTime() - d.getTime()) / (24 * 60 * 60 * 1000))\n  if (diffDays < 7) return d.toLocaleDateString(undefined, { weekday: 'short' })\n  return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })\n}\n\nfunction formatFullTime(d: Date): string {\n  return d.toLocaleString(undefined, {\n    weekday: 'short',\n    month: 'short',\n    day: 'numeric',\n    hour: 'numeric',\n    minute: '2-digit',\n  })\n}\n\nfunction labelMeta(id: string): LabelTag | undefined {\n  return labels.find((l) => l.id === id)\n}\n\nconst replyTabBase = 'inline-flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium transition-colors'\nconst replyTabActive = 'bg-background text-foreground border-input/60 border shadow-sm'\nconst replyTabIdle = 'text-muted-foreground hover:bg-background/60 hover:text-foreground'\nconst railRow =\n  'flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors'\nconst railActive = 'bg-primary/10 text-primary font-medium'\nconst railIdle = 'hover:bg-muted/60 text-foreground/80'\n\n/**\n * Inbox -- the React block, 1:1. Left rail: Compose, folders with unread counts and colour-coded\n * labels with totals. Middle: searchable message list. Right: reader pane with a reply composer\n * (Cmd/Ctrl+Enter sends). State is local; `messages` seeds it, like React's `useState(messages)`.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-inbox, [ui-inbox]',\n  standalone: true,\n  imports: [LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiButtonComponent, UiInputComponent],\n  template: `\n    <div\n      data-slot=\"inbox\"\n      class=\"bg-card text-card-foreground grid h-[680px] w-full grid-cols-[220px_340px_1fr] overflow-hidden rounded-xl border shadow-sm\"\n    >\n      <aside class=\"bg-muted/30 flex min-w-0 flex-col border-r\">\n        <div class=\"flex h-14 shrink-0 items-center border-b px-3\">\n          <button ui-button class=\"h-9 w-full justify-start gap-2 rounded-lg\" size=\"sm\">\n            <lucide-icon [img]=\"Pencil\" class=\"size-4\" />\n            Compose\n          </button>\n        </div>\n        <nav class=\"flex-1 space-y-1 overflow-y-auto px-3 pt-3 pb-3\">\n          <p class=\"text-muted-foreground px-2 pb-1 text-xs font-semibold tracking-widest uppercase\">Folders</p>\n          @for (f of folders; track f.id) {\n            <button [class]=\"folderClass(f.id)\" (click)=\"selectFolder(f.id)\">\n              <span class=\"flex min-w-0 items-center gap-2\">\n                <lucide-icon [img]=\"f.icon\" class=\"size-4 shrink-0\" />\n                <span class=\"truncate\">{{ f.label }}</span>\n              </span>\n              @if (unreadCount(f.id) > 0) {\n                <span class=\"text-muted-foreground shrink-0 text-xs tabular-nums\">{{ unreadCount(f.id) }}</span>\n              }\n            </button>\n          }\n\n          <p class=\"text-muted-foreground mt-4 px-2 pb-1 text-xs font-semibold tracking-widest uppercase\">Labels</p>\n          @for (l of labels; track l.id) {\n            <button [class]=\"labelClass(l.id)\" (click)=\"selectLabel(l.id)\">\n              <span class=\"flex min-w-0 items-center gap-2\">\n                <span [class]=\"labelDotClass(l.color)\"></span>\n                <span class=\"truncate\">{{ l.label }}</span>\n              </span>\n              <span class=\"text-muted-foreground shrink-0 text-xs tabular-nums\">{{ labelCount(l.id) }}</span>\n            </button>\n          }\n        </nav>\n      </aside>\n\n      <section class=\"bg-background flex min-w-0 flex-col border-r\">\n        <div class=\"flex h-14 shrink-0 items-center gap-2 border-b px-3\">\n          <div class=\"relative flex-1\">\n            <!-- [class], not class: lucide-angular copies a static class onto its host too, which would offset the svg twice. -->\n            <lucide-icon\n              [img]=\"Search\"\n              [class]=\"'text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2'\"\n            />\n            <ui-input\n              [value]=\"search()\"\n              (valueChange)=\"search.set($event)\"\n              placeholder=\"Search mail\"\n              class=\"h-8 rounded-lg pl-8 text-xs\"\n            />\n          </div>\n          <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Filter\">\n            <lucide-icon [img]=\"Tag\" class=\"size-4\" />\n          </button>\n        </div>\n        <div class=\"flex items-center justify-between px-4 pt-3 pb-2\">\n          <h2 class=\"text-sm font-semibold tracking-tight capitalize\">{{ listTitle() }}</h2>\n          <span class=\"text-muted-foreground text-xs tabular-nums\">\n            {{ visibleMails().length }} message{{ visibleMails().length === 1 ? '' : 's' }}\n          </span>\n        </div>\n        <div class=\"list-scroll flex-1 overflow-y-auto pb-3\">\n          @for (m of visibleMails(); track m.id) {\n            <button aria-label=\"Attach file\" [class]=\"rowClass(m.id)\" (click)=\"selectMail(m.id)\">\n              @if (m.id === activeId()) {\n                <span class=\"bg-primary absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full\"></span>\n              }\n              <div class=\"flex items-start gap-3\">\n                <ui-avatar class=\"mt-0.5 size-8 shrink-0\">\n                  <ui-avatar-fallback class=\"text-xs\">{{ m.from.initials }}</ui-avatar-fallback>\n                </ui-avatar>\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex items-center justify-between gap-2\">\n                    <p [class]=\"senderClass(m.read)\">{{ m.from.name }}</p>\n                    <span class=\"text-muted-foreground shrink-0 text-xs tabular-nums\">\n                      {{ mailTime(m.receivedAt) }}\n                    </span>\n                  </div>\n                  <p [class]=\"subjectClass(m.read)\">{{ m.subject }}</p>\n                  <p class=\"text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed\">{{ m.preview }}</p>\n                  <div class=\"mt-1.5 flex items-center gap-1.5\">\n                    @for (lid of m.labels; track $index) {\n                      <span\n                        class=\"text-muted-foreground bg-muted inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-xs\"\n                      >\n                        <span [class]=\"chipDotClass(lid)\"></span>\n                        {{ labelName(lid) }}\n                      </span>\n                    }\n                    @if (m.hasAttachment) {\n                      <lucide-icon [img]=\"Paperclip\" class=\"text-muted-foreground size-3\" />\n                    }\n                    @if (!m.read) {\n                      <span class=\"bg-primary ml-auto size-1.5 shrink-0 rounded-full\"></span>\n                    }\n                  </div>\n                </div>\n                <span\n                  role=\"button\"\n                  tabindex=\"0\"\n                  class=\"text-muted-foreground hover:text-warning inline-flex shrink-0 cursor-pointer items-center justify-center transition-colors\"\n                  [attr.title]=\"m.starred ? 'Unstar' : 'Star'\"\n                  (click)=\"toggleStar(m.id, $event)\"\n                  (keydown)=\"onStarKey(m.id, $event)\"\n                >\n                  <lucide-icon [img]=\"Star\" [class]=\"starClass(m.starred, 'size-3.5')\" />\n                </span>\n              </div>\n            </button>\n          }\n\n          @if (visibleMails().length === 0) {\n            <div class=\"flex flex-col items-center justify-center px-6 py-16 text-center\">\n              <div class=\"bg-muted mb-3 rounded-full p-3\">\n                <lucide-icon [img]=\"InboxIcon\" class=\"text-muted-foreground size-5\" />\n              </div>\n              <p class=\"text-sm font-medium\">Nothing here</p>\n              <p class=\"text-muted-foreground mt-0.5 text-xs\">No messages match the current filter.</p>\n            </div>\n          }\n        </div>\n      </section>\n\n      <section class=\"bg-background flex min-w-0 flex-col\">\n        @if (activeMail(); as mail) {\n          <div class=\"flex h-14 shrink-0 items-center justify-between gap-2 border-b px-4\">\n            <div class=\"flex items-center gap-1\">\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Back\">\n                <lucide-icon [img]=\"ArrowLeft\" class=\"size-4\" />\n              </button>\n              <button\n                ui-button\n                variant=\"ghost\"\n                size=\"icon\"\n                class=\"size-8\"\n                aria-label=\"Archive\"\n                (click)=\"archive(mail.id)\"\n              >\n                <lucide-icon [img]=\"Archive\" class=\"size-4\" />\n              </button>\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Delete\">\n                <lucide-icon [img]=\"Trash2\" class=\"size-4\" />\n              </button>\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Save\">\n                <lucide-icon [img]=\"Bookmark\" class=\"size-4\" />\n              </button>\n            </div>\n            <div class=\"flex items-center gap-1\">\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Reply\">\n                <lucide-icon [img]=\"Reply\" class=\"size-4\" />\n              </button>\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Reply all\">\n                <lucide-icon [img]=\"ReplyAll\" class=\"size-4\" />\n              </button>\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"Forward\">\n                <lucide-icon [img]=\"Forward\" class=\"size-4\" />\n              </button>\n              <button ui-button variant=\"ghost\" size=\"icon\" class=\"size-8\" aria-label=\"More options\">\n                <lucide-icon [img]=\"MoreVertical\" class=\"size-4\" />\n              </button>\n            </div>\n          </div>\n\n          <div class=\"reader-scroll flex-1 overflow-y-auto\">\n            <div class=\"px-6 pt-5 pb-3\">\n              <h1 class=\"text-foreground text-xl leading-tight font-semibold tracking-tight\">{{ mail.subject }}</h1>\n              <div class=\"mt-2 flex flex-wrap items-center gap-2\">\n                @for (lid of mail.labels; track $index) {\n                  <span\n                    class=\"text-foreground/70 bg-muted inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs\"\n                  >\n                    <span [class]=\"chipDotClass(lid)\"></span>\n                    {{ labelName(lid) }}\n                  </span>\n                }\n              </div>\n            </div>\n\n            <div class=\"flex items-start gap-3 px-6 pb-4\">\n              <ui-avatar class=\"size-10\">\n                <ui-avatar-fallback>{{ mail.from.initials }}</ui-avatar-fallback>\n              </ui-avatar>\n              <div class=\"min-w-0 flex-1\">\n                <div class=\"flex flex-wrap items-baseline gap-x-2\">\n                  <p class=\"text-sm font-semibold\">{{ mail.from.name }}</p>\n                  <p class=\"text-muted-foreground text-xs\">&lt;{{ mail.from.email }}&gt;</p>\n                </div>\n                <p class=\"text-muted-foreground text-xs\">to {{ mail.to }}</p>\n              </div>\n              <div class=\"text-muted-foreground flex shrink-0 items-center gap-2 text-xs\">\n                <span>{{ fullTime(mail.receivedAt) }}</span>\n                <button\n                  class=\"hover:text-warning transition-colors\"\n                  [attr.title]=\"mail.starred ? 'Unstar' : 'Star'\"\n                  (click)=\"toggleStar(mail.id)\"\n                >\n                  <lucide-icon [img]=\"Star\" [class]=\"starClass(mail.starred, 'size-4')\" />\n                </button>\n              </div>\n            </div>\n\n            <article class=\"text-foreground/90 px-6 pb-6 text-sm leading-relaxed whitespace-pre-wrap\">\n              {{ mail.body }}\n            </article>\n\n            @if (mail.hasAttachment) {\n              <div class=\"px-6 pb-6\">\n                <div class=\"bg-muted/40 flex items-center gap-3 rounded-lg border px-3 py-2.5\">\n                  <div class=\"bg-info/10 text-info flex size-8 items-center justify-center rounded-md\">\n                    <lucide-icon [img]=\"FileText\" class=\"size-4\" />\n                  </div>\n                  <div class=\"min-w-0 flex-1\">\n                    <p class=\"truncate text-xs font-medium\">attachment.pdf</p>\n                    <p class=\"text-muted-foreground text-xs\">142 KB · PDF</p>\n                  </div>\n                  <button ui-button aria-label=\"Download attachment\" variant=\"ghost\" size=\"sm\" class=\"h-7 text-xs\">\n                    Download\n                  </button>\n                </div>\n              </div>\n            }\n          </div>\n\n          <div class=\"bg-muted/20 border-t px-4 pt-3 pb-3\">\n            <div class=\"mb-2 flex items-center gap-1\">\n              <button [class]=\"replyTabClass('reply')\" (click)=\"changeReplyMode('reply')\">\n                <lucide-icon [img]=\"Reply\" class=\"size-3.5\" />\n                Reply\n              </button>\n              <button [class]=\"replyTabClass('reply-all')\" (click)=\"changeReplyMode('reply-all')\">\n                <lucide-icon [img]=\"ReplyAll\" class=\"size-3.5\" />\n                Reply all\n              </button>\n              <button [class]=\"replyTabClass('forward')\" (click)=\"changeReplyMode('forward')\">\n                <lucide-icon [img]=\"Forward\" class=\"size-3.5\" />\n                Forward\n              </button>\n            </div>\n            <div\n              class=\"bg-background border-input/60 focus-within:border-primary/40 focus-within:ring-primary/10 group flex flex-col overflow-hidden rounded-2xl border shadow-sm transition-colors duration-150 focus-within:shadow-md focus-within:ring-4\"\n            >\n              @if (replyRecipient()) {\n                <p class=\"border-input/40 text-muted-foreground border-b px-4 pt-2.5 pb-2 text-xs\">\n                  <span class=\"text-foreground/70 font-medium\">To:</span> {{ replyRecipient() }}\n                </p>\n              }\n              <textarea\n                [value]=\"replyDraft()\"\n                (input)=\"onDraftInput($event)\"\n                [attr.placeholder]=\"replyPlaceholder()\"\n                rows=\"1\"\n                class=\"placeholder:text-muted-foreground/70 max-h-40 min-h-[44px] resize-none border-0 bg-transparent px-4 pt-3 pb-1 text-sm leading-relaxed shadow-none outline-none focus-visible:ring-0\"\n                (keydown)=\"onReplyKey($event)\"\n              ></textarea>\n              <div class=\"flex items-center justify-between gap-2 px-2 pb-2\">\n                <div class=\"flex items-center gap-0.5\">\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"text-muted-foreground hover:text-foreground size-8\"\n                    aria-label=\"Attach file\"\n                  >\n                    <lucide-icon [img]=\"Paperclip\" class=\"size-4\" />\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"text-muted-foreground hover:text-foreground size-8\"\n                    aria-label=\"Emoji\"\n                  >\n                    <lucide-icon [img]=\"Smile\" class=\"size-4\" />\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"text-muted-foreground hover:text-foreground size-8\"\n                    aria-label=\"Mention\"\n                  >\n                    <lucide-icon [img]=\"AtSign\" class=\"size-4\" />\n                  </button>\n                </div>\n                <div class=\"flex items-center gap-2\">\n                  <kbd\n                    class=\"bg-muted text-muted-foreground hidden h-5 items-center gap-0.5 rounded border px-1.5 font-mono text-xs sm:inline-flex\"\n                    ><span>⌘</span><span>↵</span></kbd\n                  >\n                  <button\n                    ui-button\n                    class=\"h-8 gap-1.5 rounded-lg px-3 text-xs font-medium\"\n                    [disabled]=\"sendDisabled()\"\n                    aria-label=\"Send\"\n                    (click)=\"sendReply()\"\n                  >\n                    Send\n                    <lucide-icon [img]=\"ArrowRight\" class=\"size-3.5\" />\n                  </button>\n                </div>\n              </div>\n            </div>\n            @if (justSent()) {\n              <p class=\"text-success mt-2 text-center text-xs font-medium\">Reply sent</p>\n            }\n          </div>\n        } @else {\n          <div class=\"flex flex-1 items-center justify-center\">\n            <div class=\"text-center\">\n              <div class=\"bg-muted mx-auto mb-3 size-12 rounded-full p-3\">\n                <lucide-icon [img]=\"InboxIcon\" class=\"text-muted-foreground size-6\" />\n              </div>\n              <p class=\"text-sm font-medium\">No message selected</p>\n              <p class=\"text-muted-foreground mt-1 text-xs\">Pick a message from the list to read it here.</p>\n            </div>\n          </div>\n        }\n      </section>\n    </div>\n  `,\n})\nexport class UiInboxComponent {\n  /** Seed list (React `messages`, default: the stubbed sample mails). */\n  @Input() set messages(value: Mail[] | null | undefined) {\n    this.mails.set(value ?? defaultMails)\n  }\n\n  protected readonly Pencil = Pencil\n  protected readonly Search = Search\n  protected readonly Tag = Tag\n  protected readonly Paperclip = Paperclip\n  protected readonly Star = Star\n  protected readonly InboxIcon = InboxIcon\n  protected readonly ArrowLeft = ArrowLeft\n  protected readonly Archive = Archive\n  protected readonly Trash2 = Trash2\n  protected readonly Bookmark = Bookmark\n  protected readonly Reply = Reply\n  protected readonly ReplyAll = ReplyAll\n  protected readonly Forward = Forward\n  protected readonly MoreVertical = MoreVertical\n  protected readonly FileText = FileText\n  protected readonly Smile = Smile\n  protected readonly AtSign = AtSign\n  protected readonly ArrowRight = ArrowRight\n\n  protected readonly folders = folders\n  protected readonly labels = labels\n\n  readonly mails = signal<Mail[]>(defaultMails)\n  readonly activeFolder = signal<FolderId>('inbox')\n  readonly activeLabel = signal<string | null>(null)\n  readonly activeId = signal<string>('e1')\n  readonly search = signal('')\n  readonly replyMode = signal<ReplyMode>('reply')\n  readonly replyDraft = signal('')\n  readonly justSent = signal(false)\n\n  private readonly labelCounts = computed(() =>\n    labels.reduce<Record<string, number>>((acc, l) => {\n      acc[l.id] = this.mails().filter((m) => m.labels.includes(l.id)).length\n      return acc\n    }, {}),\n  )\n\n  readonly visibleMails = computed(() => {\n    const q = this.search().trim().toLowerCase()\n    let list = this.inView(this.mails())\n    if (q) {\n      list = list.filter(\n        (m) =>\n          m.subject.toLowerCase().includes(q) ||\n          m.from.name.toLowerCase().includes(q) ||\n          m.preview.toLowerCase().includes(q),\n      )\n    }\n    return [...list].sort(byNewest)\n  })\n\n  readonly activeMail = computed(() => this.mails().find((m) => m.id === this.activeId()) ?? null)\n\n  readonly replyRecipient = computed(() => {\n    const m = this.activeMail()\n    if (!m) return ''\n    if (this.replyMode() === 'forward') return ''\n    return m.from.name + ' <' + m.from.email + '>'\n  })\n\n  readonly replyPlaceholder = computed(() =>\n    this.replyMode() === 'forward' ? 'Add a note before forwarding' : 'Write a reply',\n  )\n\n  readonly sendDisabled = computed(() => !this.replyDraft().trim())\n\n  /** Mails in the current folder / label, ignoring the search (React's archive + select logic). */\n  private inView(list: Mail[]): Mail[] {\n    const label = this.activeLabel()\n    const folder = this.activeFolder()\n    return list.filter((m) => {\n      if (label) return m.labels.includes(label)\n      if (folder === 'starred') return m.starred && m.folder !== 'trash'\n      return m.folder === folder\n    })\n  }\n\n  /** React's `useEffect(..., [activeId])`: a new message resets the composer. */\n  private setActive(id: string): void {\n    if (id === this.activeId()) return\n    this.activeId.set(id)\n    this.replyDraft.set('')\n    this.replyMode.set('reply')\n    this.justSent.set(false)\n  }\n\n  unreadCount(id: FolderId): number {\n    const mails = this.mails()\n    if (id === 'starred') return mails.filter((m) => m.starred && !m.read && m.folder !== 'trash').length\n    return mails.filter((m) => m.folder === id && !m.read).length\n  }\n\n  labelCount(id: string): number {\n    return this.labelCounts()[id] ?? 0\n  }\n\n  labelName(id: string): string | undefined {\n    return labelMeta(id)?.label\n  }\n\n  listTitle(): string | undefined {\n    const label = this.activeLabel()\n    return label ? labelMeta(label)?.label : this.activeFolder()\n  }\n\n  mailTime(d: Date): string {\n    return formatMailTime(d)\n  }\n\n  fullTime(d: Date): string {\n    return formatFullTime(d)\n  }\n\n  selectMail(id: string): void {\n    this.setActive(id)\n    this.mails.update((prev) => prev.map((m) => (m.id === id ? { ...m, read: true } : m)))\n  }\n\n  selectFolder(id: FolderId): void {\n    this.activeFolder.set(id)\n    this.activeLabel.set(null)\n    const first = this.mails()\n      .filter((m) => (id === 'starred' ? m.starred && m.folder !== 'trash' : m.folder === id))\n      .sort(byNewest)[0]\n    if (first) this.setActive(first.id)\n  }\n\n  selectLabel(id: string): void {\n    this.activeLabel.set(id)\n    const first = this.mails()\n      .filter((m) => m.labels.includes(id))\n      .sort(byNewest)[0]\n    if (first) this.setActive(first.id)\n  }\n\n  toggleStar(id: string, e?: Event): void {\n    e?.stopPropagation()\n    this.mails.update((prev) => prev.map((m) => (m.id === id ? { ...m, starred: !m.starred } : m)))\n  }\n\n  onStarKey(id: string, e: KeyboardEvent): void {\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      this.toggleStar(id, e)\n    }\n  }\n\n  archive(id: string): void {\n    const next = this.mails().filter((m) => m.id !== id)\n    this.mails.set(next)\n    const nextActive = [...this.inView(next)].sort(byNewest)[0]\n    if (nextActive) this.setActive(nextActive.id)\n  }\n\n  changeReplyMode(mode: ReplyMode): void {\n    this.replyMode.set(mode)\n    this.justSent.set(false)\n  }\n\n  onDraftInput(e: Event): void {\n    this.replyDraft.set((e.target as HTMLTextAreaElement).value)\n  }\n\n  sendReply(): void {\n    const body = this.replyDraft().trim()\n    if (!body) return\n    this.replyDraft.set('')\n    this.justSent.set(true)\n    window.setTimeout(() => {\n      this.justSent.set(false)\n    }, 1800)\n  }\n\n  onReplyKey(e: KeyboardEvent): void {\n    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n      e.preventDefault()\n      this.sendReply()\n    }\n  }\n\n  folderClass(id: FolderId): string {\n    return cn(railRow, this.activeFolder() === id && !this.activeLabel() ? railActive : railIdle)\n  }\n\n  labelClass(id: string): string {\n    return cn(railRow, this.activeLabel() === id ? railActive : railIdle)\n  }\n\n  labelDotClass(color: string): string {\n    return cn('size-2.5 shrink-0 rounded-full', color)\n  }\n\n  chipDotClass(id: string): string {\n    return cn('size-1.5 rounded-full', labelMeta(id)?.color)\n  }\n\n  rowClass(id: string): string {\n    return cn(\n      'group relative w-full px-4 py-3 text-left transition-colors',\n      id === this.activeId() ? 'bg-primary/5' : 'hover:bg-muted/60',\n    )\n  }\n\n  senderClass(read: boolean): string {\n    return cn('truncate text-sm', read ? 'font-medium' : 'font-semibold')\n  }\n\n  subjectClass(read: boolean): string {\n    return cn('mt-0.5 truncate text-xs', read ? 'text-foreground/80' : 'font-semibold')\n  }\n\n  starClass(starred: boolean, size: string): string {\n    return cn(size, starred ? 'fill-warning text-warning' : '')\n  }\n\n  replyTabClass(mode: ReplyMode): string {\n    return cn(replyTabBase, this.replyMode() === mode ? replyTabActive : replyTabIdle)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/inbox/inbox.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/inbox/index.ts",
      "content": "export { UiInboxComponent, type Mail, type LabelTag, type FolderId, type ReplyMode } from './inbox.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/inbox/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/avatar.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/input.json"
  ],
  "description": "Three-pane mail surface. Left rail: Compose button, folders (Inbox/Starred/Sent/Drafts/Spam/Trash) with unread counts, colour-coded labels with totals. Middle: searchable message list with sender, subject, preview, labels, attachment glyphs, star toggle, smart timestamps. Right: reader pane with subject, from/to, body, attachment chip, reply/forward/archive actions. Stateful demo — swap `mails` for your data source.",
  "categories": [
    "communication",
    "layout"
  ]
}