{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "onboarding-checklist",
  "title": "Onboarding Checklist",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A get-started checklist with a progress ring and segmented bar, expandable steps with actions, checkable tasks and a short celebration when everything is done.",
  "dependencies": [
    "motion@^12",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/motion-presets.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/onboarding-checklist.tsx",
      "content": "// Ballmac UI: Onboarding Checklist. https://ui.ballmac.com/components/onboarding-checklist\n\"use client\";\n\nimport * as React from \"react\";\nimport { Check, ChevronDown, PartyPopper, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { duration, ease, spring } from \"@/lib/ballmac/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { useMessages } from \"@/lib/ballmac/i18n\";\n\ntype OnboardingStep = {\n  /** Unique id. */\n  id: string;\n  /** Short task name. */\n  title: string;\n  /** One or two sentences shown when the step is open. */\n  description?: React.ReactNode;\n  /** Button shown inside the open step, for example \"Invite teammates\". */\n  action?: { label: string; onClick?: () => void; href?: string };\n};\n\ntype OnboardingChecklistProps = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\" | \"defaultValue\"> & {\n  /** Tasks, in the order you want people to do them. */\n  steps: OnboardingStep[];\n  /** Controlled list of completed step ids. */\n  completed?: string[];\n  /** Initial completed ids when uncontrolled. */\n  defaultCompleted?: string[];\n  /** Called with the new list of completed ids whenever one is checked or unchecked. */\n  onCompletedChange?: (completed: string[]) => void;\n  /** Heading. */\n  title?: string;\n  /** Text under the heading while tasks remain. */\n  description?: string;\n  /** Message shown once every step is done. */\n  completeMessage?: string;\n  /** Which step is open at first. Defaults to the first unfinished one. */\n  defaultOpenId?: string;\n  /** Adds a dismiss button and calls this when it is pressed. */\n  onDismiss?: () => void;\n};\n\nfunction Ring({ done, total }: { done: number; total: number }) {\n  const reduce = useReducedMotion();\n  const r = 15;\n  const c = 2 * Math.PI * r;\n  const fraction = total ? done / total : 0;\n  return (\n    <div className=\"relative size-11 shrink-0\" aria-hidden=\"true\">\n      <svg viewBox=\"0 0 36 36\" className=\"size-full -rotate-90\">\n        <circle cx=\"18\" cy=\"18\" r={r} fill=\"none\" strokeWidth=\"3\" className=\"stroke-muted\" />\n        <motion.circle\n          cx=\"18\"\n          cy=\"18\"\n          r={r}\n          fill=\"none\"\n          strokeWidth=\"3\"\n          strokeLinecap=\"round\"\n          className=\"stroke-primary\"\n          strokeDasharray={c}\n          initial={false}\n          animate={{ strokeDashoffset: c * (1 - fraction) }}\n          transition={reduce ? { duration: 0 } : { duration: 0.6, ease: ease.out }}\n        />\n      </svg>\n      <span className=\"absolute inset-0 flex items-center justify-center text-[11px] font-semibold tabular-nums\">\n        {done}/{total}\n      </span>\n    </div>\n  );\n}\n\n/**\n * A \"get started\" list with a progress ring and segmented bar. Each task can be checked off, and the open task shows its\n * description and a call to action. When everything is done the list swaps to a short celebration.\n */\nfunction OnboardingChecklist({\n  steps,\n  completed: completedProp,\n  defaultCompleted = [],\n  onCompletedChange,\n  title,\n  description,\n  completeMessage,\n  defaultOpenId,\n  onDismiss,\n  className,\n  ...props\n}: OnboardingChecklistProps) {\n  const msg = useMessages()\n  title ??= msg(\"onboarding-checklist.title\", \"Get started\")\n  description ??= msg(\"onboarding-checklist.description\", \"Finish these steps to set up your workspace.\")\n  completeMessage ??= msg(\"onboarding-checklist.completeMessage\", \"You're all set. Nice work.\")\n  const reduce = useReducedMotion();\n  const [inner, setInner] = React.useState(defaultCompleted);\n  const completed = completedProp ?? inner;\n  const doneSet = new Set(completed);\n  const doneCount = steps.filter((s) => doneSet.has(s.id)).length;\n  const allDone = steps.length > 0 && doneCount === steps.length;\n  const firstOpen = defaultOpenId ?? steps.find((s) => !doneSet.has(s.id))?.id;\n  const [openId, setOpenId] = React.useState<string | undefined>(firstOpen);\n  const baseId = React.useId();\n\n  function toggle(id: string) {\n    const next = doneSet.has(id) ? completed.filter((c) => c !== id) : [...completed, id];\n    if (completedProp === undefined) setInner(next);\n    onCompletedChange?.(next);\n    if (!doneSet.has(id)) {\n      const after = steps.find((s) => !new Set(next).has(s.id));\n      setOpenId(after?.id);\n    }\n  }\n\n  return (\n    <section\n      data-slot=\"onboarding-checklist\"\n      aria-label={title}\n      className={cn(\n        \"relative w-full overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_12px_32px_-16px_rgb(0_0_0/0.16)]\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-x-0 top-0 h-28 bg-gradient-to-b from-chart-2/[0.09] to-transparent\"\n      />\n      <header className=\"relative flex items-start gap-4 p-5 pb-4\">\n        <Ring done={doneCount} total={steps.length} />\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"text-base font-semibold tracking-tight\">{title}</h3>\n          <p className=\"mt-0.5 text-sm text-muted-foreground\" aria-live=\"polite\">\n            {allDone ? \"All steps complete\" : description}\n          </p>\n        </div>\n        {onDismiss && (\n          <button\n            type=\"button\"\n            aria-label={msg(\"onboarding-checklist.dismissChecklist\", \"Dismiss checklist\")}\n            onClick={onDismiss}\n            className=\"-mt-1 -me-1 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n          >\n            <X aria-hidden=\"true\" className=\"size-4\" />\n          </button>\n        )}\n      </header>\n\n      <div className=\"relative px-5 pb-4\" aria-hidden=\"true\">\n        <div className=\"flex gap-1\">\n          {steps.map((s) => (\n            <motion.span\n              key={s.id}\n              className=\"h-1.5 flex-1 rounded-full\"\n              initial={false}\n              animate={{ backgroundColor: doneSet.has(s.id) ? \"var(--primary)\" : \"var(--muted)\" }}\n              transition={{ duration: reduce ? 0 : duration.slow }}\n            />\n          ))}\n        </div>\n      </div>\n\n      <AnimatePresence initial={false} mode=\"wait\">\n        {allDone ? (\n          <motion.div\n            key=\"done\"\n            initial={reduce ? false : { opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            exit={{ opacity: 0 }}\n            transition={spring.gentle}\n            className=\"relative flex flex-col items-center gap-3 px-5 pt-3 pb-8 text-center\"\n          >\n            <span className=\"flex size-12 items-center justify-center rounded-full bg-chart-2/15 text-chart-2\">\n              <PartyPopper aria-hidden=\"true\" className=\"size-6\" />\n            </span>\n            <p className=\"text-sm font-medium\">{completeMessage}</p>\n            <button\n              type=\"button\"\n              onClick={() => {\n                const next: string[] = [];\n                if (completedProp === undefined) setInner(next);\n                onCompletedChange?.(next);\n                setOpenId(steps[0]?.id);\n              }}\n              className=\"text-xs font-medium text-muted-foreground underline-offset-4 outline-none hover:text-foreground hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            >\n              {msg(\"onboarding-checklist.reviewTheStepsAgain\", \"Review the steps again\")}\n            </button>\n          </motion.div>\n        ) : (\n          <ul key=\"steps\" className=\"relative grid gap-0.5 px-2 pb-2\">\n            {steps.map((step, index) => {\n              const done = doneSet.has(step.id);\n              const open = openId === step.id;\n              const panelId = `${baseId}-${step.id}`;\n              return (\n                <li\n                  key={step.id}\n                  data-done={done || undefined}\n                  className={cn(\n                    \"rounded-xl transition-colors duration-200 motion-reduce:transition-none\",\n                    open && !done && \"bg-muted/60\",\n                  )}\n                >\n                  <div className=\"flex items-center gap-3 px-3 py-2.5\">\n                    <button\n                      type=\"button\"\n                      role=\"checkbox\"\n                      aria-checked={done}\n                      aria-label={done ? msg(\"onboarding-checklist.markNotDone\", \"{title}: mark as not done\", { title: step.title }) : msg(\"onboarding-checklist.markDone\", \"{title}: mark as done\", { title: step.title })}\n                      onClick={() => toggle(step.id)}\n                      className={cn(\n                        \"relative flex size-6 shrink-0 items-center justify-center rounded-full border outline-none transition-[background-color,border-color,box-shadow] duration-200 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\",\n                        \"after:absolute after:-inset-2 after:content-['']\",\n                        done\n                          ? \"border-primary bg-primary text-primary-foreground\"\n                          : \"border-input bg-background hover:border-foreground/40\",\n                      )}\n                    >\n                      <AnimatePresence initial={false}>\n                        {done ? (\n                          <motion.span\n                            key=\"check\"\n                            initial={reduce ? false : { scale: 0.4, opacity: 0 }}\n                            animate={{ scale: 1, opacity: 1 }}\n                            exit={{ scale: 0.4, opacity: 0 }}\n                            transition={spring.bouncy}\n                          >\n                            <Check aria-hidden=\"true\" className=\"size-3.5\" strokeWidth={3} />\n                          </motion.span>\n                        ) : (\n                          <span aria-hidden=\"true\" className=\"text-[11px] font-medium text-muted-foreground tabular-nums\">\n                            {index + 1}\n                          </span>\n                        )}\n                      </AnimatePresence>\n                    </button>\n                    <button\n                      type=\"button\"\n                      aria-expanded={open}\n                      aria-controls={panelId}\n                      onClick={() => setOpenId(open ? undefined : step.id)}\n                      className=\"flex min-w-0 flex-1 items-center justify-between gap-2 rounded-md text-start outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                    >\n                      <span\n                        className={cn(\n                          \"truncate text-sm font-medium transition-colors\",\n                          done && \"text-muted-foreground line-through decoration-muted-foreground/50\",\n                        )}\n                      >\n                        {step.title}\n                      </span>\n                      <ChevronDown\n                        aria-hidden=\"true\"\n                        className={cn(\n                          \"size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none\",\n                          open && \"rotate-180\",\n                        )}\n                      />\n                    </button>\n                  </div>\n                  <AnimatePresence initial={false}>\n                    {open && (\n                      <motion.div\n                        id={panelId}\n                        role=\"region\"\n                        aria-label={step.title}\n                        initial={reduce ? false : { height: 0, opacity: 0 }}\n                        animate={{ height: \"auto\", opacity: 1 }}\n                        exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}\n                        transition={{ duration: reduce ? 0 : duration.base, ease: ease.out }}\n                        className=\"overflow-hidden\"\n                      >\n                        <div className=\"grid gap-3 pe-3 pb-3.5 ps-[3.25rem] text-sm text-muted-foreground\">\n                          {step.description && <p className=\"leading-relaxed\">{step.description}</p>}\n                          {step.action &&\n                            (step.action.href ? (\n                              <a\n                                href={step.action.href}\n                                onClick={step.action.onClick}\n                                className=\"inline-flex h-8 w-fit items-center rounded-md bg-primary px-3 text-[13px] font-medium text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                              >\n                                {step.action.label}\n                              </a>\n                            ) : (\n                              <button\n                                type=\"button\"\n                                onClick={step.action.onClick}\n                                className=\"inline-flex h-8 w-fit items-center rounded-md bg-primary px-3 text-[13px] font-medium text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n                              >\n                                {step.action.label}\n                              </button>\n                            ))}\n                        </div>\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </li>\n              );\n            })}\n          </ul>\n        )}\n      </AnimatePresence>\n    </section>\n  );\n}\n\nexport { OnboardingChecklist, type OnboardingChecklistProps, type OnboardingStep };\n",
      "type": "registry:ui",
      "target": "@components/ballmac/onboarding-checklist.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "onboarding",
      "checklist",
      "progress",
      "activation"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "steps: {id,title,description,action}. completed/defaultCompleted/onCompletedChange track progress. The open step shows its description and action.",
      "whenToUse": [
        "First-run setup for new accounts",
        "Activation nudges in a dashboard corner"
      ],
      "whenNotToUse": [
        "Multi-page wizards; use stepper-form",
        "A plain progress bar; use progress"
      ],
      "composesWith": [
        "progress-ring",
        "card",
        "button"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Reaches each step's checkbox and its expand button"
        },
        {
          "keys": "Space",
          "action": "Toggles a step done (role checkbox)"
        },
        {
          "keys": "Enter / Space on the title",
          "action": "Expands or collapses the step (aria-expanded)"
        },
        {
          "keys": "Reduced motion",
          "action": "Check, bar and panel changes are instant"
        }
      ],
      "customization": [
        "controlled completed ids",
        "defaultOpenId",
        "onDismiss",
        "completeMessage"
      ]
    },
    "examples": [
      "onboarding-checklist-demo",
      "onboarding-checklist-states"
    ],
    "url": "https://ui.ballmac.com/components/onboarding-checklist"
  },
  "categories": [
    "saas"
  ],
  "type": "registry:ui"
}