{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "notification-center",
  "title": "Notification Center",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/notification-center/notification-center.component.ts",
      "content": "import { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core'\nimport {\n  BellOff,\n  Check,\n  CheckCheck,\n  LucideAngularModule,\n  MessageSquare,\n  ShieldAlert,\n  UserPlus,\n  X,\n  type LucideIconData,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiRelativeTimeComponent } from '@/ui/relative-time/relative-time.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\nimport { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'\n\nexport type NotificationCenterCategory = 'mention' | 'comment' | 'system' | 'security'\n\nexport interface NotificationCenterItem {\n  id: string\n  category: NotificationCenterCategory\n  title: string\n  body: string\n  date: Date\n  read: boolean\n}\n\nexport type NotificationCenterFilter = 'all' | 'unread' | 'mentions'\n\nconst categoryConfig: Record<NotificationCenterCategory, { icon: LucideIconData; tileClass: string }> = {\n  mention: { icon: UserPlus, tileClass: 'bg-primary/10 text-primary' },\n  comment: { icon: MessageSquare, tileClass: 'bg-chart-3/10 text-chart-3' },\n  system: { icon: BellOff, tileClass: 'bg-muted text-muted-foreground' },\n  security: { icon: ShieldAlert, tileClass: 'bg-warning/10 text-warning' },\n}\n\nconst now = new Date()\nconst minutesAgo = (m: number) => new Date(now.getTime() - m * 60_000)\nconst hoursAgo = (h: number) => new Date(now.getTime() - h * 3_600_000)\nconst daysAgo = (d: number) => new Date(now.getTime() - d * 86_400_000)\n\nconst stubNotifications: NotificationCenterItem[] = [\n  {\n    id: 'n1',\n    category: 'mention',\n    title: 'Priya mentioned you',\n    body: '@amara can you review the onboarding flow?',\n    date: minutesAgo(6),\n    read: false,\n  },\n  {\n    id: 'n2',\n    category: 'security',\n    title: 'New sign-in from Berlin',\n    body: 'Chrome on macOS · 84.190.201.3',\n    date: minutesAgo(52),\n    read: false,\n  },\n  {\n    id: 'n3',\n    category: 'comment',\n    title: 'Jonas commented',\n    body: 'Shipped the fix — closing the ticket.',\n    date: hoursAgo(2),\n    read: false,\n  },\n  {\n    id: 'n4',\n    category: 'system',\n    title: 'Weekly digest ready',\n    body: 'Your workspace summary for last week.',\n    date: hoursAgo(20),\n    read: true,\n  },\n  {\n    id: 'n5',\n    category: 'mention',\n    title: 'Marcus mentioned you',\n    body: 'Design review moved to 15:00, @amara.',\n    date: daysAgo(1),\n    read: false,\n  },\n  {\n    id: 'n6',\n    category: 'comment',\n    title: 'Amara commented',\n    body: 'Copy updated per legal review.',\n    date: daysAgo(1),\n    read: true,\n  },\n  {\n    id: 'n7',\n    category: 'security',\n    title: 'API key rotated',\n    body: 'uipkge_live_…4f2a was rotated automatically.',\n    date: daysAgo(2),\n    read: true,\n  },\n]\n\n/**\n * NotificationCenter -- the React block, 1:1: a full-page notification SectionCard with an\n * unread badge, mark-all-read, All / Unread / Mentions tabs that filter the list, day-grouped\n * rows (Today / Earlier) pairing category icon tiles with titles, bodies, relative timestamps,\n * unread dots and hover actions (mark read, dismiss), plus per-tab empty states. Pass\n * `[notifications]` to swap the sample data for your source.\n *\n * Icons inside `ui-button` sit in `<lucide-icon>`, so the React `size=\"sm\"` `has-[>svg]:px-2.5`\n * padding is set explicitly on the mark-all-read button.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-notification-center, [ui-notification-center]',\n  standalone: true,\n  // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card.\n  host: { '[attr.class]': '\"contents\"', 'data-slot': 'notification-center' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiRelativeTimeComponent,\n    UiSectionCardComponent,\n    UiTabsComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n  ],\n  template: `\n    <div\n      ui-section-card\n      [title]=\"'Notifications'\"\n      [description]=\"'Everything that needs your attention.'\"\n      [class]=\"className\"\n    >\n      <div slot=\"header-action\" class=\"flex flex-wrap items-center gap-2\">\n        @if (unreadCount() > 0) {\n          <span ui-badge variant=\"default\">{{ unreadCount() }} new</span>\n        }\n        <button\n          ui-button\n          variant=\"ghost\"\n          size=\"sm\"\n          class=\"px-2.5\"\n          [disabled]=\"unreadCount() === 0\"\n          (click)=\"markAllRead()\"\n        >\n          <lucide-icon [img]=\"CheckCheck\" aria-hidden=\"true\" />\n          Mark all read\n        </button>\n      </div>\n\n      <div ui-tabs [value]=\"filter()\" (valueChange)=\"setFilter($event)\" class=\"mb-2\">\n        <div ui-tabs-list>\n          <button ui-tabs-trigger value=\"all\">All</button>\n          <button ui-tabs-trigger value=\"unread\">Unread</button>\n          <button ui-tabs-trigger value=\"mentions\">Mentions</button>\n        </div>\n      </div>\n\n      @if (groups().length === 0) {\n        <div class=\"text-muted-foreground flex flex-col items-center gap-2 py-12 text-sm\">\n          <!-- [class] feeds only the svg: a static class lands on the host AND the svg (opacity twice). -->\n          <lucide-icon [img]=\"BellOff\" class=\"size-6\" [class]=\"'opacity-50'\" aria-hidden=\"true\" />\n          @if (filter() === 'mentions') {\n            No mentions — you're all caught up.\n          } @else if (filter() === 'unread') {\n            Nothing unread. Nice work.\n          } @else {\n            Your inbox is empty.\n          }\n        </div>\n      }\n\n      @for (group of groups(); track group.label) {\n        <div class=\"mb-2\">\n          <p class=\"text-muted-foreground py-1.5 text-xs font-medium tracking-widest uppercase\">{{ group.label }}</p>\n          <ul class=\"divide-y\">\n            @for (notification of group.items; track notification.id) {\n              <li\n                class=\"group hover:bg-accent/40 -mx-2 flex cursor-pointer items-start gap-3 rounded-md px-2 py-3 transition-colors\"\n                (click)=\"markRead(notification.id)\"\n              >\n                <span\n                  [class]=\"\n                    cn('mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md', tileClass(notification))\n                  \"\n                  aria-hidden=\"true\"\n                >\n                  <lucide-icon [img]=\"icon(notification)\" class=\"size-4\" />\n                </span>\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <p [class]=\"cn('truncate text-sm', notification.read ? 'font-normal' : 'font-medium')\">\n                      {{ notification.title }}\n                    </p>\n                    @if (!notification.read) {\n                      <span class=\"bg-primary size-1.5 shrink-0 rounded-full\" aria-label=\"Unread\"></span>\n                    }\n                  </div>\n                  <p class=\"text-muted-foreground truncate text-xs\">{{ notification.body }}</p>\n                </div>\n                <div class=\"flex shrink-0 items-center gap-1\">\n                  <time ui-relative-time [date]=\"notification.date\" class=\"text-muted-foreground text-xs\"></time>\n                  <span class=\"flex opacity-0 transition-opacity group-hover:opacity-100\">\n                    @if (!notification.read) {\n                      <button\n                        ui-button\n                        variant=\"ghost\"\n                        size=\"icon-sm\"\n                        aria-label=\"Mark as read\"\n                        (click)=\"$event.stopPropagation(); markRead(notification.id)\"\n                      >\n                        <lucide-icon [img]=\"Check\" aria-hidden=\"true\" />\n                      </button>\n                    }\n                    <button\n                      ui-button\n                      variant=\"ghost\"\n                      size=\"icon-sm\"\n                      aria-label=\"Dismiss\"\n                      (click)=\"$event.stopPropagation()\"\n                    >\n                      <lucide-icon [img]=\"X\" aria-hidden=\"true\" />\n                    </button>\n                  </span>\n                </div>\n              </li>\n            }\n          </ul>\n        </div>\n      }\n    </div>\n  `,\n})\nexport class UiNotificationCenterComponent {\n  protected readonly BellOff = BellOff\n  protected readonly Check = Check\n  protected readonly CheckCheck = CheckCheck\n  protected readonly X = X\n  protected readonly cn = cn\n\n  /** Your notifications; falls back to the sample list (React `notifications ?? stubNotifications`). */\n  @Input() set notifications(value: NotificationCenterItem[] | undefined) {\n    this.items.set(value ?? stubNotifications)\n  }\n  @Input('class') className?: string\n\n  readonly items = signal<NotificationCenterItem[]>(stubNotifications)\n  readonly filter = signal<NotificationCenterFilter>('all')\n\n  readonly unreadCount = computed(() => this.items().filter((n) => !n.read).length)\n\n  readonly filtered = computed(() =>\n    this.items().filter((n) => {\n      if (this.filter() === 'unread') return !n.read\n      if (this.filter() === 'mentions') return n.category === 'mention'\n      return true\n    }),\n  )\n\n  readonly groups = computed(() => {\n    const startOfToday = new Date()\n    startOfToday.setHours(0, 0, 0, 0)\n    return [\n      { label: 'Today', items: this.filtered().filter((n) => n.date >= startOfToday) },\n      { label: 'Earlier', items: this.filtered().filter((n) => n.date < startOfToday) },\n    ].filter((group) => group.items.length > 0)\n  })\n\n  protected icon(n: NotificationCenterItem): LucideIconData {\n    return categoryConfig[n.category].icon\n  }\n\n  protected tileClass(n: NotificationCenterItem): string {\n    return categoryConfig[n.category].tileClass\n  }\n\n  setFilter(value: string): void {\n    this.filter.set(value as NotificationCenterFilter)\n  }\n\n  markRead(id: string): void {\n    this.items.update((list) => list.map((n) => (n.id === id ? { ...n, read: true } : n)))\n  }\n\n  markAllRead(): void {\n    this.items.update((list) => list.map((n) => ({ ...n, read: true })))\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/notification-center/notification-center.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/notification-center/index.ts",
      "content": "export {\n  UiNotificationCenterComponent,\n  type NotificationCenterCategory,\n  type NotificationCenterFilter,\n  type NotificationCenterItem,\n} from './notification-center.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/notification-center/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/relative-time.json",
    "https://uipkge.dev/r/angular/section-card.json",
    "https://uipkge.dev/r/angular/tabs.json"
  ],
  "description": "Full-page notification center: unread badge, mark-all-read, All/Unread/Mentions tabs with functional filtering, day-grouped rows (Today / Earlier) pairing category icon tiles with titles, bodies, relative timestamps, unread dots and hover actions (mark read, dismiss). Per-tab empty states included; swap `notifications` for your source.",
  "categories": [
    "communication",
    "dashboard"
  ]
}