{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-activity",
  "title": "Agent Activity",
  "description": "Visual display of an AI agent's execution chain — steps, tools, decisions, progress.",
  "dependencies": [
    "@vllnt/ui@^0.3.0",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/agent-activity/agent-activity.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  createContext,\n  type ReactNode,\n  use,\n  useCallback,\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\nconst AgentActivityLabelsContext =\n  createContext<Required<AgentActivityLabels>>(DEFAULT_LABELS);\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 = ({\n  ref,\n  ...props\n}: AgentActivityProps & { ref?: React.Ref<HTMLElement> }) => {\n  const {\n    children,\n    className,\n    elapsed,\n    labels,\n    status = \"idle\",\n    ...rest\n  } = props;\n  const resolvedLabels = useMemo(\n    () => ({ ...DEFAULT_LABELS, ...labels }),\n    [labels],\n  );\n  return (\n    <AgentActivityLabelsContext.Provider value={resolvedLabels}>\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    </AgentActivityLabelsContext.Provider>\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 = ({\n  ref,\n  ...props\n}: AgentStepProps & { ref?: React.Ref<HTMLLIElement> }) => {\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  const labels = use(AgentActivityLabelsContext);\n\n  const handleToggle = useCallback(() => {\n    setOpen((value) => !value);\n  }, []);\n\n  const contextValue = useMemo<StepContextValue>(() => ({ status }), [status]);\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={labels}\n          onToggle={handleToggle}\n          open={open}\n        />\n        {hasDetails ? (\n          <div\n            className=\"border-t border-border/60 px-3 py-2 text-xs\"\n            hidden={!open}\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};\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 = ({\n  className,\n  ref,\n  ...rest\n}: AgentStepTitleProps & { ref?: React.Ref<HTMLParagraphElement> }) => (\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 = ({\n  className,\n  ref,\n  ...rest\n}: AgentStepDurationProps & { ref?: React.Ref<HTMLSpanElement> }) => (\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 = ({\n  className,\n  label = \"Step progress\",\n  ref,\n  value,\n  ...rest\n}: AgentStepProgressProps & { ref?: React.Ref<HTMLDivElement> }) => {\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 = ({\n  className,\n  ref,\n  ...rest\n}: AgentStepDetailProps & { ref?: React.Ref<HTMLDivElement> }) => (\n  <div\n    className={cn(\"text-xs text-muted-foreground\", className)}\n    ref={ref}\n    {...rest}\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 use(StepContext).status;\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
