{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "milestone-rail",
  "type": "registry:component",
  "title": "Milestone Rail",
  "description": "Your onboarding steps on a vertical rail that fills in as each one is done, with a progress bar and percentage keeping count up top.",
  "categories": [
    "component",
    "Getting Started"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/milestone-rail.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { PauseIcon, PlayIcon, RotateCcwIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MilestoneStep {\n  id: string;\n  title: string;\n  description?: string;\n}\n\nexport interface MilestoneRailProps {\n  steps: MilestoneStep[];\n  title?: string;\n  initialDone?: number;\n  autoPlay?: boolean;\n  stepMs?: number;\n  onChange?: (done: number) => void;\n  className?: string;\n}\n\nconst ROW = 60;\nconst NODE = 22;\nconst spring = { type: \"spring\", stiffness: 380, damping: 30 } as const;\nconst iconButton = \"transition-[scale,background-color] duration-150 ease-out after:absolute after:-inset-1 active:scale-[0.97]\";\n\nfunction Percent({ value }: { value: number }) {\n  const reduce = useReducedMotion();\n  const digits = String(value).split(\"\");\n  return (\n    <span aria-hidden className=\"inline-flex overflow-hidden tabular-nums\">\n      {digits.map((d, i) => (\n        <span className=\"relative inline-flex h-[1.2em] w-[1ch] justify-center overflow-hidden\" key={digits.length - i}>\n          <AnimatePresence initial={false} mode=\"popLayout\">\n            <motion.span\n              animate={{ y: 0, opacity: 1 }}\n              exit={reduce ? { opacity: 0 } : { y: \"-100%\", opacity: 0 }}\n              initial={reduce ? { opacity: 0 } : { y: \"100%\", opacity: 0 }}\n              key={d}\n              transition={spring}\n            >\n              {d}\n            </motion.span>\n          </AnimatePresence>\n        </span>\n      ))}\n      %\n    </span>\n  );\n}\n\nexport function MilestoneRail({\n  steps,\n  title = \"Getting started\",\n  initialDone = 0,\n  autoPlay = false,\n  stepMs = 1400,\n  onChange,\n  className,\n}: MilestoneRailProps) {\n  const reduce = useReducedMotion();\n  const [done, setDone] = useState(initialDone);\n  const [playing, setPlaying] = useState(autoPlay);\n  const total = steps.length;\n  const complete = done >= total;\n  const pct = Math.round((Math.min(done, total) / total) * 100);\n\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n  useEffect(() => {\n    onChangeRef.current?.(done);\n  }, [done]);\n\n  useEffect(() => {\n    if (!playing) return;\n    const t = setTimeout(() => setDone((d) => (d >= total ? 0 : d + 1)), done >= total ? stepMs * 1.8 : stepMs);\n    return () => clearTimeout(t);\n  }, [playing, done, total, stepMs]);\n\n  const activeIdx = Math.min(done, total - 1);\n  const fillTo = activeIdx * ROW;\n\n  return (\n    <section\n      aria-label={title}\n      className={cn(\"w-full max-w-[380px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n    >\n      <header className=\"p-4 pb-3\">\n        <div className=\"flex items-center gap-2\">\n          <h2 className=\"font-medium text-sm\">{title}</h2>\n          <AnimatePresence initial={false}>\n            {complete && (\n              <motion.span\n                animate={{ opacity: 1, scale: 1 }}\n                className=\"rounded-full bg-success/12 px-1.5 py-px font-medium text-[11px] text-success-foreground\"\n                exit={{ opacity: 0, scale: 0.8 }}\n                initial={reduce ? false : { opacity: 0, scale: 0.6 }}\n                transition={{ type: \"spring\", stiffness: 500, damping: 18 }}\n              >\n                All done\n              </motion.span>\n            )}\n          </AnimatePresence>\n          <div className=\"ms-auto flex items-center gap-0.5\">\n            <Button\n              aria-label={playing ? \"Pause demo\" : \"Play demo\"}\n              className={iconButton}\n              onClick={() => setPlaying((p) => !p)}\n              size=\"icon-xs\"\n              variant=\"ghost\"\n            >\n              {playing ? <PauseIcon /> : <PlayIcon />}\n            </Button>\n            <Button aria-label=\"Restart\" className={iconButton} onClick={() => setDone(0)} size=\"icon-xs\" variant=\"ghost\">\n              <RotateCcwIcon />\n            </Button>\n          </div>\n        </div>\n        <div className=\"mt-3 flex items-center gap-3\">\n          <div\n            aria-label=\"Progress\"\n            aria-valuemax={100}\n            aria-valuemin={0}\n            aria-valuenow={pct}\n            className=\"relative h-1.5 flex-1 overflow-hidden rounded-full bg-muted\"\n            role=\"progressbar\"\n          >\n            <motion.div\n              animate={{ x: `${pct - 100}%` }}\n              className={cn(\n                \"absolute inset-0 rounded-full transition-colors duration-500\",\n                complete ? \"bg-success\" : \"bg-primary\",\n              )}\n              initial={false}\n              transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 140, damping: 24 }}\n            />\n          </div>\n          <span className=\"w-10 text-right font-medium text-xs\">\n            <Percent value={pct} />\n            <span className=\"sr-only\">{pct}%</span>\n          </span>\n        </div>\n      </header>\n\n      <div className=\"relative px-4 pb-3\">\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute top-0 left-4\"\n          style={{ width: NODE, height: total * ROW }}\n        >\n          <span\n            className=\"absolute left-1/2 w-0.5 -translate-x-1/2 rounded-full bg-border\"\n            style={{ top: ROW / 2, height: (total - 1) * ROW }}\n          />\n          <motion.span\n            animate={{ scaleY: total > 1 ? fillTo / ((total - 1) * ROW) : 0 }}\n            className={cn(\n              \"absolute left-1/2 w-0.5 -translate-x-1/2 origin-top rounded-full transition-colors duration-500\",\n              complete ? \"bg-success\" : \"bg-primary\",\n            )}\n            initial={false}\n            style={{ top: ROW / 2, height: (total - 1) * ROW }}\n            transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 170, damping: 26 }}\n          />\n        </div>\n\n        <ol className=\"relative\">\n          {steps.map((s, i) => {\n            const isDone = i < done;\n            const isActive = i === done;\n            return (\n              <li key={s.id} style={{ height: ROW }}>\n                <button\n                  aria-current={isActive ? \"step\" : undefined}\n                  aria-label={`${s.title}${isDone ? \", done\" : isActive ? \", in progress\" : \"\"}. Set progress here.`}\n                  className=\"group/step flex h-full w-full cursor-pointer items-center gap-3 rounded-xl text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                  onClick={() => {\n                    setPlaying(false);\n                    setDone(isDone && done === i + 1 ? i : i + 1);\n                  }}\n                  type=\"button\"\n                >\n                  <span\n                    className=\"relative flex shrink-0 items-center justify-center rounded-full\"\n                    style={{ width: NODE, height: NODE }}\n                  >\n                    <span\n                      className={cn(\n                        \"absolute inset-0 rounded-full border-2 bg-card transition-colors duration-300\",\n                        isActive ? \"border-primary\" : \"border-muted-foreground/30 group-hover/step:border-foreground/50\",\n                      )}\n                    />\n                    {isActive && (\n                      <motion.span\n                        animate={reduce ? { opacity: 0 } : { scale: [1, 1.7], opacity: [0.25, 0] }}\n                        className=\"absolute inset-0 rounded-full bg-primary opacity-0\"\n                        transition={reduce ? { duration: 0 } : { duration: 1.4, repeat: Number.POSITIVE_INFINITY, ease: \"easeOut\" }}\n                      />\n                    )}\n                    <AnimatePresence initial={false}>\n                      {isActive && (\n                        <motion.span\n                          animate={{ scale: 1, opacity: 1 }}\n                          className=\"relative size-2 rounded-full bg-primary\"\n                          exit={{ scale: 0.5, opacity: 0, transition: { duration: 0.12 } }}\n                          initial={reduce ? { opacity: 0 } : { scale: 0.5, opacity: 0 }}\n                          transition={spring}\n                        />\n                      )}\n                    </AnimatePresence>\n                    <motion.span\n                      animate={{ scale: isDone ? 1 : 0.4, opacity: isDone ? 1 : 0 }}\n                      className={cn(\n                        \"absolute inset-0 rounded-full transition-colors duration-500\",\n                        complete ? \"bg-success\" : \"bg-primary\",\n                      )}\n                      initial={false}\n                      transition={\n                        reduce ? { duration: 0 } : { type: \"spring\", stiffness: 500, damping: 24, delay: isDone ? 0.12 : 0 }\n                      }\n                    />\n                    <svg\n                      aria-hidden\n                      className={cn(\"absolute size-3\", complete ? \"text-white\" : \"text-primary-foreground\")}\n                      fill=\"none\"\n                      viewBox=\"0 0 16 16\"\n                    >\n                      <motion.path\n                        animate={{ pathLength: isDone ? 1 : 0, opacity: isDone ? 1 : 0 }}\n                        d=\"M3.5 8.5l3 3 6-7\"\n                        initial={false}\n                        stroke=\"currentColor\"\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        strokeWidth={2.6}\n                        transition={reduce ? { duration: 0 } : { duration: 0.25, delay: isDone ? 0.22 : 0, ease: \"easeOut\" }}\n                      />\n                    </svg>\n                  </span>\n                  <span className=\"min-w-0 flex-1\">\n                    <span\n                      className={cn(\n                        \"block truncate font-medium text-sm transition-colors duration-300\",\n                        isDone ? \"text-muted-foreground\" : isActive ? \"text-foreground\" : \"text-foreground/70\",\n                      )}\n                    >\n                      {s.title}\n                    </span>\n                    {s.description && (\n                      <span className=\"block truncate text-muted-foreground text-xs\">{s.description}</span>\n                    )}\n                  </span>\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    {isActive && (\n                      <motion.span\n                        animate={{ opacity: 1, x: 0 }}\n                        className=\"shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground\"\n                        exit={{ opacity: 0, x: 6 }}\n                        initial={reduce ? false : { opacity: 0, x: 6 }}\n                        transition={spring}\n                      >\n                        Up next\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </button>\n              </li>\n            );\n          })}\n        </ol>\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}