{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "setup-checklist",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "setup-checklist.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  stiffness: 340,\n  damping: 32,\n};\n\nconst BADGE_SPRING = {\n  type: \"spring\" as const,\n  stiffness: 520,\n  damping: 28,\n};\n\nconst PIE_RADIUS = 4.5;\n\ntype SetupChecklistContextValue = {\n  completed: string[];\n  fraction: number;\n  toggle: (id: string) => void;\n};\n\nconst SetupChecklistContext = createContext<SetupChecklistContextValue | null>(\n  null\n);\n\nfunction useSetupChecklistContext(component: string) {\n  const context = useContext(SetupChecklistContext);\n\n  if (!context) {\n    throw new Error(`${component} must be used within <SetupChecklist>.`);\n  }\n\n  return context;\n}\n\n/** Walks the composed children and collects the ids of every SetupChecklistItem. */\nfunction collectItemIds(children: ReactNode, ids: string[]): string[] {\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    const type = child.type as { isSetupChecklistItem?: boolean };\n\n    if (type === SetupChecklistItem || type.isSetupChecklistItem === true) {\n      const { id } = child.props as SetupChecklistItemProps;\n\n      if (id && !ids.includes(id)) {\n        ids.push(id);\n      }\n\n      return;\n    }\n\n    const nested = (child.props as { children?: ReactNode }).children;\n\n    if (nested) {\n      collectItemIds(nested, ids);\n    }\n  });\n\n  return ids;\n}\n\nexport type SetupChecklistProps = {\n  children: ReactNode;\n  /** Controlled list of completed item ids. */\n  completedIds?: string[];\n  /** Initial completed item ids for uncontrolled usage. */\n  defaultCompletedIds?: string[];\n  onCompletedChange?: (completedIds: string[]) => void;\n  onItemToggle?: (id: string, completed: boolean) => void;\n  className?: string;\n};\n\nexport function SetupChecklist({\n  children,\n  completedIds,\n  defaultCompletedIds,\n  onCompletedChange,\n  onItemToggle,\n  className,\n}: SetupChecklistProps) {\n  const isControlled = completedIds !== undefined;\n  const [internalCompleted, setInternalCompleted] = useState<string[]>(\n    () => defaultCompletedIds ?? []\n  );\n  const itemIds = useMemo(() => collectItemIds(children, []), [children]);\n\n  const completed = isControlled ? completedIds : internalCompleted;\n\n  const toggle = useCallback(\n    (id: string) => {\n      const isCompleted = completed.includes(id);\n      const next = isCompleted\n        ? completed.filter((value) => value !== id)\n        : [...completed, id];\n\n      if (!isControlled) {\n        setInternalCompleted(next);\n      }\n      onCompletedChange?.(next);\n      onItemToggle?.(id, !isCompleted);\n    },\n    [completed, isControlled, onCompletedChange, onItemToggle]\n  );\n\n  const value = useMemo(() => {\n    const completedCount = itemIds.filter((id) =>\n      completed.includes(id)\n    ).length;\n\n    return {\n      completed,\n      fraction: itemIds.length > 0 ? completedCount / itemIds.length : 0,\n      toggle,\n    };\n  }, [completed, itemIds, toggle]);\n\n  return (\n    <SetupChecklistContext.Provider value={value}>\n      <div className={cn(\"w-full max-w-md\", className)}>{children}</div>\n    </SetupChecklistContext.Provider>\n  );\n}\n\nexport function SetupChecklistCard({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <motion.section\n      animate={{ opacity: 1, y: 0, scale: 1 }}\n      className={cn(\n        \"rounded-3xl border border-foreground/8 bg-card p-5 sm:p-6\",\n        className\n      )}\n      initial={reduceMotion ? false : { opacity: 0, y: 16, scale: 0.98 }}\n      transition={SPRING}\n    >\n      {children}\n    </motion.section>\n  );\n}\n\nexport function SetupChecklistHeader({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return <div className={className}>{children}</div>;\n}\n\nexport function SetupChecklistTitle({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <h2 className={cn(\"font-semibold text-base text-foreground\", className)}>\n      {children}\n    </h2>\n  );\n}\n\nexport function SetupChecklistDescription({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <p\n      className={cn(\n        \"mt-1 text-[13px] text-muted-foreground leading-snug\",\n        className\n      )}\n    >\n      {children}\n    </p>\n  );\n}\n\nexport function SetupChecklistList({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion() === true;\n\n  const listVariants: Variants = {\n    hidden: {},\n    visible: {\n      transition: reduceMotion\n        ? undefined\n        : { staggerChildren: 0.07, delayChildren: 0.08 },\n    },\n  };\n\n  return (\n    <motion.ul\n      animate=\"visible\"\n      className={cn(\"mt-5 flex flex-col gap-3\", className)}\n      initial={reduceMotion ? false : \"hidden\"}\n      variants={listVariants}\n    >\n      {children}\n    </motion.ul>\n  );\n}\n\nfunction DrawnCheck() {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <motion.svg\n      aria-hidden=\"true\"\n      className=\"size-3\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth={3.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <motion.path\n        animate={{ pathLength: 1 }}\n        d=\"M4.5 12.5l5 5L19.5 6.5\"\n        initial={reduceMotion ? false : { pathLength: 0 }}\n        transition={{ delay: 0.06, duration: 0.28, ease: \"easeOut\" }}\n      />\n    </motion.svg>\n  );\n}\n\n/** Footer slot inside the card for a CTA, e.g. a submit button. */\nexport function SetupChecklistAction({\n  children,\n  className,\n}: {\n  children: ReactNode;\n  className?: string;\n}) {\n  const reduceMotion = useReducedMotion() === true;\n\n  return (\n    <motion.div\n      animate={{ opacity: 1, y: 0 }}\n      className={cn(\"mt-5 flex justify-end gap-2\", className)}\n      initial={reduceMotion ? false : { opacity: 0, y: 10 }}\n      transition={{ ...SPRING, delay: 0.2 }}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport type SetupChecklistItemProps = {\n  id: string;\n  title: ReactNode;\n  description?: ReactNode;\n  icon?: ReactNode;\n  /** Called on row click alongside the completion toggle. */\n  onClick?: (id: string) => void;\n  className?: string;\n};\n\nexport function SetupChecklistItem({\n  id,\n  title,\n  description,\n  icon,\n  onClick,\n  className,\n}: SetupChecklistItemProps) {\n  const { completed, toggle } = useSetupChecklistContext(\"SetupChecklistItem\");\n  const reduceMotion = useReducedMotion() === true;\n\n  const isCompleted = completed.includes(id);\n\n  const itemVariants: Variants = {\n    hidden: reduceMotion ? { opacity: 1 } : { opacity: 0, y: 14 },\n    visible: { opacity: 1, y: 0, transition: SPRING },\n  };\n\n  return (\n    <motion.li variants={itemVariants}>\n      <motion.button\n        aria-pressed={isCompleted}\n        className={cn(\n          \"relative flex w-full items-center gap-4 rounded-2xl border p-4 text-left transition-colors duration-300\",\n          isCompleted\n            ? \"border-transparent bg-muted/70\"\n            : \"border-foreground/8 bg-transparent hover:bg-muted/40\",\n          className\n        )}\n        onClick={() => {\n          toggle(id);\n          onClick?.(id);\n        }}\n        type=\"button\"\n        whileTap={reduceMotion ? undefined : { scale: 0.96 }}\n      >\n        {icon ? (\n          <span\n            className={cn(\n              \"flex size-12 shrink-0 items-center justify-center rounded-xl border border-foreground/8 text-foreground transition-opacity duration-300 [&_svg]:size-5\",\n              isCompleted && \"opacity-60\"\n            )}\n          >\n            {icon}\n          </span>\n        ) : null}\n        <span className=\"min-w-0 flex-1 pr-6\">\n          <span\n            className={cn(\n              \"block font-medium text-[15px] text-foreground transition-opacity duration-300\",\n              isCompleted && \"opacity-70\"\n            )}\n          >\n            {title}\n          </span>\n          {description ? (\n            <span className=\"mt-0.5 block text-[13px] text-muted-foreground leading-snug\">\n              {description}\n            </span>\n          ) : null}\n        </span>\n        <AnimatePresence initial={false}>\n          {isCompleted ? (\n            <motion.span\n              animate={{ scale: 1, opacity: 1 }}\n              className=\"absolute top-3.5 right-3.5 flex size-5 items-center justify-center rounded-full bg-emerald-500 text-white\"\n              exit={{ scale: 0.25, opacity: 0, filter: \"blur(4px)\" }}\n              initial={\n                reduceMotion\n                  ? { scale: 1, opacity: 1 }\n                  : { scale: 0.25, opacity: 0, filter: \"blur(4px)\" }\n              }\n              transition={BADGE_SPRING}\n            >\n              <DrawnCheck />\n            </motion.span>\n          ) : null}\n        </AnimatePresence>\n      </motion.button>\n    </motion.li>\n  );\n}\n\n/** Identity-independent marker so children traversal survives HMR module swaps. */\nSetupChecklistItem.isSetupChecklistItem = true;\n\nconst PROGRESS_SPRING = {\n  type: \"spring\" as const,\n  stiffness: 110,\n  damping: 24,\n};\n\nfunction clampFraction(value: number) {\n  return Math.min(Math.max(value, 0), 1);\n}\n\nfunction ProgressPie({\n  fraction,\n  onFrame,\n  reduceMotion,\n}: {\n  fraction: number;\n  onFrame: (fraction: number) => void;\n  reduceMotion: boolean;\n}) {\n  return (\n    <svg aria-hidden=\"true\" className=\"size-5 -rotate-90\" viewBox=\"0 0 20 20\">\n      <circle\n        className=\"text-foreground\"\n        cx=\"10\"\n        cy=\"10\"\n        fill=\"none\"\n        r=\"8.25\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n      />\n      <motion.circle\n        animate={{\n          // Slight overshoot at 100% closes the hairline seam SVG leaves\n          // where the dash meets its own start.\n          pathLength:\n            clampFraction(fraction) >= 1 ? 1.05 : clampFraction(fraction),\n        }}\n        className=\"text-foreground\"\n        cx=\"10\"\n        cy=\"10\"\n        fill=\"none\"\n        initial={reduceMotion ? false : { pathLength: 0 }}\n        onUpdate={(latest) => {\n          if (typeof latest.pathLength === \"number\") {\n            onFrame(latest.pathLength);\n          }\n        }}\n        r={PIE_RADIUS}\n        stroke=\"currentColor\"\n        strokeWidth={PIE_RADIUS * 2}\n        transition={reduceMotion ? { duration: 0 } : PROGRESS_SPRING}\n      />\n    </svg>\n  );\n}\n\nexport function SetupChecklistProgress({\n  children,\n  className,\n}: {\n  children?: ReactNode;\n  className?: string;\n}) {\n  const { fraction } = useSetupChecklistContext(\"SetupChecklistProgress\");\n  const reduceMotion = useReducedMotion() === true;\n  const [animatedFraction, setAnimatedFraction] = useState(0);\n\n  const shownFraction = reduceMotion ? fraction : animatedFraction;\n  const displayPercent = Math.round(clampFraction(shownFraction) * 100);\n\n  return (\n    <div className={cn(\"mt-4 flex justify-end\", className)}>\n      <motion.div\n        animate={{ opacity: 1, y: 0 }}\n        className=\"inline-flex items-center gap-2.5 rounded-full border border-foreground/8 bg-card py-3 pr-4 pl-5\"\n        initial={reduceMotion ? false : { opacity: 0, y: 10 }}\n        transition={{ ...SPRING, delay: 0.25 }}\n      >\n        <span className=\"text-[13px] text-muted-foreground\">\n          {children ? <>{children}: </> : null}\n          <span className=\"font-medium text-foreground tabular-nums\">\n            {displayPercent}%\n          </span>\n        </span>\n        <ProgressPie\n          fraction={fraction}\n          onFrame={setAnimatedFraction}\n          reduceMotion={reduceMotion}\n        />\n      </motion.div>\n    </div>\n  );\n}\n\nexport default SetupChecklist;\n",
      "type": "registry:ui"
    }
  ],
  "title": "Setup Checklist",
  "description": "Animated onboarding checklist card with staggered task rows, a drawn checkmark badge, and a floating progress pill with a spring-smoothed pie."
}
