{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "launch-list",
  "type": "registry:component",
  "title": "Launch List",
  "description": "A getting-started list in tidy groups with a ring that fills as you go, where each task ticks and strikes through once it's done.",
  "categories": [
    "component",
    "Getting Started"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/launch-list.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ChevronDownIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LaunchTask {\n  id: string;\n  label: string;\n  hint?: string;\n  done?: boolean;\n}\n\nexport interface LaunchGroup {\n  id: string;\n  title: string;\n  tasks: LaunchTask[];\n}\n\nexport interface LaunchListProps {\n  title?: string;\n  description?: string;\n  groups: LaunchGroup[];\n  defaultOpen?: string[];\n  onToggleTask?: (taskId: string, done: boolean) => void;\n  className?: string;\n}\n\nconst spring = { type: \"spring\", stiffness: 380, damping: 32 } as const;\n\nfunction Ring({ value, total }: { value: number; total: number }) {\n  const reduce = useReducedMotion();\n  const r = 15;\n  const c = 2 * Math.PI * r;\n  const pct = total ? value / total : 0;\n  return (\n    <div className=\"relative size-11 shrink-0\">\n      <svg aria-hidden className=\"-rotate-90 size-full\" viewBox=\"0 0 36 36\">\n        <circle className=\"stroke-muted\" cx=\"18\" cy=\"18\" fill=\"none\" r={r} strokeWidth=\"3.5\" />\n        <motion.circle\n          animate={{ strokeDashoffset: c * (1 - pct) }}\n          className={pct === 1 ? \"stroke-success\" : \"stroke-primary\"}\n          cx=\"18\"\n          cy=\"18\"\n          fill=\"none\"\n          initial={false}\n          r={r}\n          strokeDasharray={c}\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.5\"\n          transition={reduce ? { duration: 0 } : { type: \"spring\", stiffness: 120, damping: 20 }}\n        />\n      </svg>\n      <span className=\"absolute inset-0 flex items-center justify-center font-medium text-[11px] tabular-nums\">\n        {Math.round(pct * 100)}%\n      </span>\n    </div>\n  );\n}\n\nfunction TaskCheck({ done }: { done: boolean }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      animate={done && !reduce ? { scale: [1, 1.12, 1] } : { scale: 1 }}\n      className={cn(\n        \"flex size-[18px] shrink-0 items-center justify-center rounded-full border-[1.5px] transition-[background-color,border-color,scale] duration-150 ease-out group-active/task:scale-90\",\n        done ? \"border-success bg-success text-white\" : \"border-muted-foreground/40 bg-background group-hover/task:border-foreground/50\",\n      )}\n      transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}\n    >\n      <svg aria-hidden className=\"size-3\" fill=\"none\" viewBox=\"0 0 16 16\">\n        <motion.path\n          animate={{ pathLength: done ? 1 : 0, opacity: done ? 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.4}\n          transition={reduce ? { duration: 0 } : { duration: 0.28, delay: done ? 0.08 : 0, ease: \"easeOut\" }}\n        />\n      </svg>\n    </motion.span>\n  );\n}\n\nexport function LaunchList({\n  title = \"Get set up\",\n  description,\n  groups: initial,\n  defaultOpen,\n  onToggleTask,\n  className,\n}: LaunchListProps) {\n  const reduce = useReducedMotion();\n  const baseId = useId();\n  const [groups, setGroups] = useState(initial);\n  const [open, setOpen] = useState<string[]>(\n    () => defaultOpen ?? [initial.find((g) => g.tasks.some((t) => !t.done))?.id ?? initial[0]?.id].filter(Boolean),\n  );\n\n  const all = groups.flatMap((g) => g.tasks);\n  const done = all.filter((t) => t.done).length;\n\n  const toggle = (taskId: string) => {\n    const next = !all.find((t) => t.id === taskId)?.done;\n    setGroups((gs) =>\n      gs.map((g) => ({ ...g, tasks: g.tasks.map((t) => (t.id === taskId ? { ...t, done: next } : t)) })),\n    );\n    onToggleTask?.(taskId, next);\n  };\n\n  return (\n    <section\n      aria-label={title}\n      className={cn(\"w-full max-w-[400px] overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n    >\n      <header className=\"flex items-center gap-3 border-b p-4\">\n        <Ring total={all.length} value={done} />\n        <div className=\"min-w-0 flex-1\">\n          <h2 className=\"font-medium text-sm\">{title}</h2>\n          <p className=\"text-muted-foreground text-xs\">\n            {description ?? (\n              <>\n                <span className=\"tabular-nums\">\n                  {done} of {all.length}\n                </span>{\" \"}\n                done{done === all.length ? \" – nice work\" : \"\"}\n              </>\n            )}\n          </p>\n        </div>\n      </header>\n      <div className=\"divide-y\">\n        {groups.map((g) => {\n          const isOpen = open.includes(g.id);\n          const gDone = g.tasks.filter((t) => t.done).length;\n          const complete = gDone === g.tasks.length;\n          const panelId = `${baseId}-${g.id}`;\n          return (\n            <div key={g.id}>\n              <button\n                aria-controls={panelId}\n                aria-expanded={isOpen}\n                className=\"flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-left outline-none transition-colors hover:bg-accent/50 focus-visible:bg-accent/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\"\n                onClick={() => setOpen((o) => (o.includes(g.id) ? o.filter((x) => x !== g.id) : [...o, g.id]))}\n                type=\"button\"\n              >\n                <span className=\"flex-1 font-medium text-sm\">{g.title}</span>\n                <span\n                  className={cn(\n                    \"rounded-full px-1.5 py-px font-medium text-[11px] tabular-nums transition-colors\",\n                    complete ? \"bg-success/12 text-success-foreground\" : \"bg-muted text-muted-foreground\",\n                  )}\n                >\n                  {gDone}/{g.tasks.length}\n                </span>\n                <motion.span\n                  animate={{ rotate: isOpen ? 180 : 0 }}\n                  className=\"text-muted-foreground\"\n                  transition={reduce ? { duration: 0 } : spring}\n                >\n                  <ChevronDownIcon className=\"size-4\" />\n                </motion.span>\n              </button>\n              <AnimatePresence initial={false}>\n                {isOpen && (\n                  <motion.div\n                    animate={{ height: \"auto\", opacity: 1 }}\n                    className=\"overflow-hidden\"\n                    exit={{ height: 0, opacity: 0 }}\n                    id={panelId}\n                    initial={{ height: 0, opacity: 0 }}\n                    transition={reduce ? { duration: 0 } : { ...spring, opacity: { duration: 0.18 } }}\n                  >\n                    <ul className=\"space-y-0.5 px-2 pb-2\">\n                      {g.tasks.map((t) => (\n                        <li key={t.id}>\n                          <button\n                            aria-pressed={!!t.done}\n                            className=\"group/task flex w-full cursor-pointer items-start gap-3 rounded-lg px-2 py-2 text-left outline-none transition-colors hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring\"\n                            onClick={() => toggle(t.id)}\n                            type=\"button\"\n                          >\n                            <span className=\"flex h-5 items-center\">\n                              <TaskCheck done={!!t.done} />\n                            </span>\n                            <span className=\"min-w-0 flex-1 text-sm leading-5\">\n                              <span className=\"relative inline-block text-sm leading-5\">\n                                <span\n                                  className={cn(\n                                    \"transition-colors duration-300\",\n                                    t.done ? \"text-muted-foreground\" : \"text-foreground\",\n                                  )}\n                                >\n                                  {t.label}\n                                </span>\n                                <motion.span\n                                  animate={{ scaleX: t.done ? 1 : 0 }}\n                                  aria-hidden\n                                  className=\"absolute top-1/2 right-0 left-0 h-px origin-left bg-muted-foreground\"\n                                  initial={false}\n                                  transition={\n                                    reduce ? { duration: 0 } : { duration: 0.3, delay: t.done ? 0.15 : 0, ease: \"easeOut\" }\n                                  }\n                                />\n                              </span>\n                              {t.hint && <span className=\"block text-muted-foreground text-xs\">{t.hint}</span>}\n                            </span>\n                          </button>\n                        </li>\n                      ))}\n                    </ul>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          );\n        })}\n      </div>\n    </section>\n  );\n}\n"
    }
  ]
}