{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-center",
  "type": "registry:block",
  "title": "Notification Center",
  "description": "Tabbed activity inbox with grouped notifications, unread state, avatars, status icons, and inline actions.",
  "dependencies": [
    "@remixicon/react@^4.9.0"
  ],
  "registryDependencies": [
    "avatar",
    "button",
    "cx",
    "globals",
    "segmented-control",
    "template-copy"
  ],
  "files": [
    {
      "path": "components/application/notification-center/notification-center.tsx",
      "content": "\"use client\";\n\nimport { useTemplateCopy } from \"@/components/foundations/template-copy/template-copy\";\n\nimport type { ComponentType, HTMLAttributes, Ref } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport {\n  RiCheckboxCircleFill,\n  RiErrorWarningFill,\n  RiInformationFill,\n  RiNotification3Fill,\n  RiNotificationOffLine,\n} from \"@remixicon/react\";\nimport { Avatar, type AvatarProps } from \"@/components/base/avatar/avatar\";\nimport { Button, type ButtonProps } from \"@/components/base/buttons/button\";\nimport {\n  SegmentedControl,\n  SegmentedControlItem,\n} from \"@/components/base/segmented-control/segmented-control\";\nimport { cx, sortCx } from \"@/utils/cx\";\n\nexport type NotificationCenterTab = \"all\" | \"mentions\" | \"system\";\nexport type NotificationCenterCategory = Exclude<NotificationCenterTab, \"all\"> | \"activity\";\nexport type NotificationCenterStatus = \"neutral\" | \"information\" | \"success\" | \"error\";\n\ntype IconComponent = ComponentType<{\n  className?: string;\n  \"aria-hidden\"?: boolean | \"true\" | \"false\";\n}>;\n\nexport type NotificationCenterAction = {\n  id: string;\n  label: string;\n  variant?: ButtonProps[\"variant\"];\n};\n\nexport type NotificationCenterItem = {\n  id: string;\n  category: NotificationCenterCategory;\n  group: string;\n  title: string;\n  description: string;\n  timestamp: string;\n  unread?: boolean;\n  status?: NotificationCenterStatus;\n  icon?: IconComponent;\n  avatar?: Pick<AvatarProps, \"src\" | \"alt\" | \"initials\" | \"color\">;\n  actions?: NotificationCenterAction[];\n};\n\nexport interface NotificationCenterProps\n  extends Omit<HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  notifications: NotificationCenterItem[];\n  defaultTab?: NotificationCenterTab;\n  tab?: NotificationCenterTab;\n  onTabChange?: (tab: NotificationCenterTab) => void;\n  onAction?: (notificationId: string, actionId: string) => void;\n  title?: string;\n  emptyMessage?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst STATUS_ICON: Record<NotificationCenterStatus, IconComponent> = {\n  neutral: RiNotification3Fill,\n  information: RiInformationFill,\n  success: RiCheckboxCircleFill,\n  error: RiErrorWarningFill,\n};\n\nconst styles = sortCx({\n  status: {\n    neutral: \"bg-background-tertiary-default text-text-secondary\",\n    information:\n      \"bg-notification-information-background text-notification-information-foreground\",\n    success: \"bg-notification-success-background text-notification-success-foreground\",\n    error: \"bg-notification-error-background text-notification-error-foreground\",\n  },\n});\n\nconst TABS: { id: NotificationCenterTab; label: string }[] = [\n  { id: \"all\", label: \"All\" },\n  { id: \"mentions\", label: \"Mentions\" },\n  { id: \"system\", label: \"System\" },\n];\n\nfunction NotificationVisual({ item }: { item: NotificationCenterItem }) {\n  const localize = useTemplateCopy();\n  if (item.avatar) {\n    return localize(<Avatar {...item.avatar} size=\"lg\" className=\"size-10\" />);\n  }\n\n  const status = item.status ?? \"neutral\";\n  const Icon = item.icon ?? STATUS_ICON[status];\n\n  return localize((\n    <span\n      className={cx(\n        \"flex size-10 shrink-0 items-center justify-center rounded-full\",\n        styles.status[status],\n      )}\n    >\n      <Icon className=\"size-5\" aria-hidden />\n    </span>\n  ));\n}\n\nfunction TabLabel({\n  label,\n  count,\n}: {\n  label: string;\n  count: number;\n}) {\n  const localize = useTemplateCopy();\n  return localize((\n    <span className=\"inline-flex items-center gap-1.5\">\n      {label}\n      <span className=\"inline-flex min-w-5 items-center justify-center rounded-sm bg-badge-neutral-background px-1 py-px text-caption-1-medium text-text-secondary\">\n        {count}\n      </span>\n    </span>\n  ));\n}\n\nexport function NotificationCenter({\n  notifications,\n  defaultTab = \"all\",\n  tab,\n  onTabChange,\n  onAction,\n  title = \"Notifications\",\n  emptyMessage = \"You’re all caught up.\",\n  className,\n  ref,\n  ...props\n}: NotificationCenterProps) {\n  const localize = useTemplateCopy();\n  const [internalTab, setInternalTab] = useState<NotificationCenterTab>(defaultTab);\n  const [readIds, setReadIds] = useState<Set<string>>(() => new Set());\n  const activeTab = tab ?? internalTab;\n\n  const setTab = (nextTab: NotificationCenterTab) => {\n    if (tab === undefined) setInternalTab(nextTab);\n    onTabChange?.(nextTab);\n  };\n\n  const isUnread = (item: NotificationCenterItem) => item.unread === true && !readIds.has(item.id);\n  const unreadCount = notifications.filter(isUnread).length;\n\n  const tabCounts = useMemo(\n    () => ({\n      all: notifications.length,\n      mentions: notifications.filter((item) => item.category === \"mentions\").length,\n      system: notifications.filter((item) => item.category === \"system\").length,\n    }),\n    [notifications],\n  );\n\n  const visibleNotifications = useMemo(\n    () =>\n      activeTab === \"all\"\n        ? notifications\n        : notifications.filter((item) => item.category === activeTab),\n    [activeTab, notifications],\n  );\n\n  const markAllRead = () => {\n    setReadIds(new Set(notifications.filter((item) => item.unread).map((item) => item.id)));\n  };\n\n  return localize((\n    <section\n      ref={ref}\n      aria-label={title}\n      className={cx(\n        \"flex w-full max-w-[430px] flex-col overflow-hidden rounded-3xl border border-border-button-default bg-notification-center-background shadow-dropdown\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex flex-col gap-3 p-4 pb-1.5\">\n        <div className=\"flex items-start justify-between gap-4\">\n          <div className=\"flex min-w-0 flex-col gap-0.5\">\n            <h2 className=\"text-title-3-medium text-text-primary\">{title}</h2>\n            <p className=\"text-body-regular text-text-secondary\">\n              {unreadCount === 0 ? \"No unread notifications\" : `${unreadCount} unread`}\n            </p>\n          </div>\n          <Button\n            variant=\"ghost\"\n            size=\"small\"\n            onClick={markAllRead}\n            disabled={unreadCount === 0}\n          >\n            Mark all read\n          </Button>\n        </div>\n\n        <SegmentedControl\n          aria-label=\"Notification category\"\n          selectedKeys={[activeTab]}\n          onSelectionChange={(keys) => {\n            const next = [...(keys as Set<string>)][0] as NotificationCenterTab | undefined;\n            if (next) setTab(next);\n          }}\n          className=\"flex w-full\"\n        >\n          {TABS.map(({ id, label }) => (\n            <SegmentedControlItem\n              key={id}\n              id={id}\n              className=\"flex-1\"\n            >\n              <TabLabel label={label} count={tabCounts[id]} />\n            </SegmentedControlItem>\n          ))}\n        </SegmentedControl>\n      </div>\n\n      <div className=\"bg-notification-center-background p-1.5\">\n        <div className=\"overflow-hidden rounded-2xl bg-background-secondary-default\">\n          <div className=\"max-h-[516px] overflow-y-auto overscroll-contain bg-background-secondary-default p-2\">\n            {visibleNotifications.length === 0 ? (\n              <div className=\"flex min-h-64 flex-col items-center justify-center gap-2 rounded-2xl bg-background-primary-default px-6 text-center\">\n                <span className=\"flex size-11 items-center justify-center rounded-full bg-background-secondary-default text-foreground-icon-secondary\">\n                  <RiNotificationOffLine className=\"size-5\" aria-hidden />\n                </span>\n                <p className=\"text-body-medium text-text-primary\">{emptyMessage}</p>\n                <p className=\"max-w-64 text-body-regular text-text-secondary\">\n                  New activity will appear here when it arrives.\n                </p>\n              </div>\n            ) : (\n              <div className=\"flex flex-col gap-2\">\n                {visibleNotifications.map((item) => {\n                  const unread = isUnread(item);\n                  return (\n                    <article\n                      key={item.id}\n                      className=\"group/item relative flex gap-3 rounded-notification-card bg-background-primary-default px-3 py-3\"\n                    >\n                      <NotificationVisual item={item} />\n                      <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n                        <div className=\"flex min-w-0 items-start justify-between gap-3\">\n                          <p className=\"min-w-0 text-body-medium text-text-primary\">{item.title}</p>\n                          <div className=\"flex shrink-0 items-center gap-2\">\n                            <span className=\"text-caption-1-medium whitespace-nowrap text-text-tertiary\">\n                              {item.timestamp}\n                            </span>\n                            {unread ? (\n                              <span\n                                className=\"size-2 rounded-full bg-accent-500\"\n                                aria-label=\"Unread\"\n                              />\n                            ) : null}\n                          </div>\n                        </div>\n                        <p className=\"text-body-regular text-text-secondary\">{item.description}</p>\n                        {item.actions?.length ? (\n                          <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n                            {item.actions.map((action) => (\n                              <Button\n                                key={action.id}\n                                size=\"small\"\n                                variant={action.variant ?? \"secondary\"}\n                                onClick={() => {\n                                  setReadIds((current) => new Set(current).add(item.id));\n                                  onAction?.(item.id, action.id);\n                                }}\n                              >\n                                {action.label}\n                              </Button>\n                            ))}\n                          </div>\n                        ) : null}\n                      </div>\n                    </article>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </section>\n  ));\n}\n",
      "type": "registry:component",
      "target": "components/application/notification-center/notification-center.tsx"
    }
  ],
  "meta": {
    "docsUrl": "https://www.boardui.com/components/notification-center",
    "example": "\"use client\";\n\nimport { NotificationCenter } from \"@/components/application/notification-center/notification-center\";\n\nconst notifications = [\n  {\n    id: \"mention-notes\",\n    category: \"mentions\",\n    group: \"Today\",\n    title: \"Livia mentioned you\",\n    description: \"Can you review the new empty state before we ship?\",\n    timestamp: \"2m\",\n    unread: true,\n    avatar: { src: \"/avatars/livia-saris.webp\", alt: \"Livia Saris\" },\n    actions: [\n      { id: \"reply\", label: \"Reply\", variant: \"primary\" },\n      { id: \"view\", label: \"View thread\", variant: \"secondary\" },\n    ],\n  },\n  {\n    id: \"backup-ready\",\n    category: \"system\",\n    group: \"Today\",\n    title: \"Workspace backup is ready\",\n    description: \"The latest backup finished successfully.\",\n    timestamp: \"18m\",\n    unread: true,\n    status: \"success\",\n  },\n];\n\nexport function Example() {\n  return (\n    <NotificationCenter\n      notifications={notifications}\n      onAction={(notificationId, actionId) => {\n        console.log(notificationId, actionId);\n      }}\n    />\n  );\n}",
    "snippets": [
      {
        "label": "filtering",
        "code": "<NotificationCenter\n  notifications={notifications}\n  defaultTab=\"mentions\"\n/>\n\n{/* Controlled filtering */}\n<NotificationCenter\n  notifications={notifications}\n  tab={activeTab}\n  onTabChange={setActiveTab}\n/>"
      },
      {
        "label": "data",
        "code": "const notification = {\n  id: \"deploy-failed\",\n  category: \"system\", // mentions | system | activity\n  group: \"Earlier this week\",\n  title: \"Preview deployment failed\",\n  description: \"The build stopped while validating routes.\",\n  timestamp: \"Sat\",\n  unread: true,\n  status: \"error\", // neutral | information | success | error\n  actions: [\n    { id: \"retry\", label: \"Retry\", variant: \"primary\" },\n    { id: \"logs\", label: \"View logs\", variant: \"secondary\" },\n  ],\n};"
      }
    ]
  }
}
