{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-feed",
  "type": "registry:ui",
  "title": "Activity feed",
  "description": "Preserves date groups and the unread boundary as live activity arrives.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "data-commerce"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/activity-feed/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "Live insertion feed with an unread boundary",
    "sceneFamily": "product-mono",
    "motionRole": "ui",
    "primaryState": "Live activity feed with an unread boundary",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/activity-feed.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/activity-feed.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nconst INSERT = { type: \"spring\", stiffness: 420, damping: 38, mass: 0.72 } as const;\nconst LEAVE = { duration: 0.14, ease: [0.23, 1, 0.32, 1] } as const;\nconst INSTANT = { duration: 0 } as const;\n\nexport type ActivityTone = \"neutral\" | \"success\" | \"warning\" | \"error\";\n\nexport type ActivityItem = {\n  id: string;\n  title: string;\n  description?: string;\n  time: string;\n  group?: string;\n  unread?: boolean;\n  tone?: ActivityTone;\n};\n\nexport type ActivityFeedProps = {\n  items: readonly ActivityItem[];\n  label?: string;\n  emptyLabel?: string;\n  unreadLabel?: string;\n  unreadStartLabel?: string;\n  toneLabels?: Partial<Record<ActivityTone, string>>;\n  onItemClick?: (item: ActivityItem) => void;\n  className?: string;\n};\n\nconst dotClass: Record<ActivityTone, string> = {\n  neutral: \"bg-neutral-400 dark:bg-neutral-500\",\n  success: \"bg-emerald-600\",\n  warning: \"bg-amber-500\",\n  error: \"bg-red-600\",\n};\n\nconst DEFAULT_TONE_LABELS: Record<ActivityTone, string> = {\n  neutral: \"\",\n  success: \"Success\",\n  warning: \"Warning\",\n  error: \"Error\",\n};\n\nexport function ActivityFeed({\n  items,\n  label = \"Activity\",\n  emptyLabel = \"No activity yet\",\n  unreadLabel = \"Unread\",\n  unreadStartLabel = \"Unread activity starts here\",\n  toneLabels: toneLabelOverrides,\n  onItemClick,\n  className = \"\",\n}: ActivityFeedProps) {\n  const toneLabels = { ...DEFAULT_TONE_LABELS, ...toneLabelOverrides };\n  const reduced = useReducedMotion() === true;\n  const previousIds = useRef<Set<string> | null>(null);\n  const [announcement, setAnnouncement] = useState<{ id: string; title: string } | null>(null);\n  let previousGroup: string | undefined;\n  let unreadDividerShown = false;\n\n  useEffect(() => {\n    const newest = items[0];\n    if (newest && previousIds.current && !previousIds.current.has(newest.id)) {\n      setAnnouncement({ id: newest.id, title: newest.title });\n    } else if (!newest) {\n      setAnnouncement(null);\n    }\n    previousIds.current = new Set(items.map((item) => item.id));\n  }, [items]);\n\n  if (items.length === 0) {\n    return (\n      <section aria-label={label} className={`w-full max-w-[430px] ${className}`}>\n        <div role=\"status\" className=\"grid min-h-28 place-items-center rounded-[10px] border border-neutral-200 bg-white px-4 text-center text-[12.5px] text-neutral-600 dark:border-white/[0.16] dark:bg-[#181818] dark:text-neutral-300\">\n          {emptyLabel}\n        </div>\n      </section>\n    );\n  }\n\n  return (\n    <section aria-label={label} className={`w-full max-w-[430px] ${className}`}>\n      <ol className=\"relative m-0 list-none p-0\">\n        <AnimatePresence initial={false}>\n          {items.map((item, index) => {\n            const showGroup = Boolean(item.group && item.group !== previousGroup);\n            previousGroup = item.group;\n            const showUnread = Boolean(item.unread && !unreadDividerShown);\n            if (showUnread) unreadDividerShown = true;\n            const content = (\n              <>\n                <span className=\"relative mt-[18px] grid size-3 shrink-0 place-items-center\" aria-hidden=\"true\">\n                  <span className={`size-2 rounded-[3px] ${dotClass[item.tone ?? \"neutral\"]}`} />\n                </span>\n                <span className=\"min-w-0 flex-1 py-2.5\">\n                  {toneLabels[item.tone ?? \"neutral\"] ? (\n                    <span className=\"sr-only\">{toneLabels[item.tone ?? \"neutral\"]}. </span>\n                  ) : null}\n                  <span className=\"flex items-baseline justify-between gap-3\">\n                    <strong className=\"truncate text-[12.5px] font-medium text-neutral-800 dark:text-neutral-100\">{item.title}</strong>\n                    <time className=\"max-w-[45%] shrink-0 truncate font-mono text-[10px] tabular-nums text-neutral-600 dark:text-neutral-400\">{item.time}</time>\n                  </span>\n                  {item.description ? <span className=\"mt-0.5 block text-[11.5px] leading-relaxed text-neutral-600 [overflow-wrap:anywhere] dark:text-neutral-400\">{item.description}</span> : null}\n                </span>\n              </>\n            );\n\n            return (\n              <motion.li\n                key={item.id}\n                layout={!reduced}\n                initial={reduced ? { opacity: 0 } : { opacity: 0, transform: \"translate3d(0, -10px, 0)\", filter: \"blur(3px)\" }}\n                animate={{ opacity: 1, transform: \"translate3d(0, 0, 0)\", filter: \"blur(0px)\" }}\n                exit={reduced ? { opacity: 0 } : { opacity: 0, transform: \"translate3d(12px, 0, 0)\", transition: LEAVE }}\n                transition={reduced ? INSTANT : { ...INSERT, delay: Math.min(index, 3) * 0.025 }}\n              >\n                {showGroup ? (\n                  <div className=\"pb-1 pt-2 text-[10.5px] font-medium text-neutral-600 [overflow-wrap:anywhere] dark:text-neutral-400\">{item.group}</div>\n                ) : null}\n                {showUnread ? (\n                  <div className=\"flex items-center gap-2 py-1\" aria-label={unreadStartLabel}>\n                    <span className=\"h-px flex-1 bg-neutral-200 dark:bg-white/10\" />\n                    <span className=\"text-[9.5px] font-medium text-neutral-500 dark:text-neutral-400\">{unreadLabel}</span>\n                    <span className=\"h-px flex-1 bg-neutral-200 dark:bg-white/10\" />\n                  </div>\n                ) : null}\n                <div className=\"relative\">\n                  {index < items.length - 1 ? <span aria-hidden=\"true\" className=\"absolute bottom-0 left-[13px] top-[30px] w-px bg-neutral-200 dark:bg-white/[0.12]\" /> : null}\n                  {onItemClick ? (\n                    <button\n                      type=\"button\"\n                      onClick={() => onItemClick(item)}\n                      className={`relative flex min-h-11 w-full gap-2.5 rounded-lg px-2 text-left outline-none transition-colors duration-150 hover:bg-neutral-100 focus-visible:bg-neutral-100 focus-visible:ring-2 focus-visible:ring-blue-600 dark:hover:bg-white/[0.07] dark:focus-visible:bg-white/[0.07] ${item.unread ? \"bg-neutral-100/80 dark:bg-white/[0.05]\" : \"\"}`}\n                    >\n                      {content}\n                    </button>\n                  ) : (\n                    <div className={`relative flex min-h-11 gap-2.5 rounded-lg px-2 ${item.unread ? \"bg-neutral-100/80 dark:bg-white/[0.05]\" : \"\"}`}>{content}</div>\n                  )}\n                </div>\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n      </ol>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n        {announcement ? <span key={announcement.id}>{announcement.title}</span> : null}\n      </span>\n    </section>\n  );\n}\n"
    }
  ]
}
