{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "notification-stack",
  "title": "Notification stack",
  "description": "Notifications folded into a neat pile that fans out into a list, each one dismissable.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/notification-stack/notification-stack.tsx",
      "content": "import { ChevronDown, X } from 'lucide-react'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport { useId, useState, type ReactNode } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type StackedNotification = {\n  id: string\n  title: string\n  body?: string\n  /** Already formatted, e.g. “2 min ago”. */\n  time?: string\n  icon?: ReactNode\n}\n\nexport type NotificationStackProps = {\n  items: StackedNotification[]\n  onDismiss?: (id: string) => void\n  labels?: Partial<Record<'title' | 'show' | 'hide' | 'dismiss' | 'empty', string>>\n  className?: string\n}\n\nconst EN = { title: 'Notifications', show: 'Show all', hide: 'Show less', dismiss: 'Dismiss', empty: 'All caught up' }\nconst PEEK = 3 // cards visible behind the first one when folded\n\n/** Notifications folded into a neat pile that fans out into a list when opened. */\nexport function NotificationStack({ items, onDismiss, labels = {}, className }: NotificationStackProps) {\n  const t = { ...EN, ...labels }\n  const [open, setOpen] = useState(false)\n  const list = useId()\n  const folded = !open && items.length > 1\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={{ type: 'spring', stiffness: 380, damping: 38 }}>\n      <section aria-label={t.title} className={cn('w-full min-w-0 max-w-sm', className)}>\n        <header className=\"mb-2 flex min-h-11 items-center justify-between px-1\">\n          <h2 className=\"text-sm font-semibold\">\n            {t.title} <span className=\"font-normal text-muted-foreground tabular-nums\">{items.length}</span>\n          </h2>\n          {items.length > 1 && (\n            <button\n              type=\"button\"\n              aria-expanded={open}\n              aria-controls={list}\n              onClick={() => setOpen(!open)}\n              className=\"inline-flex min-h-10 items-center gap-1 rounded-full px-3 text-sm text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n            >\n              {open ? t.hide : t.show}\n              <ChevronDown className={cn('size-4 transition-transform duration-200', open && 'rotate-180')} aria-hidden />\n            </button>\n          )}\n        </header>\n\n        {items.length === 0 ? (\n          <p className=\"rounded-2xl border border-dashed px-4 py-6 text-center text-sm text-muted-foreground\">{t.empty}</p>\n        ) : (\n          // Folded, every card sits in the same grid cell; open, they become a column. Layout animation moves between the two.\n          <motion.ol id={list} layout className={cn('relative', folded ? 'grid pb-[22px]' : 'flex flex-col gap-2')}>\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              {items.map((n, i) => {\n                const hidden = folded && i > PEEK - 1\n                return (\n                  <motion.li\n                    key={n.id}\n                    layout\n                    initial={{ opacity: 0, y: -8 }}\n                    animate={{ opacity: hidden ? 0 : 1, y: folded ? i * 11 : 0, scale: folded ? 1 - i * 0.05 : 1 }}\n                    exit={{ opacity: 0, scale: 0.96 }}\n                    aria-hidden={folded && i > 0 ? true : undefined}\n                    style={{ zIndex: items.length - i, borderRadius: 16, gridArea: folded ? '1 / 1' : undefined, transformOrigin: 'top center' }}\n                    className={cn('flex items-start gap-3 border bg-card p-3 text-card-foreground shadow-sm', hidden && 'pointer-events-none')}\n                  >\n                    {n.icon && <span className=\"grid size-9 shrink-0 place-items-center rounded-xl bg-muted [&_svg]:size-4\">{n.icon}</span>}\n                    <motion.div layout=\"position\" className=\"min-w-0 flex-1\">\n                      <p className=\"flex items-baseline justify-between gap-2\">\n                        <span className=\"truncate text-sm font-medium\">{n.title}</span>\n                        {n.time && <span className=\"shrink-0 text-xs text-muted-foreground\">{n.time}</span>}\n                      </p>\n                      {n.body && <p className={cn('mt-0.5 text-sm text-muted-foreground', folded ? 'line-clamp-1' : 'line-clamp-2')}>{n.body}</p>}\n                    </motion.div>\n                    {onDismiss && (!folded || i === 0) && (\n                      <button\n                        type=\"button\"\n                        aria-label={`${t.dismiss}: ${n.title}`}\n                        onClick={() => onDismiss(n.id)}\n                        className=\"-mt-1 -mr-1 grid size-9 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors duration-150 hover:bg-muted hover:text-foreground\"\n                      >\n                        <X className=\"size-4\" aria-hidden />\n                      </button>\n                    )}\n                  </motion.li>\n                )\n              })}\n            </AnimatePresence>\n          </motion.ol>\n        )}\n      </section>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "overlays"
  ],
  "type": "registry:component"
}