{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pipeline",
  "title": "AI Pipeline",
  "description": "Multi-stage workflow visualization with horizontal or vertical layout, stage status indicators, progress tracking, and expandable stage details.",
  "dependencies": [
    "@radix-ui/react-collapsible",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/ai/ai-pipeline/components/elements/ai-pipeline.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\nimport {\n  Bot,\n  Check,\n  ChevronDown,\n  Circle,\n  Clock,\n  Loader2,\n  X,\n} from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype StageStatus = \"pending\" | \"active\" | \"completed\" | \"error\";\n\ninterface PipelineStage {\n  id: string;\n  name: string;\n  description?: string;\n  status: StageStatus;\n  agent?: string;\n  startTime?: Date;\n  endTime?: Date;\n}\n\ninterface AiPipelineContextValue {\n  stages: PipelineStage[];\n  orientation: \"horizontal\" | \"vertical\";\n  expandedStages: Set<string>;\n  toggleStage: (id: string) => void;\n}\n\nconst AiPipelineContext = React.createContext<AiPipelineContextValue | null>(\n  null,\n);\n\nfunction usePipelineContext() {\n  const context = React.useContext(AiPipelineContext);\n  if (!context) {\n    throw new Error(\"AiPipeline components must be used within <AiPipeline>\");\n  }\n  return context;\n}\n\ninterface AiPipelineProps {\n  stages: PipelineStage[];\n  orientation?: \"horizontal\" | \"vertical\";\n  defaultExpanded?: string[];\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiPipeline({\n  stages,\n  orientation = \"horizontal\",\n  defaultExpanded = [],\n  children,\n  className,\n}: AiPipelineProps) {\n  const [expandedStages, setExpandedStages] = React.useState<Set<string>>(\n    () => new Set(defaultExpanded),\n  );\n\n  const toggleStage = React.useCallback((id: string) => {\n    setExpandedStages((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) {\n        next.delete(id);\n      } else {\n        next.add(id);\n      }\n      return next;\n    });\n  }, []);\n\n  const contextValue = React.useMemo(\n    () => ({ stages, orientation, expandedStages, toggleStage }),\n    [stages, orientation, expandedStages, toggleStage],\n  );\n\n  const completedCount = stages.filter((s) => s.status === \"completed\").length;\n  const progress =\n    stages.length > 0 ? (completedCount / stages.length) * 100 : 0;\n\n  return (\n    <AiPipelineContext.Provider value={contextValue}>\n      <div\n        data-slot=\"ai-pipeline\"\n        data-orientation={orientation}\n        className={cn(\n          \"rounded-lg border border-border bg-card text-card-foreground overflow-hidden\",\n          className,\n        )}\n      >\n        {children ?? (\n          <>\n            <AiPipelineHeader progress={progress} />\n            <AiPipelineContent />\n          </>\n        )}\n      </div>\n    </AiPipelineContext.Provider>\n  );\n}\n\ninterface AiPipelineHeaderProps {\n  title?: string;\n  progress?: number;\n  children?: React.ReactNode;\n  className?: string;\n}\n\nfunction AiPipelineHeader({\n  title = \"Pipeline\",\n  progress,\n  children,\n  className,\n}: AiPipelineHeaderProps) {\n  const { stages } = usePipelineContext();\n\n  const completedCount = stages.filter((s) => s.status === \"completed\").length;\n  const activeStage = stages.find((s) => s.status === \"active\");\n\n  return (\n    <div\n      data-slot=\"ai-pipeline-header\"\n      className={cn(\"px-4 py-3 border-b border-border\", className)}\n    >\n      <div className=\"flex items-center justify-between gap-3\">\n        <div className=\"flex items-center gap-3\">\n          <h3 className=\"font-semibold text-sm\">{title}</h3>\n          <span className=\"inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n            {completedCount}/{stages.length}\n          </span>\n        </div>\n        {activeStage && (\n          <span className=\"inline-flex items-center gap-1.5 text-xs text-muted-foreground\">\n            <Loader2 className=\"size-3 animate-spin\" />\n            {activeStage.name}\n          </span>\n        )}\n        {children}\n      </div>\n      {progress !== undefined && (\n        <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted\">\n          <div\n            className=\"h-full bg-primary transition-all duration-500 ease-out\"\n            style={{ width: `${progress}%` }}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface AiPipelineContentProps {\n  className?: string;\n}\n\nfunction AiPipelineContent({ className }: AiPipelineContentProps) {\n  const { stages, orientation } = usePipelineContext();\n\n  return (\n    <div\n      data-slot=\"ai-pipeline-content\"\n      className={cn(\n        \"p-4\",\n        orientation === \"horizontal\"\n          ? \"flex items-start gap-2 overflow-x-auto\"\n          : \"space-y-2\",\n        className,\n      )}\n    >\n      {stages.map((stage, index) => (\n        <React.Fragment key={stage.id}>\n          <AiPipelineStage stage={stage} />\n          {index < stages.length - 1 && <AiPipelineConnector index={index} />}\n        </React.Fragment>\n      ))}\n    </div>\n  );\n}\n\ninterface AiPipelineStageProps {\n  stage: PipelineStage;\n  className?: string;\n}\n\nfunction AiPipelineStage({ stage, className }: AiPipelineStageProps) {\n  const { orientation, expandedStages, toggleStage } = usePipelineContext();\n  const isExpanded = expandedStages.has(stage.id);\n\n  const statusConfig = React.useMemo(() => {\n    const configs: Record<\n      StageStatus,\n      {\n        icon: React.ReactNode;\n        className: string;\n        bgClassName: string;\n      }\n    > = {\n      pending: {\n        icon: <Circle className=\"size-4\" />,\n        className: \"text-muted-foreground\",\n        bgClassName: \"bg-muted\",\n      },\n      active: {\n        icon: <Loader2 className=\"size-4 animate-spin\" />,\n        className: \"text-blue-600 dark:text-blue-400\",\n        bgClassName: \"bg-blue-100 dark:bg-blue-950\",\n      },\n      completed: {\n        icon: <Check className=\"size-4\" />,\n        className: \"text-green-600 dark:text-green-400\",\n        bgClassName: \"bg-green-100 dark:bg-green-950\",\n      },\n      error: {\n        icon: <X className=\"size-4\" />,\n        className: \"text-red-600 dark:text-red-400\",\n        bgClassName: \"bg-red-100 dark:bg-red-950\",\n      },\n    };\n    return configs[stage.status];\n  }, [stage.status]);\n\n  const formatDuration = React.useCallback((start: Date, end?: Date) => {\n    const endTime = end ?? new Date();\n    const durationMs = endTime.getTime() - start.getTime();\n    const seconds = Math.floor(durationMs / 1000);\n    if (seconds < 60) return `${seconds}s`;\n    const minutes = Math.floor(seconds / 60);\n    return `${minutes}m ${seconds % 60}s`;\n  }, []);\n\n  if (orientation === \"horizontal\") {\n    return (\n      <CollapsiblePrimitive.Root\n        open={isExpanded}\n        onOpenChange={() => toggleStage(stage.id)}\n      >\n        <div\n          data-slot=\"ai-pipeline-stage\"\n          data-status={stage.status}\n          className={cn(\n            \"flex shrink-0 flex-col items-center\",\n            orientation === \"horizontal\" && \"min-w-[120px]\",\n            className,\n          )}\n        >\n          <CollapsiblePrimitive.Trigger\n            className={cn(\n              \"flex flex-col items-center gap-2 rounded-lg p-3 transition-colors\",\n              \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            )}\n          >\n            <div\n              className={cn(\n                \"flex size-10 items-center justify-center rounded-full transition-colors\",\n                statusConfig.bgClassName,\n              )}\n            >\n              <span className={statusConfig.className}>\n                {statusConfig.icon}\n              </span>\n            </div>\n            <div className=\"text-center\">\n              <span className=\"text-sm font-medium\">{stage.name}</span>\n              {stage.agent && (\n                <span className=\"mt-0.5 flex items-center justify-center gap-1 text-xs text-muted-foreground\">\n                  <Bot className=\"size-3\" />\n                  {stage.agent}\n                </span>\n              )}\n            </div>\n          </CollapsiblePrimitive.Trigger>\n          <CollapsiblePrimitive.Content className=\"w-full data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n            <div className=\"mt-2 rounded-lg border border-border bg-muted/30 p-3 text-xs\">\n              {stage.description && (\n                <p className=\"text-muted-foreground\">{stage.description}</p>\n              )}\n              {stage.startTime && (\n                <div className=\"mt-2 flex items-center gap-1 text-muted-foreground\">\n                  <Clock className=\"size-3\" />\n                  {formatDuration(stage.startTime, stage.endTime)}\n                </div>\n              )}\n            </div>\n          </CollapsiblePrimitive.Content>\n        </div>\n      </CollapsiblePrimitive.Root>\n    );\n  }\n\n  return (\n    <CollapsiblePrimitive.Root\n      open={isExpanded}\n      onOpenChange={() => toggleStage(stage.id)}\n    >\n      <div\n        data-slot=\"ai-pipeline-stage\"\n        data-status={stage.status}\n        className={cn(\n          \"rounded-lg border border-border overflow-hidden\",\n          className,\n        )}\n      >\n        <CollapsiblePrimitive.Trigger\n          className={cn(\n            \"flex w-full items-center gap-3 p-3 text-left transition-colors\",\n            \"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          )}\n        >\n          <div\n            className={cn(\n              \"flex size-8 shrink-0 items-center justify-center rounded-full\",\n              statusConfig.bgClassName,\n            )}\n          >\n            <span className={statusConfig.className}>{statusConfig.icon}</span>\n          </div>\n          <div className=\"flex-1 min-w-0\">\n            <div className=\"flex items-center gap-2\">\n              <span className=\"font-medium text-sm\">{stage.name}</span>\n              {stage.agent && (\n                <span className=\"inline-flex items-center gap-1 text-xs text-muted-foreground\">\n                  <Bot className=\"size-3\" />\n                  {stage.agent}\n                </span>\n              )}\n            </div>\n            {stage.startTime && (\n              <span className=\"text-xs text-muted-foreground\">\n                {formatDuration(stage.startTime, stage.endTime)}\n              </span>\n            )}\n          </div>\n          <ChevronDown\n            className={cn(\n              \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n              isExpanded && \"rotate-180\",\n            )}\n          />\n        </CollapsiblePrimitive.Trigger>\n        <CollapsiblePrimitive.Content className=\"border-t border-border data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down\">\n          {stage.description && (\n            <div className=\"p-3\">\n              <p className=\"text-sm text-muted-foreground\">\n                {stage.description}\n              </p>\n            </div>\n          )}\n        </CollapsiblePrimitive.Content>\n      </div>\n    </CollapsiblePrimitive.Root>\n  );\n}\n\ninterface AiPipelineConnectorProps {\n  index: number;\n  className?: string;\n}\n\nfunction AiPipelineConnector({ index, className }: AiPipelineConnectorProps) {\n  const { stages, orientation } = usePipelineContext();\n\n  const currentStage = stages[index];\n  const nextStage = stages[index + 1];\n\n  const isCompleted =\n    currentStage?.status === \"completed\" || currentStage?.status === \"error\";\n  const isActive = nextStage?.status === \"active\";\n\n  if (orientation === \"horizontal\") {\n    return (\n      <div\n        data-slot=\"ai-pipeline-connector\"\n        className={cn(\"flex h-10 shrink-0 items-center self-center\", className)}\n      >\n        <div\n          className={cn(\n            \"h-0.5 w-8 rounded-full transition-colors\",\n            isCompleted\n              ? \"bg-green-500 dark:bg-green-400\"\n              : isActive\n                ? \"bg-blue-500 dark:bg-blue-400 animate-pulse\"\n                : \"bg-muted\",\n          )}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"ai-pipeline-connector\"\n      className={cn(\"flex justify-center py-1\", className)}\n    >\n      <div\n        className={cn(\n          \"h-4 w-0.5 rounded-full transition-colors\",\n          isCompleted\n            ? \"bg-green-500 dark:bg-green-400\"\n            : isActive\n              ? \"bg-blue-500 dark:bg-blue-400 animate-pulse\"\n              : \"bg-muted\",\n        )}\n      />\n    </div>\n  );\n}\n\ninterface AiPipelineCompactProps {\n  stages: PipelineStage[];\n  className?: string;\n}\n\nfunction AiPipelineCompact({ stages, className }: AiPipelineCompactProps) {\n  const completedCount = stages.filter((s) => s.status === \"completed\").length;\n  const activeStage = stages.find((s) => s.status === \"active\");\n  const hasError = stages.some((s) => s.status === \"error\");\n\n  return (\n    <div\n      data-slot=\"ai-pipeline-compact\"\n      className={cn(\n        \"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm\",\n        hasError && \"border-red-300 dark:border-red-800\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center gap-1\">\n        {stages.map((stage) => (\n          <div\n            key={stage.id}\n            title={stage.name}\n            className={cn(\n              \"size-2 rounded-full transition-colors\",\n              stage.status === \"pending\" && \"bg-muted\",\n              stage.status === \"active\" && \"bg-blue-500 animate-pulse\",\n              stage.status === \"completed\" && \"bg-green-500\",\n              stage.status === \"error\" && \"bg-red-500\",\n            )}\n          />\n        ))}\n      </div>\n      <span className=\"text-xs text-muted-foreground\">\n        {hasError\n          ? \"Error\"\n          : activeStage\n            ? activeStage.name\n            : `${completedCount}/${stages.length}`}\n      </span>\n    </div>\n  );\n}\n\nexport {\n  AiPipeline,\n  AiPipelineHeader,\n  AiPipelineContent,\n  AiPipelineStage,\n  AiPipelineConnector,\n  AiPipelineCompact,\n};\nexport type { AiPipelineProps, PipelineStage, StageStatus };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Compound component for pipeline visualization. Orientations: horizontal, vertical. Stage states: pending, active, completed, error. Features progress bar, animated connectors, expandable stage details, and compact variant.",
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}