{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-activity",
  "title": "Agent Activity",
  "description": "A collapsible agent-run timeline with streaming steps, sources, details, and inline visual artifacts.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/trovecn/ai-workbench/agent-activity.tsx",
      "content": "\"use client\";\n\nimport { useId, useState, type ComponentType, type ReactNode } from \"react\";\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  CircleIcon,\n  FileTextIcon,\n  GlobeIcon,\n  ImageIcon,\n  SearchIcon,\n  SquareTerminalIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { spring } from \"@/lib/springs\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AgentActivityStatus = \"pending\" | \"active\" | \"complete\";\n\nexport type AgentActivityIcon = \"thinking\" | \"reasoning\" | \"search\" | \"tool\" | \"image\";\n\nexport interface AgentActivityImage {\n  src: string;\n  alt: string;\n  caption?: ReactNode;\n}\n\nexport interface AgentActivityDetails {\n  summary: ReactNode;\n  items: readonly ReactNode[];\n  defaultOpen?: boolean;\n}\n\nexport interface AgentActivityEntry {\n  id: string;\n  /** The completed label shown after the step resolves. */\n  label: ReactNode;\n  /** Optional in-progress label, such as \"Reading…\" before \"Read…\". */\n  activeLabel?: ReactNode;\n  status: AgentActivityStatus;\n  /** Marks a description that changes token by token while its step is active. */\n  isStreamingText?: boolean;\n  /** A compact secondary line, including text that may update while streaming. */\n  description?: ReactNode;\n  icon?: AgentActivityIcon;\n  /** Defaults to an understated dot when false. */\n  showIcon?: boolean;\n  sources?: readonly ReactNode[];\n  details?: AgentActivityDetails;\n  image?: AgentActivityImage;\n}\n\nexport interface AgentActivityProps {\n  entries: readonly AgentActivityEntry[];\n  /** The interactive run label; it remains the stable anchor while steps arrive. */\n  title?: ReactNode;\n  defaultOpen?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\nconst ICONS: Record<AgentActivityIcon, ComponentType<{ className?: string }>> = {\n  thinking: CircleIcon,\n  reasoning: FileTextIcon,\n  search: SearchIcon,\n  tool: SquareTerminalIcon,\n  image: ImageIcon,\n};\n\nconst railDrawTransition = { ...spring.moderate.enter, bounce: 0.06 };\n// Output is append-only trace data, so it should simply arrive in place rather\n// than competing with the structural step and rail motion.\nconst evidenceRevealTransition = spring.quick.exit;\nconst reducedFadeTransition = spring.quick.exit;\n\nfunction AgentActivityDetails({ details }: { details: AgentActivityDetails }) {\n  const [isOpen, setIsOpen] = useState(details.defaultOpen ?? false);\n  const id = useId();\n  const reduceMotion = useReducedMotion();\n\n  return (\n    <div className=\"mt-1.5\">\n      <button\n        type=\"button\"\n        aria-expanded={isOpen}\n        aria-controls={id}\n        onClick={() => setIsOpen((open) => !open)}\n        className=\"group inline-flex items-center gap-1 text-caption text-muted-foreground transition-colors duration-quick hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <span>{details.summary}</span>\n        <motion.span\n          animate={{ transform: `rotate(${isOpen ? 180 : 0}deg)` }}\n          transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n        >\n          <ChevronDownIcon className=\"size-3\" />\n        </motion.span>\n      </button>\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.div\n            id={id}\n            initial={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            animate={reduceMotion ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n            exit={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            transition={reduceMotion ? reducedFadeTransition : spring.moderate.enter}\n            className=\"overflow-hidden\"\n          >\n            <div className=\"mt-1 flex flex-col gap-1 pl-2.5 text-meta text-muted-foreground\">\n              {details.items.map((item, index) => (\n                <span key={index}>{item}</span>\n              ))}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction AgentActivityStep({\n  entry,\n  hasFollowingStep,\n}: {\n  entry: AgentActivityEntry;\n  hasFollowingStep: boolean;\n}) {\n  const reduceMotion = useReducedMotion();\n  const Icon = entry.icon ? ICONS[entry.icon] : CircleIcon;\n  const isActive = entry.status === \"active\";\n  const displayLabel = isActive ? (entry.activeLabel ?? entry.label) : entry.label;\n  const hasLiveDescription = isActive && entry.isStreamingText && Boolean(entry.description);\n  const hasSupportingContent = Boolean(\n    entry.description || entry.sources?.length || entry.details || entry.image,\n  );\n\n  if (entry.status === \"pending\") return null;\n\n  return (\n    <motion.li\n      initial={reduceMotion ? { opacity: 0 } : hasLiveDescription ? { opacity: 0 } : { height: 0 }}\n      animate={\n        reduceMotion ? { opacity: 1 } : hasLiveDescription ? { opacity: 1 } : { height: \"auto\" }\n      }\n      transition={reduceMotion ? reducedFadeTransition : spring.slow.enter}\n      className={cn(\"relative\", !hasLiveDescription && \"overflow-hidden\")}\n    >\n      <motion.div\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1 }}\n        transition={reduceMotion ? reducedFadeTransition : { ...spring.quick.enter, delay: 0.06 }}\n        className=\"flex gap-3\"\n      >\n        <div className=\"flex w-4 shrink-0 flex-col items-center\">\n          <span className=\"mt-0.5 flex size-4 items-center justify-center text-muted-foreground\">\n            {entry.status === \"complete\" ? (\n              <CheckIcon className=\"size-3.5\" />\n            ) : entry.showIcon === false ? (\n              <span className=\"size-1.5 rounded-full bg-muted-foreground\" />\n            ) : (\n              <Icon className=\"size-3.5\" />\n            )}\n          </span>\n          {hasSupportingContent && hasFollowingStep ? (\n            <motion.span\n              initial={\n                reduceMotion\n                  ? { opacity: 0, transform: \"scaleY(1)\" }\n                  : { opacity: 0, transform: \"scaleY(0)\" }\n              }\n              animate={{ opacity: 1, transform: \"scaleY(1)\" }}\n              transition={reduceMotion ? reducedFadeTransition : railDrawTransition}\n              style={{ transformOrigin: \"top\" }}\n              className=\"my-1 w-px flex-1 bg-border/60\"\n              aria-hidden=\"true\"\n            />\n          ) : null}\n        </div>\n\n        <div className=\"min-w-0 flex-1 pb-3\">\n          <div\n            className={cn(\n              \"text-control leading-snug text-foreground\",\n              isActive && !reduceMotion && \"agent-activity-shimmer\",\n            )}\n          >\n            {displayLabel}\n            {isActive ? <span aria-hidden=\"true\">…</span> : null}\n          </div>\n          {entry.description ? (\n            entry.isStreamingText ? (\n              <div className=\"mt-1 text-body leading-relaxed text-muted-foreground\">\n                {entry.description}\n              </div>\n            ) : (\n              <motion.div\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                transition={reduceMotion ? reducedFadeTransition : evidenceRevealTransition}\n                className=\"mt-1 text-body leading-relaxed text-muted-foreground\"\n              >\n                {entry.description}\n              </motion.div>\n            )\n          ) : null}\n          {entry.sources && entry.sources.length > 0 ? (\n            <div className=\"mt-2 flex flex-wrap gap-1.5\">\n              {entry.sources.map((source, index) => (\n                <motion.span\n                  key={index}\n                  initial={{ opacity: 0 }}\n                  animate={{ opacity: 1 }}\n                  transition={{\n                    ...(reduceMotion ? reducedFadeTransition : evidenceRevealTransition),\n                    delay: reduceMotion ? 0 : index * 0.04,\n                  }}\n                  className=\"rounded-md bg-muted px-1.5 py-0.5 font-mono text-meta text-muted-foreground\"\n                >\n                  <GlobeIcon className=\"mr-1 inline-block size-2.5 align-[-1px]\" />\n                  {source}\n                </motion.span>\n              ))}\n            </div>\n          ) : null}\n          {entry.details ? (\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={reduceMotion ? reducedFadeTransition : evidenceRevealTransition}\n            >\n              <AgentActivityDetails details={entry.details} />\n            </motion.div>\n          ) : null}\n          {entry.image ? (\n            <motion.figure\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={reduceMotion ? reducedFadeTransition : spring.moderate.exit}\n              className=\"mt-2.5\"\n            >\n              {/* The visual may be a runtime URL from an agent result, so Next's\n                static Image optimization and its host allow-list cannot be\n                assumed by this distributable component. */}\n              {/* oxlint-disable-next-line no-img-element */}\n              <img\n                src={entry.image.src}\n                alt={entry.image.alt}\n                className=\"aspect-video h-auto w-full max-w-56 rounded-lg border border-border bg-card object-cover\"\n              />\n              {entry.image.caption ? (\n                <figcaption className=\"mt-1 text-meta text-muted-foreground\">\n                  {entry.image.caption}\n                </figcaption>\n              ) : null}\n            </motion.figure>\n          ) : null}\n        </div>\n      </motion.div>\n    </motion.li>\n  );\n}\n\n/**\n * A collapsible agent-run timeline. Pending steps stay out of the document,\n * active steps retain the live state, and completed steps become readable history.\n * Motion story: the header is the fixed anchor; each arriving static step\n * makes vertical space, then settles into the execution trail. A live text\n * step only fades so its changing line breaks are never clipped.\n */\nfunction AgentActivity({\n  entries,\n  title = \"Thinking\",\n  defaultOpen = true,\n  open,\n  onOpenChange,\n  className,\n}: AgentActivityProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const isOpen = open ?? internalOpen;\n  const contentId = useId();\n  const reduceMotion = useReducedMotion();\n  const visibleEntries = entries.filter((entry) => entry.status !== \"pending\");\n\n  const setOpen = (next: boolean) => {\n    if (open === undefined) setInternalOpen(next);\n    onOpenChange?.(next);\n  };\n\n  return (\n    <section data-slot=\"agent-activity\" className={cn(\"w-full max-w-md\", className)}>\n      <button\n        type=\"button\"\n        aria-expanded={isOpen}\n        aria-controls={contentId}\n        onClick={() => setOpen(!isOpen)}\n        className=\"group inline-flex items-center gap-1.5 py-1 text-control text-muted-foreground transition-colors duration-quick hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n      >\n        <span>{title}</span>\n        <motion.span\n          animate={{ transform: `rotate(${isOpen ? 180 : 0}deg)` }}\n          transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n        >\n          <ChevronDownIcon className=\"size-3.5\" />\n        </motion.span>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.div\n            id={contentId}\n            initial={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            animate={reduceMotion ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n            exit={reduceMotion ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            transition={reduceMotion ? reducedFadeTransition : spring.moderate.enter}\n            className=\"overflow-hidden\"\n          >\n            <ol className=\"pt-3\" aria-label=\"Agent activity\">\n              <AnimatePresence initial={false}>\n                {visibleEntries.map((entry, index) => (\n                  <AgentActivityStep\n                    key={entry.id}\n                    entry={entry}\n                    hasFollowingStep={index < visibleEntries.length - 1}\n                  />\n                ))}\n              </AnimatePresence>\n            </ol>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </section>\n  );\n}\n\nexport { AgentActivity };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}