{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-progress",
  "type": "registry:ui",
  "title": "Task progress",
  "description": "Presents queued, active, blocked, failed, recovered, and complete work across compact and expanded layouts.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "agent-ui"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/task-progress/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "Task state advances continuously along one progress rail",
    "sceneFamily": "product-mono",
    "motionRole": "ui",
    "primaryState": "Active task progress rail",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/task-progress.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/task-progress.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useMemo, useState } from \"react\";\n\nexport type TaskProgressStatus = \"queued\" | \"active\" | \"blocked\" | \"failed\" | \"recovered\" | \"complete\";\n\nexport type TaskProgressItem = {\n  id: string;\n  title: string;\n  detail?: string;\n  status: TaskProgressStatus;\n  progress?: number;\n  meta?: string;\n};\n\nexport type TaskProgressProps = {\n  tasks: readonly TaskProgressItem[];\n  label?: string;\n  expandedLabel?: string;\n  compactLabel?: string;\n  statusLabels?: Partial<Record<TaskProgressStatus, string>>;\n  className?: string;\n};\n\nconst defaultStatusLabels: Record<TaskProgressStatus, string> = {\n  queued: \"Queued\",\n  active: \"In progress\",\n  blocked: \"Blocked\",\n  failed: \"Failed\",\n  recovered: \"Recovered\",\n  complete: \"Complete\",\n};\n\nconst statusStyle: Record<TaskProgressStatus, string> = {\n  queued: \"border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/10 dark:bg-white/[.03] dark:text-neutral-400\",\n  active: \"border-neutral-900 bg-neutral-950 text-white dark:border-white dark:bg-white dark:text-neutral-950\",\n  blocked: \"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-400/40 dark:bg-amber-400/10 dark:text-amber-200\",\n  failed: \"border-red-300 bg-red-50 text-red-700 dark:border-red-400/40 dark:bg-red-400/10 dark:text-red-200\",\n  recovered: \"border-blue-300 bg-blue-50 text-blue-700 dark:border-blue-400/40 dark:bg-blue-400/10 dark:text-blue-200\",\n  complete: \"border-emerald-300 bg-emerald-50 text-emerald-700 dark:border-emerald-400/40 dark:bg-emerald-400/10 dark:text-emerald-200\",\n};\n\nfunction StatusMark({ status }: { status: TaskProgressStatus }) {\n  if (status === \"complete\") return <span aria-hidden=\"true\">✓</span>;\n  if (status === \"failed\") return <span aria-hidden=\"true\">!</span>;\n  if (status === \"blocked\") return <span aria-hidden=\"true\">×</span>;\n  if (status === \"recovered\") return <span aria-hidden=\"true\">↻</span>;\n  return <span aria-hidden=\"true\">{status === \"active\" ? \"•\" : \"○\"}</span>;\n}\n\nexport function TaskProgress({\n  tasks,\n  label = \"Task progress\",\n  expandedLabel = \"Expanded\",\n  compactLabel = \"Compact\",\n  statusLabels,\n  className = \"\",\n}: TaskProgressProps) {\n  const [layout, setLayout] = useState<\"expanded\" | \"compact\">(\"expanded\");\n  const reduced = useReducedMotion() === true;\n  const labels = { ...defaultStatusLabels, ...statusLabels };\n  const summary = useMemo(() => tasks.reduce<Record<TaskProgressStatus, number>>((counts, task) => {\n    counts[task.status] += 1;\n    return counts;\n  }, { queued: 0, active: 0, blocked: 0, failed: 0, recovered: 0, complete: 0 }), [tasks]);\n\n  return (\n    <section className={`w-full rounded-[10px] border border-neutral-200 bg-white p-3 text-neutral-950 dark:border-white/10 dark:bg-[#1b1b1b] dark:text-neutral-50 ${className}`}>\n      <header className=\"mb-3 flex min-h-11 items-center gap-2 px-1\">\n        <div className=\"min-w-0\">\n          <strong className=\"block text-[12px]\">{label}</strong>\n          <span className=\"block text-[9px] text-neutral-500 dark:text-neutral-400\">\n            {summary.active ? `${summary.active} ${labels.active.toLowerCase()}` : `${summary.complete}/${tasks.length} ${labels.complete.toLowerCase()}`}\n          </span>\n        </div>\n        <div className=\"ml-auto flex rounded-lg bg-neutral-100 p-0.5 dark:bg-white/5\" role=\"group\" aria-label={label}>\n          <button type=\"button\" aria-pressed={layout === \"expanded\"} onClick={() => setLayout(\"expanded\")} className={`min-h-11 rounded-md px-3 text-[10px] outline-none focus-visible:ring-2 focus-visible:ring-blue-600 ${layout === \"expanded\" ? \"bg-white shadow-sm dark:bg-white/10\" : \"text-neutral-500\"}`}>{expandedLabel}</button>\n          <button type=\"button\" aria-pressed={layout === \"compact\"} onClick={() => setLayout(\"compact\")} className={`min-h-11 rounded-md px-3 text-[10px] outline-none focus-visible:ring-2 focus-visible:ring-blue-600 ${layout === \"compact\" ? \"bg-white shadow-sm dark:bg-white/10\" : \"text-neutral-500\"}`}>{compactLabel}</button>\n        </div>\n      </header>\n\n      <motion.ol layout={!reduced} className={layout === \"compact\" ? \"flex flex-wrap gap-1.5\" : \"grid gap-1.5\"} aria-label={label}>\n        {tasks.map((task) => (\n          <motion.li layout={!reduced} key={task.id} className={`relative overflow-hidden rounded-lg border ${layout === \"compact\" ? \"min-h-11 px-2\" : \"min-h-14 px-3\"} ${task.status === \"active\" ? \"border-neutral-400 bg-neutral-50 dark:border-white/25 dark:bg-white/[.03]\" : \"border-neutral-100 dark:border-white/[.08]\"}`}>\n            <div className=\"flex min-h-10 items-center gap-2.5 py-2\">\n              <span className={`grid size-6 shrink-0 place-items-center rounded-md border text-[10px] ${statusStyle[task.status]}`}><StatusMark status={task.status} /></span>\n              <span className=\"min-w-0 flex-1\">\n                <strong className=\"block truncate text-[11px] font-medium\">{task.title}</strong>\n                {layout === \"expanded\" && task.detail ? <span className=\"block truncate text-[9px] text-neutral-500 dark:text-neutral-400\">{task.detail}</span> : null}\n              </span>\n              {layout === \"expanded\" ? <span className=\"shrink-0 text-[9px] text-neutral-500 dark:text-neutral-400\">{labels[task.status]}</span> : null}\n              {task.meta ? <code className=\"shrink-0 text-[9px] text-neutral-400\">{task.meta}</code> : null}\n            </div>\n            <AnimatePresence initial={false}>\n              {task.status === \"active\" && typeof task.progress === \"number\" ? <motion.span key=\"progress\" className=\"absolute inset-x-0 bottom-0 h-px bg-neutral-200 dark:bg-white/10\" initial={false} animate={{ opacity: 1 }} exit={{ opacity: 0 }}><motion.i className=\"block h-full bg-neutral-950 dark:bg-white\" initial={{ width: 0 }} animate={{ width: `${Math.max(0, Math.min(100, task.progress))}%` }} transition={{ duration: reduced ? 0 : 0.32 }} /></motion.span> : null}\n            </AnimatePresence>\n          </motion.li>\n        ))}\n      </motion.ol>\n      <span className=\"sr-only\" role=\"status\">{tasks.map((task, index) => `${index + 1}. ${task.title}: ${labels[task.status]}`).join(\". \")}</span>\n    </section>\n  );\n}\n"
    }
  ]
}
