{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "job-progress",
  "title": "Job Progress",
  "description": "A weighted multi-stage progress surface for work measured in minutes rather than seconds.",
  "dependencies": [
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0"
  ],
  "registryDependencies": [
    "button",
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/job-progress.tsx",
      "content": "import { CheckCircleIcon as CheckIcon } from '@solar-icons/react/bold/check-circle'\nimport { LoaderIcon as LoaderCircleIcon } from '@solar-icons/react/linear/loader'\nimport { CloseIcon as XIcon } from '@solar-icons/react/linear/close'\nimport * as React from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from 'cn'\n\ninterface JobStage {\n  detail?: string\n  duration?: string\n  id: string\n  label: string\n  progress?: number\n  status: 'complete' | 'pending' | 'running'\n  weight?: number\n}\n\ninterface JobProgressProps extends React.ComponentProps<'section'> {\n  eta?: string\n  onCancel?: () => void\n  onPausedChange?: (paused: boolean) => void\n  paused?: boolean\n  stages: JobStage[]\n  title: string\n}\n\nconst labelTone: Record<JobStage['status'], string> = {\n  complete: 'text-muted-foreground',\n  pending: 'text-muted-foreground/60',\n  running: 'text-foreground',\n}\n\nfunction stageFill(stage: JobStage) {\n  if (stage.status === 'complete') return 1\n  if (stage.status === 'running') return Math.min(Math.max(stage.progress ?? 0, 0), 1)\n  return 0\n}\n\nfunction JobProgress({\n  className,\n  eta,\n  onCancel,\n  onPausedChange,\n  paused = false,\n  stages,\n  title,\n  ...props\n}: JobProgressProps) {\n  const [confirming, setConfirming] = React.useState(false)\n  const finished = stages.every((stage) => stage.status === 'complete')\n  const totalWeight = stages.reduce((sum, stage) => sum + (stage.weight ?? 1), 0)\n  const doneWeight = stages.reduce((sum, stage) => sum + (stage.weight ?? 1) * stageFill(stage), 0)\n  const progress = totalWeight ? Math.round((doneWeight / totalWeight) * 100) : 0\n\n  return (\n    <section\n      data-slot=\"job-progress\"\n      className={cn(\n        'rounded-[16px] bg-card p-5 font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card',\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-start gap-3\">\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"text-[15px] leading-6 font-medium text-foreground\">{title}</h3>\n          <p className=\"mt-0.5 text-[13px] leading-5 text-muted-foreground tabular-nums\">\n            {finished\n              ? 'Finished'\n              : paused\n                ? `Paused at ${progress}%`\n                : eta\n                  ? `${progress}% complete · ${eta} remaining`\n                  : `${progress}% complete`}\n          </p>\n        </div>\n        {!finished && (onPausedChange || onCancel) && (\n          <div className=\"flex shrink-0 items-center gap-1\">\n            {confirming ? (\n              <>\n                <Button\n                  className=\"h-7 rounded-full px-3 text-muted-foreground\"\n                  onClick={() => setConfirming(false)}\n                  size=\"sm\"\n                  variant=\"ghost\"\n                >\n                  Keep running\n                </Button>\n                <Button\n                  className=\"h-7 rounded-full bg-destructive/10 px-3 text-destructive hover:bg-destructive/20 hover:text-destructive\"\n                  onClick={() => {\n                    setConfirming(false)\n                    onCancel?.()\n                  }}\n                  size=\"sm\"\n                  variant=\"ghost\"\n                >\n                  Stop\n                </Button>\n              </>\n            ) : (\n              <>\n                {onPausedChange && (\n                  <Button\n                    aria-label={paused ? 'Resume job' : 'Pause job'}\n                    aria-pressed={paused}\n                    onClick={() => onPausedChange(!paused)}\n                    size=\"icon-sm\"\n                    variant=\"ghost\"\n                  >\n                    {paused ? (\n                      <svg\n                        aria-hidden=\"true\"\n                        className=\"size-3.5\"\n                        fill=\"currentColor\"\n                        viewBox=\"0 0 16 16\"\n                      >\n                        <path d=\"M5 3.5v9l7.5-4.5z\" />\n                      </svg>\n                    ) : (\n                      <svg\n                        aria-hidden=\"true\"\n                        className=\"size-3.5\"\n                        fill=\"currentColor\"\n                        viewBox=\"0 0 16 16\"\n                      >\n                        <rect height=\"9\" rx=\"1\" width=\"2.5\" x=\"4\" y=\"3.5\" />\n                        <rect height=\"9\" rx=\"1\" width=\"2.5\" x=\"9.5\" y=\"3.5\" />\n                      </svg>\n                    )}\n                  </Button>\n                )}\n                {onCancel && (\n                  <Button\n                    aria-label=\"Cancel job\"\n                    onClick={() => setConfirming(true)}\n                    size=\"icon-sm\"\n                    variant=\"ghost\"\n                  >\n                    <XIcon />\n                  </Button>\n                )}\n              </>\n            )}\n          </div>\n        )}\n      </div>\n      <progress aria-label={`${title} progress`} className=\"sr-only\" max={100} value={progress} />\n      <div aria-hidden=\"true\" className=\"mt-4 flex gap-1\">\n        {stages.map((stage) => (\n          <span\n            className=\"h-1.5 overflow-hidden rounded-full bg-foreground/8\"\n            key={stage.id}\n            style={{ flexGrow: stage.weight ?? 1, flexBasis: 0 }}\n          >\n            <span\n              className={cn(\n                'block h-full origin-left rounded-full bg-blue-500 transition-[scale,opacity] duration-300 ease-linear dark:bg-blue-400',\n                paused && stage.status === 'running' && 'opacity-50',\n              )}\n              style={{ scale: `${stageFill(stage)} 1` }}\n            />\n          </span>\n        ))}\n      </div>\n      <ol className=\"mt-5 space-y-3\">\n        {stages.map((stage) => (\n          <li className=\"flex items-start gap-3\" key={stage.id}>\n            <span className=\"flex h-5 w-4 shrink-0 items-center justify-center\">\n              <StageIcon paused={paused} status={stage.status} />\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"flex items-baseline gap-3\">\n                <span className={cn('min-w-0 flex-1 text-sm leading-5', labelTone[stage.status])}>\n                  {stage.label}\n                </span>\n                {stage.status === 'complete' && stage.duration && (\n                  <span className=\"shrink-0 text-[13px] leading-5 text-muted-foreground/70 tabular-nums\">\n                    {stage.duration}\n                  </span>\n                )}\n              </p>\n              {stage.status === 'running' && stage.detail && (\n                <p className=\"mt-0.5 text-[13px] leading-5 text-muted-foreground/70 tabular-nums\">\n                  {stage.detail}\n                </p>\n              )}\n            </div>\n          </li>\n        ))}\n      </ol>\n    </section>\n  )\n}\n\nfunction StageIcon({ paused, status }: Pick<JobStage, 'status'> & { paused?: boolean }) {\n  if (status === 'complete')\n    return (\n      <CheckIcon\n        aria-label=\"Complete\"\n        className=\"size-4 text-emerald-600 transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] dark:text-emerald-500 starting:scale-90 starting:opacity-0 motion-reduce:starting:scale-100\"\n      />\n    )\n  if (status === 'running')\n    return (\n      <LoaderCircleIcon\n        aria-label=\"Running\"\n        className={cn(\n          'size-4 text-blue-500 dark:text-blue-400',\n          paused ? 'opacity-50' : 'animate-spin motion-reduce:animate-none',\n        )}\n      />\n    )\n  return (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"size-3.5 rounded-full border-[1.5px] border-muted-foreground/50\"\n      />\n      <span className=\"sr-only\">Pending</span>\n    </>\n  )\n}\n\nexport { JobProgress }\nexport type { JobStage, JobProgressProps }\n",
      "type": "registry:component",
      "target": "@components/ai/job-progress.tsx"
    }
  ],
  "type": "registry:component"
}