{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "notifications-popover",
  "title": "Notifications Popover",
  "type": "registry:block",
  "description": "Header-grade notifications panel. Slot-trigger Popover the consumer wraps around their own Bell button. Filter tabs (All/Unread), Today/Earlier groups, category-coloured accent bars, hover-dismiss, mark-all-read, slide-in animation. Slot receives `unreadCount` so the trigger can show a badge.",
  "categories": [
    "communication",
    "dashboard",
    "overlay"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/notifications-popover/notifications-popover.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport {\n  Archive,\n  Bell,\n  BellOff,\n  Calendar,\n  CreditCard,\n  FileText,\n  GraduationCap,\n  Target,\n  UserPlus,\n  X,\n} 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/popover/popover'\n\nexport type PopoverNotificationCategory = 'hr' | 'payroll' | 'timeoff' | 'performance' | 'training' | 'system'\n\ninterface PopoverNotification {\n  id: string\n  title: string\n  body: string\n  category: PopoverNotificationCategory\n  timestamp: Date\n  read: boolean\n}\n\nfunction seedNotifications(): PopoverNotification[] {\n  const now = new Date()\n  const min = 60 * 1000\n  const hour = 60 * min\n  return [\n    { id: '1', title: 'Time off approved', body: 'Your annual leave request for Mar 15-18 has been approved by Sarah Connor.', category: 'timeoff', timestamp: new Date(now.getTime() - 12 * min), read: false },\n    { id: '2', title: 'New employee onboarded', body: 'Marcus Rivera has joined the Engineering team as Senior Developer.', category: 'hr', timestamp: new Date(now.getTime() - 45 * min), read: false },\n    { id: '3', title: 'Performance review due', body: 'Annual performance review for 3 direct reports is due by end of this week.', category: 'performance', timestamp: new Date(now.getTime() - 2 * hour), read: false },\n    { id: '4', title: 'Payroll processed', body: 'March 2026 payroll has been processed successfully. 103 employees paid.', category: 'payroll', timestamp: new Date(now.getTime() - 5 * hour), read: true },\n    { id: '5', title: 'Training enrollment open', body: 'New course available: \"Leadership Fundamentals Q2\". Enroll before Mar 20.', category: 'training', timestamp: new Date(now.getTime() - 8 * hour), read: true },\n    { id: '6', title: 'Document requires signature', body: 'Updated Employee Handbook 2026 needs your acknowledgement.', category: 'system', timestamp: new Date(now.getTime() - 26 * hour), read: true },\n    { id: '7', title: 'Time off request pending', body: 'Alice Johnson requested sick leave for Mar 12. Awaiting your approval.', category: 'timeoff', timestamp: new Date(now.getTime() - 28 * hour), read: true },\n    { id: '8', title: 'Benefits enrollment closing', body: \"Open enrollment period ends Mar 31. 12 employees haven't enrolled yet.\", category: 'hr', timestamp: new Date(now.getTime() - 48 * hour), read: true },\n  ]\n}\n\nfunction accent(category: PopoverNotificationCategory): string {\n  switch (category) {\n    case 'hr':\n      return 'bg-success'\n    case 'payroll':\n      return 'bg-info'\n    case 'timeoff':\n      return 'bg-chart-3'\n    case 'performance':\n      return 'bg-warning'\n    case 'training':\n      return 'bg-primary'\n    default:\n      return 'bg-muted-foreground'\n  }\n}\n\nfunction tileBg(category: PopoverNotificationCategory): string {\n  switch (category) {\n    case 'hr':\n      return 'bg-success/10 text-success'\n    case 'payroll':\n      return 'bg-info/10 text-info'\n    case 'timeoff':\n      return 'bg-chart-3/10 text-chart-3'\n    case 'performance':\n      return 'bg-warning/10 text-warning'\n    case 'training':\n      return 'bg-primary/10 text-primary'\n    default:\n      return 'bg-muted text-muted-foreground'\n  }\n}\n\nfunction categoryIcon(category: PopoverNotificationCategory, cls: string) {\n  switch (category) {\n    case 'hr':\n      return icon(UserPlus, 'user-plus', cls)\n    case 'payroll':\n      return icon(CreditCard, 'credit-card', cls)\n    case 'timeoff':\n      return icon(Calendar, 'calendar', cls)\n    case 'performance':\n      return icon(Target, 'target', cls)\n    case 'training':\n      return icon(GraduationCap, 'graduation-cap', cls)\n    default:\n      return icon(FileText, 'file-text', cls)\n  }\n}\n\nfunction formatTime(date: Date): string {\n  const diffMs = Date.now() - date.getTime()\n  const diffMin = Math.floor(diffMs / 60000)\n  if (diffMin < 1) return 'Just now'\n  if (diffMin < 60) return `${diffMin}m ago`\n  const diffHrs = Math.floor(diffMin / 60)\n  if (diffHrs < 24) return `${diffHrs}h ago`\n  const diffDays = Math.floor(diffHrs / 24)\n  if (diffDays === 1) return 'Yesterday'\n  return `${diffDays}d ago`\n}\n\n/**\n * <uip-notifications-popover> — the registry NotificationsPopover block as a web component.\n *\n * Class strings are React's NotificationsPopover verbatim. React state becomes\n * @state fields. React's `trigger` render-prop becomes a `trigger` slot:\n * put your own Bell button in `slot=\"trigger\"` (it receives the live unread\n * dot via the fallback); with no slotted trigger the default Bell button\n * renders. React's injected `<style>` (scrollbar, hover, notif-slide-in) is\n * copied into `static styles` (the keyframes exception covers the slide-in;\n * the scrollbar/fade rules ride along as React-authored block CSS).\n */\nexport class UipNotificationsPopover 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  // Panel + slide-in CSS is copied from React's injected <style> (keyframes exception).\n  static styles = [\n    tailwind,\n    css`:host { display: inline-block; }`,\n    css`\n      .notification-scroll::-webkit-scrollbar { width: 4px; }\n      .notification-scroll::-webkit-scrollbar-track { background: transparent; }\n      .notification-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\n      .notification-item { cursor: pointer; transition: background-color 0.15s ease; animation: notif-slide-in 0.25s ease both; }\n      .notification-item:hover { background-color: var(--muted); }\n      @keyframes notif-slide-in {\n        from { opacity: 0; transform: translateY(4px); }\n        to { opacity: 1; transform: translateY(0); }\n      }\n      .notification-scroll { position: relative; }\n      .notification-scroll::after {\n        content: '';\n        position: sticky;\n        bottom: 0;\n        left: 0;\n        right: 0;\n        display: block;\n        height: 24px;\n        margin-top: -24px;\n        background: linear-gradient(to bottom, transparent, var(--popover));\n        pointer-events: none;\n      }\n    `,\n  ]\n\n  static properties = {\n    notifications: { state: true },\n    activeFilter: { state: true },\n    isOpen: { state: true },\n  }\n\n  private notifications: PopoverNotification[] = []\n  private activeFilter: 'all' | 'unread' = 'all'\n  private isOpen = false\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', 'notifications-popover')\n    if (this.notifications.length === 0) this.notifications = seedNotifications()\n  }\n\n  private get unreadCount(): number {\n    return this.notifications.filter((n) => !n.read).length\n  }\n\n  private get filtered(): PopoverNotification[] {\n    if (this.activeFilter === 'unread') return this.notifications.filter((n) => !n.read)\n    return this.notifications\n  }\n\n  private markAsRead(id: string) {\n    this.notifications = this.notifications.map((n) => (n.id === id ? { ...n, read: true } : n))\n  }\n\n  private markAllRead() {\n    this.notifications = this.notifications.map((n) => ({ ...n, read: true }))\n  }\n\n  private dismiss(id: string, e: Event) {\n    e.stopPropagation()\n    this.notifications = this.notifications.filter((n) => n.id !== id)\n  }\n\n  private renderItem(n: PopoverNotification, delay: number) {\n    return html`\n      <div\n        data-slot=\"notifications-popover\"\n        class=\"notification-item group relative\"\n        style=\"animation-delay: ${delay}ms\"\n        @click=${() => this.markAsRead(n.id)}\n      >\n        <div\n          class=${cn(\n            'absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full transition-opacity',\n            !n.read ? accent(n.category) : 'opacity-0',\n          )}\n        ></div>\n        <div class=\"flex gap-3 px-4 py-3\">\n          <div class=${cn('mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg', tileBg(n.category))}>\n            ${categoryIcon(n.category, 'size-4')}\n          </div>\n          <div class=\"min-w-0 flex-1\">\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=${cn('text-[13px] leading-snug', !n.read ? 'font-semibold' : 'font-medium')}>${n.title}</p>\n              <div class=\"flex shrink-0 items-center gap-1.5\">\n                <span class=\"text-muted-foreground text-xs whitespace-nowrap tabular-nums\">${formatTime(n.timestamp)}</span>\n                ${!n.read ? html`<span class=\"bg-primary size-1.5 shrink-0 rounded-full\"></span>` : ''}\n              </div>\n            </div>\n            <p class=\"text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed\">${n.body}</p>\n          </div>\n          <button\n            class=\"text-muted-foreground hover:text-foreground mt-0.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100\"\n            title=\"Dismiss\"\n            @click=${(e: Event) => this.dismiss(n.id, e)}\n          >\n            ${icon(X, 'x', 'size-3.5')}\n          </button>\n        </div>\n      </div>\n    `\n  }\n\n  render() {\n    const now = new Date()\n    // Mirror the live count for slotted custom triggers (React's trigger({ unreadCount })).\n    this.setAttribute('unread-count', String(this.unreadCount))\n    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n    const today = this.filtered.filter((n) => n.timestamp >= todayStart)\n    const earlier = this.filtered.filter((n) => n.timestamp < todayStart)\n    return html`\n      <uip-popover\n        ?open=${this.isOpen}\n        @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n          this.isOpen = e.detail.open\n        }}\n      >\n        <slot name=\"trigger\" slot=\"trigger\">\n          <uip-button\n            variant=\"ghost\"\n            size=\"icon\"\n            class=\"text-muted-foreground hover:text-foreground relative size-9 rounded-lg\"\n            aria-label=\"Notifications\"\n          >\n            ${icon(Bell, 'bell', 'size-4')}\n            ${this.unreadCount > 0\n              ? html`<span class=\"bg-primary ring-background absolute top-1.5 right-1.5 size-2 rounded-full ring-2\"></span>`\n              : ''}\n          </uip-button>\n        </slot>\n        <div class=\"notification-panel w-full max-w-[380px] overflow-hidden rounded-lg border p-0 shadow-xl\">\n          <div class=\"flex items-center justify-between px-4 pt-4 pb-3\">\n            <div class=\"flex items-center gap-2.5\">\n              <h3 class=\"text-sm font-semibold tracking-tight\">Notifications</h3>\n              ${this.unreadCount > 0\n                ? html`<uip-badge\n                    class=\"bg-primary/15 text-primary hover:bg-primary/15 h-5 rounded-full px-1.5 text-xs font-bold tabular-nums\"\n                    >${this.unreadCount}</uip-badge\n                  >`\n                : ''}\n            </div>\n            ${this.unreadCount > 0\n              ? html`<uip-button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  class=\"text-muted-foreground hover:text-foreground -mr-1 h-7 px-2 text-xs\"\n                  @click=${this.markAllRead}\n                >\n                  Mark all read\n                </uip-button>`\n              : ''}\n          </div>\n\n          <div class=\"border-b px-4\">\n            <div class=\"flex gap-0\">\n              <button\n                class=${cn(\n                  'relative px-3 pb-2.5 text-xs font-medium transition-colors',\n                  this.activeFilter === 'all' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',\n                )}\n                @click=${() => {\n                  this.activeFilter = 'all'\n                }}\n              >\n                All\n                ${this.activeFilter === 'all'\n                  ? html`<span class=\"bg-primary absolute right-0 bottom-0 left-0 h-[2px] rounded-t-full\"></span>`\n                  : ''}\n              </button>\n              <button\n                class=${cn(\n                  'relative px-3 pb-2.5 text-xs font-medium transition-colors',\n                  this.activeFilter === 'unread' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',\n                )}\n                @click=${() => {\n                  this.activeFilter = 'unread'\n                }}\n              >\n                Unread\n                ${this.activeFilter === 'unread'\n                  ? html`<span class=\"bg-primary absolute right-0 bottom-0 left-0 h-[2px] rounded-t-full\"></span>`\n                  : ''}\n              </button>\n            </div>\n          </div>\n\n          <div class=\"notification-scroll max-h-[420px] overflow-y-auto\">\n            ${this.filtered.length === 0\n              ? html`\n                  <div class=\"flex flex-col items-center justify-center py-14 text-center\">\n                    <div class=\"bg-muted mb-3 rounded-full p-3\">\n                      ${icon(BellOff, 'bell-off', 'text-muted-foreground size-5')}\n                    </div>\n                    <p class=\"text-sm font-medium\">All caught up</p>\n                    <p class=\"text-muted-foreground mt-0.5 text-xs\">\n                      No ${this.activeFilter === 'unread' ? 'unread ' : ''}notifications\n                    </p>\n                  </div>\n                `\n              : html`\n                  ${today.length > 0\n                    ? html`\n                        <div class=\"px-4 pt-3 pb-1\">\n                          <span class=\"text-muted-foreground text-xs font-semibold tracking-widest uppercase\">Today</span>\n                        </div>\n                        ${today.map((n, i) => this.renderItem(n, i * 30))}\n                      `\n                    : ''}\n                  ${earlier.length > 0\n                    ? html`\n                        <div class=\"px-4 pt-3 pb-1\">\n                          <span class=\"text-muted-foreground text-xs font-semibold tracking-widest uppercase\"\n                            >Earlier</span\n                          >\n                        </div>\n                        ${earlier.map((n, i) => this.renderItem(n, (today.length + i) * 30))}\n                      `\n                    : ''}\n                `}\n          </div>\n\n          <div class=\"border-t px-4 py-2.5\">\n            <a\n              href=\"#\"\n              class=\"text-muted-foreground hover:text-foreground flex items-center justify-center gap-1.5 text-xs transition-colors\"\n              @click=${() => {\n                this.isOpen = false\n              }}\n            >\n              ${icon(Archive, 'archive', 'size-3')} View all notifications\n            </a>\n          </div>\n        </div>\n      </uip-popover>\n    `\n  }\n}\n\ncustomElements.get('uip-notifications-popover') || customElements.define('uip-notifications-popover', UipNotificationsPopover)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-notifications-popover': UipNotificationsPopover\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/notifications-popover/notifications-popover.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/popover.json"
  ]
}
