{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-activity",
  "type": "registry:component",
  "title": "Agent Activity",
  "description": "Visual display of an AI agent's execution chain — steps, tools, decisions, progress.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/agent-activity/agent-activity.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  createContext,\n  forwardRef,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport {\n  AlertTriangle,\n  CheckCircle2,\n  ChevronDown,\n  Circle,\n  Loader2,\n  MinusCircle,\n} from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Status state for a single {@link AgentStep}.\n *\n * @public\n */\nexport type AgentStepStatus =\n  | \"completed\"\n  | \"error\"\n  | \"pending\"\n  | \"running\"\n  | \"skipped\";\n\n/**\n * Status state for the parent {@link AgentActivity}.\n *\n * @public\n */\nexport type AgentActivityStatus = \"completed\" | \"error\" | \"idle\" | \"running\";\n\nconst STATUS_CLASSES: Record<\n  AgentStepStatus,\n  { iconClass: string; ringClass: string; rowClass: string }\n> = {\n  completed: {\n    iconClass: \"text-emerald-600 dark:text-emerald-400\",\n    ringClass: \"ring-emerald-500/30\",\n    rowClass: \"border-border bg-background\",\n  },\n  error: {\n    iconClass: \"text-destructive\",\n    ringClass: \"ring-destructive/40\",\n    rowClass: \"border-destructive/40 bg-destructive/5\",\n  },\n  pending: {\n    iconClass: \"text-muted-foreground\",\n    ringClass: \"ring-border\",\n    rowClass: \"border-border bg-muted/20 text-muted-foreground\",\n  },\n  running: {\n    iconClass: \"text-primary\",\n    ringClass: \"ring-primary/30\",\n    rowClass: \"border-primary/30 bg-primary/5\",\n  },\n  skipped: {\n    iconClass: \"text-muted-foreground/60\",\n    ringClass: \"ring-border\",\n    rowClass: \"border-border bg-background text-muted-foreground/80\",\n  },\n};\n\nconst DEFAULT_STATUS_ICON: Record<AgentStepStatus, ReactNode> = {\n  completed: <CheckCircle2 aria-hidden=\"true\" className=\"size-4\" />,\n  error: <AlertTriangle aria-hidden=\"true\" className=\"size-4\" />,\n  pending: <Circle aria-hidden=\"true\" className=\"size-4\" />,\n  running: <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />,\n  skipped: <MinusCircle aria-hidden=\"true\" className=\"size-4\" />,\n};\n\ntype StepContextValue = {\n  status: AgentStepStatus;\n};\n\nconst StepContext = createContext<StepContextValue>({ status: \"pending\" });\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type AgentActivityLabels = {\n  /** Caption above the steps list. Defaults to `\"Activity\"`. */\n  activity?: string;\n  /** Caption for the collapse-details toggle. Defaults to `\"Hide details\"`. */\n  collapse?: string;\n  /** Aria-label for the elapsed-time region. Defaults to `\"Elapsed\"`. */\n  elapsed?: string;\n  /** Caption for the expand-details toggle. Defaults to `\"Show details\"`. */\n  expand?: string;\n};\n\nconst DEFAULT_LABELS = {\n  activity: \"Activity\",\n  collapse: \"Hide details\",\n  elapsed: \"Elapsed\",\n  expand: \"Show details\",\n} as const satisfies Required<AgentActivityLabels>;\n\n/**\n * Props for {@link AgentActivity}.\n *\n * @public\n */\nexport type AgentActivityProps = {\n  /** Optional total elapsed time string shown in the header. */\n  elapsed?: ReactNode;\n  /** Localizable strings. */\n  labels?: AgentActivityLabels;\n  /** Top-level agent status. Defaults to `\"idle\"`. */\n  status?: AgentActivityStatus;\n} & ComponentPropsWithoutRef<\"section\">;\n\nconst ACTIVITY_LIVE_REGION_ROLE: Record<AgentActivityStatus, \"log\" | \"status\"> =\n  {\n    completed: \"status\",\n    error: \"status\",\n    idle: \"status\",\n    running: \"log\",\n  };\n\n/**\n * Visual display of an AI agent's execution chain — steps taken, tools\n * called, decisions made, and current progress. Composes {@link AgentStep}\n * children. Use `aria-live=\"polite\"` on the log so assistive tech reads\n * new steps as the agent progresses without stealing focus.\n *\n * @example\n * ```tsx\n * <AgentActivity status=\"running\" elapsed=\"3.2s\">\n *   <AgentStep status=\"completed\" icon={<Search />}>\n *     <AgentStepTitle>Searching codebase</AgentStepTitle>\n *     <AgentStepDetail>Found 12 files matching \"auth\".</AgentStepDetail>\n *     <AgentStepDuration>1.2s</AgentStepDuration>\n *   </AgentStep>\n *   <AgentStep status=\"running\" icon={<Code />}>\n *     <AgentStepTitle>Writing fix</AgentStepTitle>\n *     <AgentStepProgress value={60} />\n *   </AgentStep>\n * </AgentActivity>\n * ```\n *\n * @public\n */\nexport const AgentActivity = forwardRef<HTMLElement, AgentActivityProps>(\n  (props, ref) => {\n    const {\n      children,\n      className,\n      elapsed,\n      labels,\n      status = \"idle\",\n      ...rest\n    } = props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    return (\n      <section\n        aria-live={status === \"running\" ? \"polite\" : \"off\"}\n        className={cn(\n          \"flex flex-col gap-3 rounded-2xl border bg-background p-4\",\n          className,\n        )}\n        data-status={status}\n        ref={ref}\n        role={ACTIVITY_LIVE_REGION_ROLE[status]}\n        {...rest}\n      >\n        <header className=\"flex items-center justify-between gap-3\">\n          <h3 className=\"text-sm font-semibold tracking-tight text-foreground\">\n            {resolvedLabels.activity}\n          </h3>\n          {elapsed ? (\n            <span\n              aria-label={resolvedLabels.elapsed}\n              className=\"text-xs font-mono text-muted-foreground\"\n            >\n              {elapsed}\n            </span>\n          ) : null}\n        </header>\n        <ol className=\"flex flex-col gap-2\">{children}</ol>\n      </section>\n    );\n  },\n);\nAgentActivity.displayName = \"AgentActivity\";\n\n/**\n * Props for {@link AgentStep}.\n *\n * @public\n */\nexport type AgentStepProps = {\n  /** When false, the step renders the toggle but starts collapsed. */\n  defaultOpen?: boolean;\n  /** Optional icon shown to the left of the title. */\n  icon?: ReactNode;\n  /** Step status. */\n  status: AgentStepStatus;\n} & ComponentPropsWithoutRef<\"li\">;\n\ntype ContentSplit = {\n  body: ReactNode[];\n  header: ReactNode[];\n};\n\nfunction getDisplayName(value: unknown): string | undefined {\n  if (\n    value !== null &&\n    (typeof value === \"object\" || typeof value === \"function\") &&\n    \"displayName\" in value &&\n    typeof value.displayName === \"string\"\n  ) {\n    return value.displayName;\n  }\n  return undefined;\n}\n\nfunction isAgentStepDetailElement(child: ReactNode): boolean {\n  if (child === null || typeof child !== \"object\") return false;\n  if (!(\"type\" in child)) return false;\n  return getDisplayName(child.type) === \"AgentStepDetail\";\n}\n\nfunction splitChildren(children: ReactNode): ContentSplit {\n  const items = Array.isArray(children)\n    ? (children as ReactNode[])\n    : [children];\n  return items.reduce<ContentSplit>(\n    (split, child) => {\n      if (isAgentStepDetailElement(child)) {\n        split.body.push(child);\n      } else {\n        split.header.push(child);\n      }\n      return split;\n    },\n    { body: [], header: [] },\n  );\n}\n\n/**\n * One row inside an {@link AgentActivity}. The component partitions\n * children into an always-visible header (title, duration, progress) and a\n * collapsible body (any number of {@link AgentStepDetail} blocks).\n *\n * @public\n */\ntype StepRowProps = {\n  detailsId: string;\n  hasDetails: boolean;\n  header: ReactNode;\n  icon: ReactNode;\n  iconClass: string;\n  labels: Required<AgentActivityLabels>;\n  onToggle: () => void;\n  open: boolean;\n};\n\nfunction StepRow({\n  detailsId,\n  hasDetails,\n  header,\n  icon,\n  iconClass,\n  labels,\n  onToggle,\n  open,\n}: StepRowProps): ReactNode {\n  return (\n    <div className=\"flex items-start gap-3 px-3 py-2\">\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"mt-0.5 inline-flex size-5 shrink-0 items-center justify-center\",\n          iconClass,\n        )}\n      >\n        {icon}\n      </span>\n      <div className=\"flex min-w-0 flex-1 flex-col gap-1\">{header}</div>\n      {hasDetails ? (\n        <button\n          aria-controls={detailsId}\n          aria-expanded={open}\n          aria-label={open ? labels.collapse : labels.expand}\n          className=\"ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-foreground/10 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          onClick={onToggle}\n          type=\"button\"\n        >\n          <ChevronDown\n            aria-hidden=\"true\"\n            className={cn(\n              \"size-4 transition-transform\",\n              open ? \"rotate-180\" : \"rotate-0\",\n            )}\n          />\n        </button>\n      ) : null}\n    </div>\n  );\n}\n\nexport const AgentStep = forwardRef<HTMLLIElement, AgentStepProps>(\n  (props, ref) => {\n    const {\n      children,\n      className,\n      defaultOpen = true,\n      icon,\n      status,\n      ...rest\n    } = props;\n    const split = useMemo(() => splitChildren(children), [children]);\n    const palette = STATUS_CLASSES[status];\n    const hasDetails = split.body.length > 0;\n    const [open, setOpen] = useState(defaultOpen);\n    const detailsId = useId();\n\n    const handleToggle = useCallback(() => {\n      setOpen((value) => !value);\n    }, []);\n\n    const contextValue = useMemo<StepContextValue>(\n      () => ({ status }),\n      [status],\n    );\n    const resolvedIcon = icon ?? DEFAULT_STATUS_ICON[status];\n\n    return (\n      <li\n        className={cn(\n          \"rounded-xl border ring-1\",\n          palette.rowClass,\n          palette.ringClass,\n          className,\n        )}\n        data-status={status}\n        ref={ref}\n        {...rest}\n      >\n        <StepContext.Provider value={contextValue}>\n          <StepRow\n            detailsId={detailsId}\n            hasDetails={hasDetails}\n            header={split.header}\n            icon={resolvedIcon}\n            iconClass={palette.iconClass}\n            labels={DEFAULT_LABELS}\n            onToggle={handleToggle}\n            open={open}\n          />\n          {hasDetails && open ? (\n            <div\n              className=\"border-t border-border/60 px-3 py-2 text-xs\"\n              id={detailsId}\n            >\n              <div className=\"flex flex-col gap-2 pl-8\">{split.body}</div>\n            </div>\n          ) : null}\n        </StepContext.Provider>\n      </li>\n    );\n  },\n);\nAgentStep.displayName = \"AgentStep\";\n\n/**\n * Props for {@link AgentStepTitle}.\n *\n * @public\n */\nexport type AgentStepTitleProps = ComponentPropsWithoutRef<\"p\">;\n\n/**\n * Title slot for an {@link AgentStep}.\n *\n * @public\n */\nexport const AgentStepTitle = forwardRef<\n  HTMLParagraphElement,\n  AgentStepTitleProps\n>(({ className, ...rest }, ref) => (\n  <p\n    className={cn(\n      \"text-sm font-medium leading-tight text-foreground\",\n      className,\n    )}\n    ref={ref}\n    {...rest}\n  />\n));\nAgentStepTitle.displayName = \"AgentStepTitle\";\n\n/**\n * Props for {@link AgentStepDuration}.\n *\n * @public\n */\nexport type AgentStepDurationProps = ComponentPropsWithoutRef<\"span\">;\n\n/**\n * Duration badge for an {@link AgentStep}.\n *\n * @public\n */\nexport const AgentStepDuration = forwardRef<\n  HTMLSpanElement,\n  AgentStepDurationProps\n>(({ className, ...rest }, ref) => (\n  <span\n    className={cn(\"font-mono text-xs text-muted-foreground\", className)}\n    ref={ref}\n    {...rest}\n  />\n));\nAgentStepDuration.displayName = \"AgentStepDuration\";\n\n/**\n * Props for {@link AgentStepProgress}.\n *\n * @public\n */\nexport type AgentStepProgressProps = {\n  /** Optional aria-label override. Defaults to `\"Step progress\"`. */\n  label?: string;\n  /** Progress value, 0–100. */\n  value: number;\n} & Omit<ComponentPropsWithoutRef<\"div\">, \"role\">;\n\n/**\n * Progress bar for a running {@link AgentStep}.\n *\n * @public\n */\nexport const AgentStepProgress = forwardRef<\n  HTMLDivElement,\n  AgentStepProgressProps\n>(({ className, label = \"Step progress\", value, ...rest }, ref) => {\n  const clamped = Math.max(0, Math.min(100, value));\n  return (\n    <div\n      aria-label={label}\n      aria-valuemax={100}\n      aria-valuemin={0}\n      aria-valuenow={clamped}\n      className={cn(\"h-1.5 w-full rounded-full bg-muted\", className)}\n      ref={ref}\n      role=\"progressbar\"\n      {...rest}\n    >\n      <span\n        className=\"block h-full rounded-full bg-primary transition-[width]\"\n        style={{ width: `${clamped.toString()}%` }}\n      />\n    </div>\n  );\n});\nAgentStepProgress.displayName = \"AgentStepProgress\";\n\n/**\n * Props for {@link AgentStepDetail}.\n *\n * @public\n */\nexport type AgentStepDetailProps = ComponentPropsWithoutRef<\"div\">;\n\n/**\n * Collapsible detail block inside an {@link AgentStep} (tool output, log\n * snippet, anything secondary).\n *\n * @public\n */\nexport const AgentStepDetail = forwardRef<HTMLDivElement, AgentStepDetailProps>(\n  ({ className, ...rest }, ref) => (\n    <div\n      className={cn(\"text-xs text-muted-foreground\", className)}\n      ref={ref}\n      {...rest}\n    />\n  ),\n);\nAgentStepDetail.displayName = \"AgentStepDetail\";\n\n/**\n * Hook for reading the current step's status from inside a custom child.\n *\n * @public\n */\nexport function useAgentStepStatus(): AgentStepStatus {\n  return useContext(StepContext).status;\n}\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
