{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "live-feed",
  "title": "Live Feed",
  "description": "Rolling activity stream for surfacing incidents, deploys, and operational signals in real time.",
  "dependencies": [
    "@vllnt/ui@^0.3.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/live-feed/live-feed.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { normalizeDate } from \"@vllnt/ui\";\nimport { useLiveDate } from \"@vllnt/ui\";\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@vllnt/ui\";\nimport {\n  SeverityBadge,\n  type SeverityBadgeLevel,\n} from \"@vllnt/ui\";\n\nexport type LiveFeedEvent = {\n  id: string;\n  message?: string;\n  severity: SeverityBadgeLevel;\n  source?: string;\n  timestamp: Date | number | string;\n  title: string;\n};\n\nexport type LiveFeedProps = React.ComponentPropsWithoutRef<\"div\"> & {\n  description?: string;\n  emptyLabel?: string;\n  events: LiveFeedEvent[];\n  maxItems?: number;\n  now?: Date | number | string;\n  tickMs?: number;\n  title?: string;\n};\n\nconst SECOND_MS = 1000;\nconst MINUTE_MS = 60 * SECOND_MS;\nconst HOUR_MS = 60 * MINUTE_MS;\nconst DAY_MS = 24 * HOUR_MS;\n\nfunction formatRelative(eventDate: Date, now: Date): string {\n  const deltaMs = now.getTime() - eventDate.getTime();\n\n  if (deltaMs < 5 * SECOND_MS) {\n    return \"just now\";\n  }\n\n  if (deltaMs < MINUTE_MS) {\n    return `${Math.floor(deltaMs / SECOND_MS)}s ago`;\n  }\n\n  if (deltaMs < HOUR_MS) {\n    return `${Math.floor(deltaMs / MINUTE_MS)}m ago`;\n  }\n\n  if (deltaMs < DAY_MS) {\n    return `${Math.floor(deltaMs / HOUR_MS)}h ago`;\n  }\n\n  if (deltaMs < 7 * DAY_MS) {\n    return `${Math.floor(deltaMs / DAY_MS)}d ago`;\n  }\n\n  return SHORT_DATE_FORMATTER.format(eventDate);\n}\n\nconst SHORT_DATE_FORMATTER = new Intl.DateTimeFormat(\"en-US\", {\n  day: \"numeric\",\n  month: \"short\",\n});\n\nconst ABSOLUTE_FORMATTER = new Intl.DateTimeFormat(\"en-US\", {\n  hour: \"numeric\",\n  minute: \"2-digit\",\n  month: \"short\",\n  second: \"2-digit\",\n});\n\nfunction formatAbsolute(eventDate: Date): string {\n  return ABSOLUTE_FORMATTER.format(eventDate);\n}\n\nfunction sortEventsDesc(events: LiveFeedEvent[]): LiveFeedEvent[] {\n  return [...events].sort(\n    (a, b) =>\n      normalizeDate(b.timestamp).getTime() -\n      normalizeDate(a.timestamp).getTime(),\n  );\n}\n\nfunction LiveFeedRow({\n  event,\n  isLatest,\n  now,\n}: {\n  event: LiveFeedEvent;\n  isLatest: boolean;\n  now: Date;\n}) {\n  const eventDate = normalizeDate(event.timestamp);\n\n  return (\n    <li className=\"flex gap-3 border-b border-border/60 py-3 last:border-b-0\">\n      <div className=\"pt-1\">\n        <SeverityBadge\n          level={event.severity}\n          pulse={isLatest ? event.severity === \"critical\" : undefined}\n          tone=\"soft\"\n        />\n      </div>\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex items-baseline justify-between gap-2\">\n          <p className=\"truncate text-sm font-medium\">{event.title}</p>\n          <time\n            className=\"whitespace-nowrap text-xs text-muted-foreground\"\n            dateTime={eventDate.toISOString()}\n            title={formatAbsolute(eventDate)}\n          >\n            {formatRelative(eventDate, now)}\n          </time>\n        </div>\n        {event.message ? (\n          <p className=\"mt-0.5 text-sm text-muted-foreground\">\n            {event.message}\n          </p>\n        ) : null}\n        {event.source ? (\n          <p className=\"mt-1 text-xs uppercase tracking-[0.14em] text-muted-foreground\">\n            {event.source}\n          </p>\n        ) : null}\n      </div>\n    </li>\n  );\n}\n\nexport const LiveFeed = ({\n  className,\n  description,\n  emptyLabel = \"No events yet\",\n  events,\n  maxItems = 50,\n  now,\n  ref,\n  tickMs = 30_000,\n  title = \"Live feed\",\n  ...props\n}: LiveFeedProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const liveNow = useLiveDate(now, tickMs);\n  const visibleEvents = React.useMemo(\n    () => sortEventsDesc(events).slice(0, maxItems),\n    [events, maxItems],\n  );\n\n  return (\n    <Card className={cn(\"shadow-sm\", className)} ref={ref} {...props}>\n      <CardHeader className=\"flex flex-row items-start justify-between gap-3 gap-y-0 pb-3\">\n        <div className=\"space-y-1\">\n          <CardTitle className=\"text-base\">{title}</CardTitle>\n          {description ? (\n            <CardDescription>{description}</CardDescription>\n          ) : null}\n        </div>\n        <Badge variant=\"outline\">\n          <span\n            aria-hidden=\"true\"\n            className=\"relative mr-1.5 inline-flex size-2\"\n          >\n            <span className=\"absolute inline-flex size-2 animate-ping rounded-full bg-emerald-500 opacity-70\" />\n            <span className=\"relative inline-flex size-2 rounded-full bg-emerald-500\" />\n          </span>{\" \"}\n          Live\n        </Badge>\n      </CardHeader>\n      <CardContent className=\"pt-0\">\n        {visibleEvents.length === 0 ? (\n          <div className=\"py-8 text-center text-sm text-muted-foreground\">\n            {emptyLabel}\n          </div>\n        ) : (\n          <ul className=\"max-h-[360px] divide-y divide-border/60 overflow-y-auto\">\n            {visibleEvents.map((event, index) => (\n              <LiveFeedRow\n                event={event}\n                isLatest={index === 0}\n                key={event.id}\n                now={liveNow}\n              />\n            ))}\n          </ul>\n        )}\n      </CardContent>\n    </Card>\n  );\n};\n\nLiveFeed.displayName = \"LiveFeed\";\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
