{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "activity-feed",
  "title": "ActivityFeed",
  "description": "A chronological feed of system events (provision, transfer, agent) on a status-vocabulary timeline, with expandable detail and optional per-entry actions.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/skeleton.json",
    "https://ui.neon.com/r/empty-state.json",
    "https://ui.neon.com/r/neon-tokens.json"
  ],
  "files": [
    {
      "path": "src/components/activity-feed/activity-feed.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowDown01Icon,\n  Copy01Icon,\n  Tick02Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { EmptyState } from \"@/components/empty-state/empty-state\";\nimport { Button } from \"@/components/ui/button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\n/** The event's outcome, drawn from the house status vocabulary. */\nexport type ActivityStatus = \"success\" | \"error\" | \"pending\" | \"info\";\n\nexport interface ActivityEntry {\n  id: string;\n  status: ActivityStatus;\n  /** One-line summary, e.g. \"Provisioned production database\". */\n  title: string;\n  /** Display-ready relative time, e.g. \"2m ago\". */\n  timestamp: string;\n  /** Who or what caused it, e.g. \"agent\" or a user name. */\n  source?: string;\n  /** Expandable long detail: an error message, request id, or result. */\n  detail?: string;\n  /** When set, renders an action (e.g. \"Retry\") that fires onAction. */\n  actionLabel?: string;\n}\n\n/* Marker color per status; alive states breathe on the shared cadence. */\nconst MARKER: Record<ActivityStatus, string> = {\n  error: \"bg-destructive\",\n  info: \"bg-muted-foreground/50\",\n  pending:\n    \"neon-status-breathe bg-current text-[var(--status-scaling)] motion-reduce:animate-none\",\n  success: \"bg-primary\",\n};\n\nconst ROW_STAGGER_MS = 60;\nconst COPY_FLASH_MS = 1500;\n\n/** Copies the detail text; the icon flashes a primary check. */\nconst CopyButton = ({ value }: { value: string }) => {\n  const [copied, setCopied] = useState(false);\n\n  const copy = async () => {\n    setCopied(true);\n    window.setTimeout(() => setCopied(false), COPY_FLASH_MS);\n\n    try {\n      await navigator.clipboard.writeText(value);\n    } catch {\n      // Clipboard unavailable; the check still flashes.\n    }\n  };\n\n  return (\n    <button\n      aria-label={copied ? \"Copied\" : \"Copy details\"}\n      className=\"absolute top-1 right-1 inline-flex size-6 items-center justify-center rounded-md text-muted-foreground/50 outline-none transition-colors hover:bg-muted/40 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\"\n      onClick={copy}\n      type=\"button\"\n    >\n      <HugeiconsIcon\n        className={cn(\"size-3 transition-colors\", copied && \"text-primary\")}\n        icon={copied ? Tick02Icon : Copy01Icon}\n        strokeWidth={2}\n      />\n    </button>\n  );\n};\nconst RISE =\n  \"fill-mode-backwards fade-in-0 slide-in-from-bottom-2 animate-in duration-500 motion-reduce:animate-none\";\n\nconst ActivityRow = ({\n  entry,\n  index,\n  onAction,\n}: {\n  entry: ActivityEntry;\n  index: number;\n  onAction?: (entry: ActivityEntry) => void;\n}) => {\n  const [expanded, setExpanded] = useState(false);\n\n  return (\n    <li\n      className={cn(\n        \"group relative flex items-start gap-3 pb-6 pl-6 last:pb-0\",\n        RISE\n      )}\n      data-slot=\"activity-row\"\n      data-status={entry.status}\n      style={{ animationDelay: `${index * ROW_STAGGER_MS}ms` }}\n    >\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"absolute top-[7px] left-[-2.5px] size-1.5 shrink-0\",\n          MARKER[entry.status]\n        )}\n        data-slot=\"activity-marker\"\n      />\n\n      <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n        <div className=\"flex items-baseline gap-2.5\">\n          <p\n            className=\"truncate font-medium text-foreground text-sm\"\n            title={entry.title}\n          >\n            {entry.title}\n          </p>\n          <span className=\"ml-auto shrink-0 font-mono text-[10px] text-muted-foreground/70 tabular-nums\">\n            {entry.timestamp}\n          </span>\n        </div>\n\n        {entry.source ? (\n          <p className=\"font-mono text-[10px] text-muted-foreground/70\">\n            {entry.source}\n          </p>\n        ) : null}\n\n        {entry.detail ? (\n          <div className=\"mt-0.5\">\n            <button\n              aria-expanded={expanded}\n              className=\"inline-flex items-center gap-1 font-mono text-[10px] text-muted-foreground/70 outline-none transition-colors hover:text-foreground focus-visible:text-foreground\"\n              onClick={() => setExpanded((current) => !current)}\n              type=\"button\"\n            >\n              <HugeiconsIcon\n                className={cn(\n                  \"size-3 transition-transform duration-200\",\n                  expanded && \"rotate-180\"\n                )}\n                icon={ArrowDown01Icon}\n                strokeWidth={2}\n              />\n              {expanded ? \"Hide details\" : \"Details\"}\n            </button>\n            {expanded ? (\n              <div className=\"fade-in-0 slide-in-from-top-1 relative mt-1.5 animate-in rounded-md border border-border/60 bg-background duration-200 motion-reduce:animate-none\">\n                <CopyButton value={entry.detail} />\n                <div className=\"max-h-44 overflow-auto whitespace-pre-wrap p-2.5 pr-9 font-mono text-[10px] text-muted-foreground leading-relaxed\">\n                  {entry.detail}\n                </div>\n              </div>\n            ) : null}\n          </div>\n        ) : null}\n\n        {entry.actionLabel && onAction ? (\n          <div className=\"mt-0.5\">\n            <Button\n              className=\"h-6 rounded-full border border-border/60 px-2.5 text-muted-foreground text-xs transition-colors hover:border-border hover:bg-transparent hover:text-foreground active:scale-[0.98]\"\n              onClick={() => onAction(entry)}\n              size=\"sm\"\n              variant=\"ghost\"\n            >\n              {entry.actionLabel}\n            </Button>\n          </div>\n        ) : null}\n      </div>\n    </li>\n  );\n};\n\nexport type ActivityFeedProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  /** Events, newest first. The top row reads as \"now\". */\n  entries: ActivityEntry[];\n  /** Card title. */\n  label?: string;\n  /** Fires when an entry's action is used. */\n  onAction?: (entry: ActivityEntry) => void;\n  /** Override the built-in empty state. */\n  empty?: ReactNode;\n  isLoading?: boolean;\n  error?: Error | string | null;\n};\n\nconst cardClassName =\n  \"flex w-full min-w-0 flex-col rounded-lg border border-border/60 bg-card p-4 shadow-none ring-0\";\n\nconst Header = ({ label, count }: { label: string; count?: number }) => (\n  <div className=\"flex items-center justify-between gap-3\">\n    <p className=\"font-medium text-foreground text-sm\">{label}</p>\n    {count === undefined ? null : (\n      <span className=\"font-mono text-[10px] text-muted-foreground/60 tabular-nums\">\n        {count}\n      </span>\n    )}\n  </div>\n);\n\nexport const ActivityFeed = ({\n  entries,\n  label = \"Activity\",\n  onAction,\n  empty,\n  isLoading = false,\n  error = null,\n  className,\n  ...props\n}: ActivityFeedProps) => {\n  if (isLoading) {\n    return (\n      <div\n        aria-busy=\"true\"\n        className={cn(cardClassName, className)}\n        data-slot=\"activity-feed\"\n        {...props}\n      >\n        <Header label={label} />\n        <div className=\"mt-4 space-y-4\">\n          <Skeleton aria-hidden=\"true\" className=\"h-8 w-full\" />\n          <Skeleton aria-hidden=\"true\" className=\"h-8 w-full\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (error) {\n    const message = typeof error === \"string\" ? error : error.message;\n\n    return (\n      <div\n        className={cn(cardClassName, className)}\n        data-slot=\"activity-feed\"\n        role=\"alert\"\n        {...props}\n      >\n        <Header label={label} />\n        <p className=\"mt-3 flex items-baseline gap-2 text-sm\">\n          <span className=\"font-mono text-destructive text-xs\">error</span>\n          <span className=\"text-foreground\">{message}</span>\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={cn(cardClassName, className)}\n      data-slot=\"activity-feed\"\n      {...props}\n    >\n      <Header count={entries.length} label={label} />\n\n      {entries.length === 0 ? (\n        <div className=\"mt-3\">\n          {empty ?? (\n            <EmptyState\n              description=\"System events land here as your app provisions, transfers, and runs.\"\n              title=\"No activity yet\"\n            />\n          )}\n        </div>\n      ) : (\n        <ol className=\"relative mt-4 flex flex-col\">\n          <span\n            aria-hidden=\"true\"\n            className=\"absolute top-2 bottom-3 left-0 w-px bg-border\"\n          />\n          {entries.map((entry, index) => (\n            <ActivityRow\n              entry={entry}\n              index={index}\n              key={entry.id}\n              onAction={onAction}\n            />\n          ))}\n        </ol>\n      )}\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}