{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-center",
  "type": "registry:ui",
  "title": "Notification Center",
  "description": "Bell-icon trigger + dropdown inbox with per-item read/dismiss controls. Composes Button + Popover + Badge.",
  "dependencies": [
    "@saasflare/ui"
  ],
  "files": [
    {
      "path": "components/ui/notification-center.tsx",
      "type": "registry:ui",
      "content": "// @draft\n\"use client\"\n\n/**\n * @fileoverview Saasflare NotificationCenter — bell-icon trigger + dropdown\n * with a list of notifications and per-item read/dismiss controls.\n * @author Saasflare™\n *\n * Composes the existing Button + Popover + Badge primitives. Designed for\n * the \"in-app inbox\" pattern (unread badge on a header bell). Controlled\n * via the `items` array — saasflare-ui does NOT own the data store; the\n * consumer is expected to wire reads/dismisses through whichever state\n * layer they use (React state, react-query, RTK, etc.).\n *\n * @module packages/ui/components/ui/notification-center\n * @package ui\n * @layer core\n *\n * @example\n * <NotificationCenter\n *   items={[\n *     { id: \"1\", title: \"New comment\", description: \"Lina replied to your post\", timestamp: \"5m ago\", read: false },\n *     { id: \"2\", title: \"Build passed\", timestamp: \"1h ago\", read: true },\n *   ]}\n *   onMarkRead={(id) => api.read(id)}\n *   onMarkAllRead={() => api.readAll()}\n * />\n */\n\nimport { type ReactNode } from \"react\"\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\nimport { Badge } from \"@saasflare/ui\"\nimport { Button } from \"@saasflare/ui\"\nimport { CheckIcon } from \"@saasflare/ui\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@saasflare/ui\"\n\n/** A single notification item. */\nexport interface NotificationItem {\n    /** Stable unique id. */\n    id: string\n    /** Bold title line. */\n    title: ReactNode\n    /** Secondary body line. */\n    description?: ReactNode\n    /** Display timestamp (already formatted — e.g. \"5m ago\"). */\n    timestamp?: string\n    /** Whether the notification has been read. */\n    read?: boolean\n    /** Optional leading icon. */\n    icon?: ReactNode\n    /** Optional href — wraps the row in `<a>`. */\n    href?: string\n}\n\n/** Props for the NotificationCenter component. */\nexport interface NotificationCenterProps extends SaasflareComponentProps {\n    /** Notifications to display. */\n    items: NotificationItem[]\n    /** Header title in the popover. Default: `\"Notifications\"`. */\n    title?: string\n    /** Called when the user clicks a row (also passes the item). */\n    onItemClick?: (item: NotificationItem) => void\n    /** Called when \"Mark as read\" is tapped on a row. */\n    onMarkRead?: (id: string) => void\n    /** Called when \"Mark all as read\" is tapped. Hide the action by omitting this. */\n    onMarkAllRead?: () => void\n    /** Custom empty-state content. */\n    emptyState?: ReactNode\n    /** Maximum items rendered (older ones truncated). */\n    limit?: number\n    /** Additional class names on the trigger button. */\n    triggerClassName?: string\n    /** Additional class names on the popover content. */\n    contentClassName?: string\n}\n\nfunction BellIcon(props: React.SVGProps<SVGSVGElement>) {\n    return (\n        <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden=\"true\"\n            {...props}\n        >\n            <path d=\"M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9\" />\n            <path d=\"M10.3 21a1.94 1.94 0 0 0 3.4 0\" />\n        </svg>\n    )\n}\n\n/**\n * Bell trigger + dropdown inbox of notifications.\n *\n * @component\n * @layer core\n */\nexport function NotificationCenter({\n    items,\n    title = \"Notifications\",\n    onItemClick,\n    onMarkRead,\n    onMarkAllRead,\n    emptyState,\n    limit,\n    triggerClassName,\n    contentClassName,\n    surface,\n    radius,\n    animated,\n    iconWeight,\n}: NotificationCenterProps) {\n    const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n\n    const visible = typeof limit === \"number\" ? items.slice(0, limit) : items\n    const unreadCount = items.filter((i) => !i.read).length\n\n    return (\n        <Popover>\n            <PopoverTrigger asChild>\n                <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    intent=\"neutral\"\n                    size=\"icon\"\n                    aria-label={`${title}${unreadCount > 0 ? ` (${unreadCount} unread)` : \"\"}`}\n                    surface={sf.surface}\n                    radius={sf.radius}\n                    animated={sf.animated}\n                    iconWeight={sf.iconWeight}\n                    data-slot=\"notification-center-trigger\"\n                    className={cn(\"relative\", triggerClassName)}\n                >\n                    <BellIcon className=\"size-4\" />\n                    {unreadCount > 0 && (\n                        <span\n                            data-slot=\"notification-center-badge\"\n                            aria-hidden=\"true\"\n                            className=\"absolute right-1 top-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold leading-none text-destructive-foreground\"\n                        >\n                            {unreadCount > 99 ? \"99+\" : unreadCount}\n                        </span>\n                    )}\n                </Button>\n            </PopoverTrigger>\n            <PopoverContent\n                data-slot=\"notification-center-content\"\n                align=\"end\"\n                className={cn(\"w-80 p-0\", contentClassName)}\n            >\n                <div className=\"flex items-center justify-between gap-2 border-b px-3 py-2\">\n                    <div className=\"flex items-center gap-2\">\n                        <p className=\"text-sm font-semibold\">{title}</p>\n                        {unreadCount > 0 && (\n                            <Badge variant=\"soft\" intent=\"primary\" size=\"sm\">\n                                {unreadCount}\n                            </Badge>\n                        )}\n                    </div>\n                    {onMarkAllRead && unreadCount > 0 && (\n                        <button\n                            type=\"button\"\n                            data-slot=\"notification-center-mark-all\"\n                            onClick={onMarkAllRead}\n                            className=\"text-xs text-muted-foreground transition-colors hover:text-foreground\"\n                        >\n                            Mark all as read\n                        </button>\n                    )}\n                </div>\n                <div\n                    data-slot=\"notification-center-list\"\n                    className=\"max-h-96 overflow-y-auto\"\n                >\n                    {visible.length === 0 ? (\n                        <div\n                            data-slot=\"notification-center-empty\"\n                            className=\"px-4 py-8 text-center text-sm text-muted-foreground\"\n                        >\n                            {emptyState ?? \"You're all caught up.\"}\n                        </div>\n                    ) : (\n                        visible.map((item) => {\n                            const inner = (\n                                <>\n                                    {!item.read && (\n                                        <>\n                                            <span\n                                                aria-hidden=\"true\"\n                                                className=\"mt-1.5 size-1.5 shrink-0 rounded-full bg-primary\"\n                                            />\n                                            <span className=\"sr-only\">Unread</span>\n                                        </>\n                                    )}\n                                    {item.icon !== undefined && (\n                                        <span className=\"mt-0.5 flex shrink-0 items-center [&_svg]:size-4 text-muted-foreground\">\n                                            {item.icon}\n                                        </span>\n                                    )}\n                                    <div className=\"min-w-0 flex-1\">\n                                        <p className=\"truncate text-sm font-medium text-foreground\">\n                                            {item.title}\n                                        </p>\n                                        {item.description !== undefined && (\n                                            <p className=\"line-clamp-2 text-xs text-muted-foreground\">\n                                                {item.description}\n                                            </p>\n                                        )}\n                                        {item.timestamp && (\n                                            <p className=\"mt-0.5 text-[10px] uppercase tracking-wider text-muted-foreground/70\">\n                                                {item.timestamp}\n                                            </p>\n                                        )}\n                                    </div>\n                                    {!item.read && onMarkRead && (\n                                        <button\n                                            type=\"button\"\n                                            data-slot=\"notification-center-mark\"\n                                            onClick={(e) => {\n                                                e.preventDefault()\n                                                e.stopPropagation()\n                                                onMarkRead(item.id)\n                                            }}\n                                            aria-label=\"Mark as read\"\n                                            className=\"relative shrink-0 text-muted-foreground transition-colors hover:text-foreground\"\n                                        >\n                                            <CheckIcon weight={sf.iconWeight} className=\"size-3\" />\n                                        </button>\n                                    )}\n                                </>\n                            )\n\n                            const rowClass = cn(\n                                \"relative flex items-start gap-2 border-b px-3 py-2.5 last:border-b-0\",\n                                \"transition-colors hover:bg-accent/40\",\n                                !item.read && \"bg-primary/5\",\n                            )\n                            const onClick = () => onItemClick?.(item)\n                            const titleLabel =\n                                typeof item.title === \"string\" ? item.title : \"Notification\"\n                            const actionLabel = item.read ? titleLabel : `Unread: ${titleLabel}`\n\n                            /* Stretched-overlay pattern: the row action is a SIBLING\n                             * of the content (absolute inset-0), never its ancestor,\n                             * so the nested mark-as-read <button> stays valid HTML.\n                             * The positioned mark-as-read button paints above it. */\n                            const overlay = item.href ? (\n                                <a\n                                    href={item.href}\n                                    data-slot=\"notification-center-item-action\"\n                                    onClick={onClick}\n                                    aria-label={actionLabel}\n                                    className=\"absolute inset-0\"\n                                />\n                            ) : onItemClick ? (\n                                <button\n                                    type=\"button\"\n                                    data-slot=\"notification-center-item-action\"\n                                    onClick={onClick}\n                                    aria-label={actionLabel}\n                                    className=\"absolute inset-0 cursor-pointer\"\n                                />\n                            ) : null\n\n                            return (\n                                <div\n                                    key={item.id}\n                                    data-slot=\"notification-center-item\"\n                                    data-read={String(!!item.read)}\n                                    className={rowClass}\n                                >\n                                    {overlay}\n                                    {inner}\n                                </div>\n                            )\n                        })\n                    )}\n                </div>\n            </PopoverContent>\n        </Popover>\n    )\n}\n",
      "target": "components/ui/notification-center.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/notification-center.json"
  }
}
