{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "notifications-popover",
  "title": "Notifications Popover",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/notifications-popover/NotificationsPopover.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref, computed } from 'vue'\nimport { UserPlus, Calendar, CreditCard, FileText, GraduationCap, Target, X, BellOff, Archive } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\n\ntype NotificationCategory = 'hr' | 'payroll' | 'timeoff' | 'performance' | 'training' | 'system'\n\ninterface Notification {\n  id: string\n  title: string\n  body: string\n  category: NotificationCategory\n  timestamp: Date\n  read: boolean\n  actionUrl?: string\n  actor?: string\n}\n\nconst categoryConfig: Record<NotificationCategory, { icon: any; accent: string; bg: string }> = {\n  hr: {\n    icon: UserPlus,\n    accent: 'bg-success',\n    bg: 'bg-success/10 text-success',\n  },\n  payroll: {\n    icon: CreditCard,\n    accent: 'bg-info',\n    bg: 'bg-info/10 text-info',\n  },\n  timeoff: {\n    icon: Calendar,\n    accent: 'bg-chart-3',\n    bg: 'bg-chart-3/10 text-chart-3',\n  },\n  performance: {\n    icon: Target,\n    accent: 'bg-warning',\n    bg: 'bg-warning/10 text-warning',\n  },\n  training: {\n    icon: GraduationCap,\n    accent: 'bg-primary',\n    bg: 'bg-primary/10 text-primary',\n  },\n  system: {\n    icon: FileText,\n    accent: 'bg-muted-foreground',\n    bg: 'bg-muted text-muted-foreground',\n  },\n}\n\nconst now = new Date()\n\nconst notifications = ref<Notification[]>([\n  {\n    id: '1',\n    title: 'Time off approved',\n    body: 'Your annual leave request for Mar 15-18 has been approved by Sarah Connor.',\n    category: 'timeoff',\n    timestamp: new Date(now.getTime() - 12 * 60 * 1000),\n    read: false,\n    actor: 'Sarah Connor',\n  },\n  {\n    id: '2',\n    title: 'New employee onboarded',\n    body: 'Marcus Rivera has joined the Engineering team as Senior Developer.',\n    category: 'hr',\n    timestamp: new Date(now.getTime() - 45 * 60 * 1000),\n    read: false,\n    actor: 'Marcus Rivera',\n  },\n  {\n    id: '3',\n    title: 'Performance review due',\n    body: 'Annual performance review for 3 direct reports is due by end of this week.',\n    category: 'performance',\n    timestamp: new Date(now.getTime() - 2 * 60 * 60 * 1000),\n    read: false,\n  },\n  {\n    id: '4',\n    title: 'Payroll processed',\n    body: 'March 2026 payroll has been processed successfully. 103 employees paid.',\n    category: 'payroll',\n    timestamp: new Date(now.getTime() - 5 * 60 * 60 * 1000),\n    read: true,\n  },\n  {\n    id: '5',\n    title: 'Training enrollment open',\n    body: 'New course available: \"Leadership Fundamentals Q2\". Enroll before Mar 20.',\n    category: 'training',\n    timestamp: new Date(now.getTime() - 8 * 60 * 60 * 1000),\n    read: true,\n  },\n  {\n    id: '6',\n    title: 'Document requires signature',\n    body: 'Updated Employee Handbook 2026 needs your acknowledgement.',\n    category: 'system',\n    timestamp: new Date(now.getTime() - 26 * 60 * 60 * 1000),\n    read: true,\n    actionUrl: '/documents',\n  },\n  {\n    id: '7',\n    title: 'Time off request pending',\n    body: 'Alice Johnson requested sick leave for Mar 12. Awaiting your approval.',\n    category: 'timeoff',\n    timestamp: new Date(now.getTime() - 28 * 60 * 60 * 1000),\n    read: true,\n    actor: 'Alice Johnson',\n  },\n  {\n    id: '8',\n    title: 'Benefits enrollment closing',\n    body: \"Open enrollment period ends Mar 31. 12 employees haven't enrolled yet.\",\n    category: 'hr',\n    timestamp: new Date(now.getTime() - 48 * 60 * 60 * 1000),\n    read: true,\n  },\n])\n\nconst activeFilter = ref<'all' | 'unread'>('all')\nconst isOpen = ref(false)\n\nconst unreadCount = computed(() => notifications.value.filter((n) => !n.read).length)\n\nconst filteredNotifications = computed(() => {\n  if (activeFilter.value === 'unread') {\n    return notifications.value.filter((n) => !n.read)\n  }\n  return notifications.value\n})\n\nconst todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n\nconst groupedNotifications = computed(() => {\n  const today = filteredNotifications.value.filter((n) => n.timestamp >= todayStart)\n  const earlier = filteredNotifications.value.filter((n) => n.timestamp < todayStart)\n  return { today, earlier }\n})\n\nfunction formatTime(date: Date): string {\n  const diffMs = now.getTime() - 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\nfunction markAsRead(id: string) {\n  const n = notifications.value.find((n) => n.id === id)\n  if (n) n.read = true\n}\n\nfunction markAllRead() {\n  notifications.value.forEach((n) => (n.read = true))\n}\n\nfunction dismissNotification(id: string) {\n  notifications.value = notifications.value.filter((n) => n.id !== id)\n}\n</script>\n\n<template>\n  <Popover v-model:open=\"isOpen\">\n    <PopoverTrigger as-child>\n      <slot :unread-count=\"unreadCount\" />\n    </PopoverTrigger>\n    <PopoverContent\n      align=\"end\"\n      :side-offset=\"8\"\n      class=\"notification-panel w-[380px] overflow-hidden rounded-lg border p-0 shadow-xl\"\n    >\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          <Badge\n            v-if=\"unreadCount > 0\"\n            class=\"bg-primary/15 text-primary hover:bg-primary/15 h-5 rounded-full px-1.5 text-[10px] font-bold tabular-nums\"\n          >\n            {{ unreadCount }}\n          </Badge>\n        </div>\n        <Button\n          v-if=\"unreadCount > 0\"\n          variant=\"ghost\"\n          size=\"sm\"\n          class=\"text-muted-foreground hover:text-foreground -mr-1 h-7 px-2 text-xs\"\n          @click=\"markAllRead\"\n        >\n          Mark all read\n        </Button>\n      </div>\n\n      <div class=\"border-b px-4\">\n        <div class=\"flex gap-0\">\n          <button\n            :class=\"[\n              'relative px-3 pb-2.5 text-xs font-medium transition-colors',\n              activeFilter === 'all' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',\n            ]\"\n            @click=\"activeFilter = 'all'\"\n          >\n            All\n            <span\n              v-if=\"activeFilter === 'all'\"\n              class=\"bg-primary absolute right-0 bottom-0 left-0 h-[2px] rounded-t-full\"\n            />\n          </button>\n          <button\n            :class=\"[\n              'relative px-3 pb-2.5 text-xs font-medium transition-colors',\n              activeFilter === 'unread' ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',\n            ]\"\n            @click=\"activeFilter = 'unread'\"\n          >\n            Unread\n            <span\n              v-if=\"activeFilter === 'unread'\"\n              class=\"bg-primary absolute right-0 bottom-0 left-0 h-[2px] rounded-t-full\"\n            />\n          </button>\n        </div>\n      </div>\n\n      <div class=\"notification-scroll max-h-[420px] overflow-y-auto\">\n        <div\n          v-if=\"filteredNotifications.length === 0\"\n          class=\"flex flex-col items-center justify-center py-14 text-center\"\n        >\n          <div class=\"bg-muted mb-3 rounded-full p-3\">\n            <BellOff class=\"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 {{ activeFilter === 'unread' ? 'unread ' : '' }}notifications\n          </p>\n        </div>\n\n        <template v-else>\n          <template v-if=\"groupedNotifications.today.length > 0\">\n            <div class=\"px-4 pt-3 pb-1\">\n              <span class=\"text-muted-foreground text-[10px] font-semibold tracking-widest uppercase\">Today</span>\n            </div>\n            <div\n              v-for=\"(n, index) in groupedNotifications.today\"\n              :key=\"n.id\"\n              class=\"notification-item group relative\"\n              :style=\"{ animationDelay: `${index * 30}ms` }\"\n              @click=\"markAsRead(n.id)\"\n            >\n              <div\n                :class=\"[\n                  'absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full transition-opacity',\n                  !n.read ? categoryConfig[n.category].accent : 'opacity-0',\n                ]\"\n              />\n\n              <div class=\"flex gap-3 px-4 py-3\">\n                <div\n                  :class=\"[\n                    'mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg',\n                    categoryConfig[n.category].bg,\n                  ]\"\n                >\n                  <component :is=\"categoryConfig[n.category].icon\" class=\"size-4\" />\n                </div>\n\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex items-start justify-between gap-2\">\n                    <p :class=\"['text-[13px] leading-snug', !n.read ? 'font-semibold' : 'font-medium']\">\n                      {{ n.title }}\n                    </p>\n                    <div class=\"flex shrink-0 items-center gap-1.5\">\n                      <span class=\"text-muted-foreground text-[10px] whitespace-nowrap tabular-nums\">\n                        {{ formatTime(n.timestamp) }}\n                      </span>\n                      <span v-if=\"!n.read\" class=\"bg-primary size-1.5 shrink-0 rounded-full\" />\n                    </div>\n                  </div>\n                  <p class=\"text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed\">\n                    {{ n.body }}\n                  </p>\n                </div>\n\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.stop=\"dismissNotification(n.id)\"\n                >\n                  <X class=\"size-3.5\" />\n                </button>\n              </div>\n            </div>\n          </template>\n\n          <template v-if=\"groupedNotifications.earlier.length > 0\">\n            <div class=\"px-4 pt-3 pb-1\">\n              <span class=\"text-muted-foreground text-[10px] font-semibold tracking-widest uppercase\">Earlier</span>\n            </div>\n            <div\n              v-for=\"(n, index) in groupedNotifications.earlier\"\n              :key=\"n.id\"\n              class=\"notification-item group relative\"\n              :style=\"{ animationDelay: `${(groupedNotifications.today.length + index) * 30}ms` }\"\n              @click=\"markAsRead(n.id)\"\n            >\n              <div\n                :class=\"[\n                  'absolute top-2 bottom-2 left-0 w-[3px] rounded-r-full transition-opacity',\n                  !n.read ? categoryConfig[n.category].accent : 'opacity-0',\n                ]\"\n              />\n\n              <div class=\"flex gap-3 px-4 py-3\">\n                <div\n                  :class=\"[\n                    'mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg',\n                    categoryConfig[n.category].bg,\n                  ]\"\n                >\n                  <component :is=\"categoryConfig[n.category].icon\" class=\"size-4\" />\n                </div>\n\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex items-start justify-between gap-2\">\n                    <p :class=\"['text-[13px] leading-snug', !n.read ? 'font-semibold' : 'font-medium']\">\n                      {{ n.title }}\n                    </p>\n                    <div class=\"flex shrink-0 items-center gap-1.5\">\n                      <span class=\"text-muted-foreground text-[10px] whitespace-nowrap tabular-nums\">\n                        {{ formatTime(n.timestamp) }}\n                      </span>\n                      <span v-if=\"!n.read\" class=\"bg-primary size-1.5 shrink-0 rounded-full\" />\n                    </div>\n                  </div>\n                  <p class=\"text-muted-foreground mt-0.5 line-clamp-2 text-xs leading-relaxed\">\n                    {{ n.body }}\n                  </p>\n                </div>\n\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.stop=\"dismissNotification(n.id)\"\n                >\n                  <X class=\"size-3.5\" />\n                </button>\n              </div>\n            </div>\n          </template>\n        </template>\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=\"isOpen = false\"\n        >\n          <Archive class=\"size-3\" />\n          View all notifications\n        </a>\n      </div>\n    </PopoverContent>\n  </Popover>\n</template>\n\n<style scoped>\n.notification-panel {\n  --scrollbar-size: 4px;\n}\n\n.notification-scroll::-webkit-scrollbar {\n  width: var(--scrollbar-size);\n}\n\n.notification-scroll::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.notification-scroll::-webkit-scrollbar-thumb {\n  background: var(--border);\n  border-radius: 2px;\n}\n\n.notification-item {\n  cursor: pointer;\n  transition: background-color 0.15s ease;\n  animation: notif-slide-in 0.25s ease both;\n}\n\n.notification-item:hover {\n  background-color: var(--muted);\n}\n\n@keyframes notif-slide-in {\n  from {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.notification-scroll {\n  position: relative;\n}\n\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</style>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/NotificationsPopover.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/popover.json"
  ],
  "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": [
    "dashboard",
    "overlay"
  ]
}