{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "notification-center",
  "title": "Notification Center",
  "type": "registry:block",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/notification-center/notification-center.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { BellOff, Check, CheckCheck, MessageSquare, ShieldAlert, UserPlus, X } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/badge/badge'\nimport '../../components/button/button'\nimport '../../components/relative-time/relative-time'\nimport '../../components/section-card/section-card'\nimport '../../components/tabs/tabs'\n\nexport type NotificationCenterCategory = 'mention' | 'comment' | 'system' | 'security'\n\nexport interface NotificationCenterEntry {\n  id: string\n  category: NotificationCenterCategory\n  title: string\n  body: string\n  date: Date\n  read: boolean\n}\n\ntype NotificationFilter = 'all' | 'unread' | 'mentions'\n\nfunction stubNotifications(): NotificationCenterEntry[] {\n  const now = new Date()\n  const minutesAgo = (m: number) => new Date(now.getTime() - m * 60_000)\n  const hoursAgo = (h: number) => new Date(now.getTime() - h * 3_600_000)\n  const daysAgo = (d: number) => new Date(now.getTime() - d * 86_400_000)\n  return [\n    { id: 'n1', category: 'mention', title: 'Priya mentioned you', body: '@amara can you review the onboarding flow?', date: minutesAgo(6), read: false },\n    { id: 'n2', category: 'security', title: 'New sign-in from Berlin', body: 'Chrome on macOS · 84.190.201.3', date: minutesAgo(52), read: false },\n    { id: 'n3', category: 'comment', title: 'Jonas commented', body: 'Shipped the fix — closing the ticket.', date: hoursAgo(2), read: false },\n    { id: 'n4', category: 'system', title: 'Weekly digest ready', body: 'Your workspace summary for last week.', date: hoursAgo(20), read: true },\n    { id: 'n5', category: 'mention', title: 'Marcus mentioned you', body: 'Design review moved to 15:00, @amara.', date: daysAgo(1), read: false },\n    { id: 'n6', category: 'comment', title: 'Amara commented', body: 'Copy updated per legal review.', date: daysAgo(1), read: true },\n    { id: 'n7', category: 'security', title: 'API key rotated', body: 'uipkge_live_…4f2a was rotated automatically.', date: daysAgo(2), read: true },\n  ]\n}\n\nfunction tileClass(category: NotificationCenterCategory): string {\n  switch (category) {\n    case 'mention':\n      return 'bg-primary/10 text-primary'\n    case 'comment':\n      return 'bg-chart-3/10 text-chart-3'\n    case 'security':\n      return 'bg-warning/10 text-warning'\n    default:\n      return 'bg-muted text-muted-foreground'\n  }\n}\n\nfunction categoryIcon(category: NotificationCenterCategory, cls: string) {\n  switch (category) {\n    case 'mention':\n      return icon(UserPlus, 'user-plus', cls)\n    case 'comment':\n      return icon(MessageSquare, 'message-square', cls)\n    case 'security':\n      return icon(ShieldAlert, 'shield-alert', cls)\n    default:\n      return icon(BellOff, 'bell-off', cls)\n  }\n}\n\n/**\n * <uip-notification-center> — the registry NotificationCenter block as a web component.\n *\n * Class strings are React's NotificationCenter verbatim. React state becomes\n * @state fields. `notifications` is a settable property (defaults to the\n * stub list; pass an array from JS to override — the demo's per-story arrays\n * are wired via getElementById scripts). Category icons are static per-row\n * markup. Dismiss removes the row (React's Dismiss button only stopped\n * propagation; here it dismisses for a working control).\n */\nexport class UipNotificationCenter extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    notifications: { attribute: false },\n    items: { state: true },\n    filter: { state: true },\n  }\n\n  notifications?: NotificationCenterEntry[]\n  private items: NotificationCenterEntry[] = []\n  private filter: NotificationFilter = 'all'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'notification-center')\n    if (this.items.length === 0) this.items = this.notifications ?? stubNotifications()\n  }\n\n  private patch(id: string, changes: Partial<NotificationCenterEntry>) {\n    this.items = this.items.map((n) => (n.id === id ? { ...n, ...changes } : n))\n  }\n\n  private dismiss(id: string, e: Event) {\n    e.stopPropagation()\n    this.items = this.items.filter((n) => n.id !== id)\n  }\n\n  render() {\n    const unreadCount = this.items.filter((n) => !n.read).length\n    const filtered = 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    const startOfToday = new Date()\n    startOfToday.setHours(0, 0, 0, 0)\n    const groups = [\n      { label: 'Today', items: filtered.filter((n) => n.date >= startOfToday) },\n      { label: 'Earlier', items: filtered.filter((n) => n.date < startOfToday) },\n    ].filter((g) => g.items.length > 0)\n    return html`\n      <uip-section-card\n        data-slot=\"notification-center\"\n        heading=\"Notifications\"\n        description=\"Everything that needs your attention.\"\n      >\n        <div slot=\"header-action\" class=\"flex flex-wrap items-center gap-2\">\n          ${unreadCount > 0 ? html`<uip-badge variant=\"default\">${unreadCount} new</uip-badge>` : ''}\n          <uip-button\n            variant=\"ghost\"\n            size=\"sm\"\n            ?disabled=${unreadCount === 0}\n            @click=${() => {\n              this.items = this.items.map((n) => ({ ...n, read: true }))\n            }}\n          >\n            ${icon(CheckCheck, 'check-check')} Mark all read\n          </uip-button>\n        </div>\n\n        <uip-tabs\n          class=\"mb-2\"\n          value=${this.filter}\n          @value-change=${(e: CustomEvent<{ value: string }>) => {\n            this.filter = e.detail.value as NotificationFilter\n          }}\n        >\n          <span slot=\"trigger\" data-value=\"all\">All</span>\n          <span slot=\"trigger\" data-value=\"unread\">Unread</span>\n          <span slot=\"trigger\" data-value=\"mentions\">Mentions</span>\n        </uip-tabs>\n\n        ${groups.length === 0\n          ? html`\n              <div class=\"text-muted-foreground flex flex-col items-center gap-2 py-12 text-sm\">\n                ${icon(BellOff, 'bell-off', 'size-6 opacity-50')}\n                ${this.filter === 'mentions'\n                  ? \"No mentions — you're all caught up.\"\n                  : this.filter === 'unread'\n                    ? 'Nothing unread. Nice work.'\n                    : 'Your inbox is empty.'}\n              </div>\n            `\n          : ''}\n        ${groups.map(\n          (group) => html`\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                ${group.items.map(\n                  (notification) => html`\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=${() => this.patch(notification.id, { read: true })}\n                    >\n                      <span\n                        class=${cn('mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md', tileClass(notification.category))}\n                        aria-hidden=\"true\"\n                      >\n                        ${categoryIcon(notification.category, '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                          ${!notification.read\n                            ? html`<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                        <uip-relative-time\n                          date=${notification.date.toISOString()}\n                          class=\"text-muted-foreground text-xs\"\n                        ></uip-relative-time>\n                        <span class=\"flex opacity-0 transition-opacity group-hover:opacity-100\">\n                          ${!notification.read\n                            ? html`<uip-button\n                                variant=\"ghost\"\n                                size=\"icon-sm\"\n                                aria-label=\"Mark as read\"\n                                @click=${(e: Event) => {\n                                  e.stopPropagation()\n                                  this.patch(notification.id, { read: true })\n                                }}\n                              >\n                                ${icon(Check, 'check')}\n                              </uip-button>`\n                            : ''}\n                          <uip-button\n                            variant=\"ghost\"\n                            size=\"icon-sm\"\n                            aria-label=\"Dismiss\"\n                            @click=${(e: Event) => this.dismiss(notification.id, e)}\n                          >\n                            ${icon(X, 'x')}\n                          </uip-button>\n                        </span>\n                      </div>\n                    </li>\n                  `,\n                )}\n              </ul>\n            </div>\n          `,\n        )}\n      </uip-section-card>\n    `\n  }\n}\n\ncustomElements.get('uip-notification-center') || customElements.define('uip-notification-center', UipNotificationCenter)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-notification-center': UipNotificationCenter\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/notification-center/notification-center.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/relative-time.json",
    "https://uipkge.dev/r/lit/section-card.json",
    "https://uipkge.dev/r/lit/tabs.json"
  ]
}
