{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"task-steps","type":"registry:ui","title":"Task Steps","description":"The system narrates its work.","dependencies":["motion"],"categories":["async"],"docs":"https://www.interior.dev/docs/task-steps","files":[{"path":"registry/interior/task-steps.tsx","type":"registry:ui","target":"components/interior/task-steps.tsx","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nconst POP = { type: \"spring\", stiffness: 640, damping: 22, mass: 0.7 } as const;\nconst CELL = { type: \"spring\", stiffness: 520, damping: 34, mass: 0.45 } as const;\nconst STILL = { duration: 0 } as const;\n\nexport type TaskStep = {\n  id: string;\n  label: string;\n\n  meta?: string;\n};\n\nexport type TaskStepStatus = \"pending\" | \"active\" | \"done\" | \"error\";\n\nexport type UseTaskStepsOptions = {\n  steps: TaskStep[];\n\n  current: number;\n\n  failed?: boolean;\n};\n\nexport function useTaskSteps({ steps, current, failed = false }: UseTaskStepsOptions) {\n  const complete = !failed && current >= steps.length;\n\n  const rows = steps.map((step, i) => ({\n    ...step,\n    status: (i < current\n      ? \"done\"\n      : i === current && failed\n        ? \"error\"\n        : i === current && !complete\n          ? \"active\"\n          : \"pending\") as TaskStepStatus,\n  }));\n\n  const active = rows.find((r) => r.status === \"active\");\n  const sentence = failed\n    ? `Failed at ${steps[Math.min(current, steps.length - 1)]?.label ?? \"step\"}`\n    : complete\n      ? `All ${steps.length} steps complete`\n      : active\n        ? `${active.label}, step ${current + 1} of ${steps.length}`\n        : \"\";\n\n  return { rows, complete, failed, sentence };\n}\n\nconst Tick = (\n  <svg viewBox=\"0 0 256 256\" width=\"11\" height=\"11\" fill=\"none\" aria-hidden>\n    <polyline\n      points=\"216 72 104 184 48 128\"\n      stroke=\"currentColor\"\n      strokeWidth=\"26\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst Cross = (\n  <svg viewBox=\"0 0 256 256\" width=\"10\" height=\"10\" fill=\"none\" aria-hidden>\n    <path\n      d=\"M200 56 56 200 M56 56l144 144\"\n      stroke=\"currentColor\"\n      strokeWidth=\"26\"\n      strokeLinecap=\"round\"\n    />\n  </svg>\n);\n\nconst Arc = ({ spin }: { spin: boolean }) => (\n  <motion.svg\n    viewBox=\"0 0 16 16\"\n    className=\"size-3\"\n    aria-hidden\n    animate={spin ? { rotate: 360 } : { rotate: 0 }}\n    transition={spin ? { duration: 0.8, ease: \"linear\", repeat: Infinity } : STILL}\n  >\n    <circle\n      cx=\"8\"\n      cy=\"8\"\n      r=\"6\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeOpacity=\"0.25\"\n      strokeWidth=\"2\"\n    />\n    <path\n      d=\"M8 2 a6 6 0 0 1 6 6\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n    />\n  </motion.svg>\n);\n\nexport type TaskStepsProps = UseTaskStepsOptions & {\n  label?: string;\n  className?: string;\n};\n\nexport function TaskSteps({\n  steps,\n  current,\n  failed = false,\n  label = \"Task progress\",\n  className = \"\",\n}: TaskStepsProps) {\n  const { rows, complete, sentence } = useTaskSteps({ steps, current, failed });\n  const reduced = useReducedMotion() === true;\n\n  const [spoken, setSpoken] = useState(\"\");\n  useEffect(() => {\n    if (!sentence) return;\n    const t = setTimeout(() => setSpoken(sentence), 500);\n    return () => clearTimeout(t);\n  }, [sentence]);\n\n  return (\n    <div className={`w-full ${className}`}>\n      <ol aria-label={label} className=\"space-y-0.5\">\n        {rows.map((row) => {\n          const tone =\n            row.status === \"done\"\n              ? \"text-stone-600 dark:text-stone-300\"\n              : row.status === \"active\"\n                ? \"font-medium text-stone-800 dark:text-stone-100\"\n                : row.status === \"error\"\n                  ? \"font-medium text-red-600 dark:text-red-400\"\n                  : \"text-stone-400 dark:text-stone-500\";\n\n          return (\n            <li\n              key={row.id}\n              aria-current={row.status === \"active\" ? \"step\" : undefined}\n              className=\"flex h-7 items-center gap-2.5 px-1\"\n            >\n              <span className=\"relative grid size-4 shrink-0 place-items-center\">\n                <AnimatePresence initial={false}>\n                  {row.status === \"done\" ? (\n                    <motion.span\n                      key=\"done\"\n                      className=\"col-start-1 row-start-1 grid size-4 place-items-center rounded-[5px] bg-emerald-500/[0.14] text-emerald-600 dark:bg-emerald-400/[0.16] dark:text-emerald-400\"\n                      initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.4 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      exit={{ opacity: 0, transition: STILL }}\n                      transition={reduced ? STILL : POP}\n                    >\n                      {Tick}\n                    </motion.span>\n                  ) : row.status === \"error\" ? (\n                    <motion.span\n                      key=\"error\"\n                      className=\"col-start-1 row-start-1 grid size-4 place-items-center rounded-[5px] bg-red-500/[0.12] text-red-600 dark:bg-red-400/[0.14] dark:text-red-400\"\n                      initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.4 }}\n                      animate={{ opacity: 1, scale: 1 }}\n                      exit={{ opacity: 0, transition: STILL }}\n                      transition={reduced ? STILL : POP}\n                    >\n                      {Cross}\n                    </motion.span>\n                  ) : row.status === \"active\" ? (\n                    <motion.span\n                      key=\"active\"\n                      className=\"col-start-1 row-start-1 text-stone-500 dark:text-stone-400\"\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0, transition: STILL }}\n                      transition={reduced ? STILL : CELL}\n                    >\n                      <Arc spin />\n                    </motion.span>\n                  ) : (\n                    <motion.span\n                      key=\"pending\"\n                      className=\"col-start-1 row-start-1 size-[5px] rounded-[2px] bg-stone-300 dark:bg-white/20\"\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0, transition: STILL }}\n                      transition={STILL}\n                    />\n                  )}\n                </AnimatePresence>\n              </span>\n\n              {row.status === \"active\" && !reduced ? (\n                <motion.span\n                  className=\"min-w-0 flex-1 truncate bg-[linear-gradient(90deg,#78716c_38%,#1c1917_50%,#78716c_62%)] bg-clip-text text-[12.5px] font-medium text-transparent [background-size:220%_100%] dark:bg-[linear-gradient(90deg,#a8a29e_38%,#fafaf9_50%,#a8a29e_62%)]\"\n                  animate={{ backgroundPosition: [\"120% 0\", \"-120% 0\"] }}\n                  transition={{ duration: 1.6, ease: \"linear\", repeat: Infinity }}\n                >\n                  {row.label}\n                </motion.span>\n              ) : (\n                <span\n                  className={`min-w-0 flex-1 truncate text-[12.5px] transition-colors duration-200 ${tone}`}\n                >\n                  {row.label}\n                </span>\n              )}\n\n              {row.meta ? (\n                <span\n                  className={`shrink-0 font-mono text-[10.5px] tabular-nums transition-opacity duration-200 ${\n                    row.status === \"done\"\n                      ? \"text-stone-400 opacity-100 dark:text-stone-500\"\n                      : \"opacity-0\"\n                  }`}\n                  aria-hidden={row.status !== \"done\"}\n                >\n                  {row.meta}\n                </span>\n              ) : null}\n            </li>\n          );\n        })}\n      </ol>\n      <span role=\"status\" className=\"sr-only\">\n        {spoken}\n      </span>\n      <span className=\"sr-only\" aria-live={complete || failed ? \"polite\" : \"off\"}>\n        {complete ? \"Run complete\" : failed ? \"Run failed\" : \"\"}\n      </span>\n    </div>\n  );\n}\n"}]}