{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "onboarding-checklist",
  "title": "Onboarding Checklist",
  "description": "A setup guide with tasks, accessible checkboxes, action buttons, and a progress bar.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "card",
    "@drift-ui/use-controllable-state",
    "@drift-ui/shape"
  ],
  "files": [
    {
      "path": "registry/default/ui/onboarding-checklist.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  calculateProgress,\n  getCompletedCount,\n  isItemCompleted,\n  toggleItem,\n  type OnboardingChecklistProgress,\n} from \"@/registry/default/lib/onboarding-checklist.ts\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { Button, buttonVariants } from \"@/registry/default/ui/button\"\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/default/ui/card\"\n\nexport type OnboardingChecklistAction = {\n  label: string\n  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void\n  href?: string\n  disabled?: boolean\n}\n\nexport type OnboardingChecklistItem = {\n  id: string\n  title: string\n  description?: React.ReactNode\n  action?: OnboardingChecklistAction\n  disabled?: boolean\n}\n\nexport type { OnboardingChecklistProgress }\n\nexport interface OnboardingChecklistProps extends Omit<\n  React.ComponentProps<typeof Card>,\n  \"title\"\n> {\n  title?: React.ReactNode\n  description?: React.ReactNode\n  items: OnboardingChecklistItem[]\n  completed?: string[]\n  defaultCompleted?: string[]\n  onCompletedChange?: (completed: string[]) => void\n  onItemToggle?: (item: OnboardingChecklistItem, completed: boolean) => void\n  collapsible?: boolean\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  progressFormat?: (progress: OnboardingChecklistProgress) => React.ReactNode\n}\n\nfunction OnboardingChecklist({\n  title = \"Getting started\",\n  description,\n  items = [],\n  completed: completedProp,\n  defaultCompleted = [],\n  onCompletedChange,\n  onItemToggle,\n  collapsible = false,\n  open: openProp,\n  defaultOpen = true,\n  onOpenChange,\n  progressFormat,\n  className,\n  ...props\n}: OnboardingChecklistProps) {\n  const generatedId = React.useId()\n  const contentId = `${generatedId}-content`\n\n  const { value: completed, setValue: setCompleted } = useControllableState<\n    string[]\n  >({\n    value: completedProp,\n    defaultValue: defaultCompleted,\n    onChange: onCompletedChange,\n  })\n\n  const { value: isOpen, setValue: setIsOpen } = useControllableState<boolean>({\n    value: openProp,\n    defaultValue: defaultOpen,\n    onChange: onOpenChange,\n  })\n\n  const completedCount = getCompletedCount(items, completed)\n  const progress = calculateProgress(items.length, completedCount)\n\n  const handleToggle = (item: OnboardingChecklistItem) => {\n    if (item.disabled) return\n    const willComplete = !isItemCompleted(completed, item.id)\n    const next = toggleItem(completed, item.id)\n    setCompleted(next)\n    onItemToggle?.(item, willComplete)\n  }\n\n  const isExpanded = !collapsible || isOpen\n\n  return (\n    <Card\n      data-slot=\"onboarding-checklist\"\n      className={cn(\"w-full\", className)}\n      {...props}\n    >\n      <CardHeader className=\"gap-3\">\n        <div className=\"flex items-start justify-between gap-2\">\n          <div className=\"space-y-1\">\n            {title && <CardTitle>{title}</CardTitle>}\n            {description && <CardDescription>{description}</CardDescription>}\n          </div>\n          {collapsible && (\n            <CardAction>\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"size-control-sm text-muted-foreground\"\n                onClick={() => setIsOpen(!isOpen)}\n                aria-expanded={isOpen}\n                aria-controls={contentId}\n                aria-label={isOpen ? \"Collapse checklist\" : \"Expand checklist\"}\n              >\n                <ChevronDownIcon\n                  className={cn(\n                    \"size-4 transition-transform duration-200\",\n                    isOpen && \"rotate-180\"\n                  )}\n                />\n              </Button>\n            </CardAction>\n          )}\n        </div>\n\n        <div className=\"col-span-full flex flex-col gap-1.5 pt-1\">\n          <div className=\"flex items-center justify-between text-xs text-muted-foreground\">\n            <span>Progress</span>\n            <span>\n              {progressFormat ? progressFormat(progress) : progress.ratioText}\n            </span>\n          </div>\n          <progress\n            aria-label={`${typeof title === \"string\" ? title : \"Onboarding\"} progress`}\n            aria-valuemin={0}\n            aria-valuemax={progress.total}\n            aria-valuenow={progress.completedCount}\n            aria-valuetext={progress.ratioText}\n            value={progress.completedCount}\n            max={progress.total || 1}\n            className={cn(\n              \"h-2 w-full appearance-none overflow-hidden rounded-full bg-secondary transition-all\",\n              \"[&::-webkit-progress-bar]:rounded-full [&::-webkit-progress-bar]:bg-secondary\",\n              \"[&::-webkit-progress-value]:rounded-full [&::-webkit-progress-value]:bg-primary [&::-webkit-progress-value]:transition-all\",\n              \"[&::-moz-progress-bar]:rounded-full [&::-moz-progress-bar]:bg-primary\"\n            )}\n          />\n        </div>\n      </CardHeader>\n\n      <CardContent id={contentId} hidden={!isExpanded}>\n        <ul className=\"flex flex-col gap-2.5\" role=\"list\">\n          {items.map((item) => {\n            const isChecked = isItemCompleted(completed, item.id)\n            const inputId = `${generatedId}-item-${item.id}`\n\n            return (\n              <li\n                key={item.id}\n                className={cn(\n                  \"flex items-center justify-between gap-3 rounded-lg border p-3 transition-colors\",\n                  isChecked\n                    ? \"border-muted bg-muted/40\"\n                    : \"border-border bg-card\",\n                  item.disabled && \"opacity-60\"\n                )}\n              >\n                <div className=\"flex min-w-0 flex-1 items-start gap-3\">\n                  <div className=\"pt-0.5\">\n                    <input\n                      type=\"checkbox\"\n                      id={inputId}\n                      checked={isChecked}\n                      disabled={item.disabled}\n                      onChange={() => handleToggle(item)}\n                      className={cn(\n                        \"size-4 shrink-0 cursor-pointer rounded border-input text-primary accent-primary\",\n                        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none\",\n                        \"disabled:cursor-not-allowed disabled:opacity-50\"\n                      )}\n                    />\n                  </div>\n                  <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n                    <label\n                      htmlFor={inputId}\n                      className={cn(\n                        \"cursor-pointer text-sm leading-tight font-medium select-none\",\n                        isChecked && \"text-muted-foreground line-through\",\n                        item.disabled && \"cursor-not-allowed\"\n                      )}\n                    >\n                      {item.title}\n                    </label>\n                    {item.description && (\n                      <div\n                        className={cn(\n                          \"text-xs leading-relaxed text-muted-foreground\",\n                          isChecked && \"opacity-75\"\n                        )}\n                      >\n                        {item.description}\n                      </div>\n                    )}\n                  </div>\n                </div>\n\n                {item.action && (\n                  <div className=\"shrink-0\">\n                    {item.action.href ? (\n                      <a\n                        href={\n                          item.action.disabled ? undefined : item.action.href\n                        }\n                        role={item.action.disabled ? \"link\" : undefined}\n                        className={cn(\n                          buttonVariants({ variant: \"outline\", size: \"sm\" }),\n                          \"h-control-sm text-xs\",\n                          item.action.disabled &&\n                            \"pointer-events-none opacity-50\"\n                        )}\n                        aria-disabled={item.action.disabled}\n                        tabIndex={item.action.disabled ? -1 : undefined}\n                        onClick={\n                          item.action.disabled\n                            ? (e) => e.preventDefault()\n                            : undefined\n                        }\n                      >\n                        {item.action.label}\n                      </a>\n                    ) : (\n                      <Button\n                        type=\"button\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        className=\"h-control-sm text-xs\"\n                        disabled={item.action.disabled}\n                        onClick={item.action.onClick}\n                      >\n                        {item.action.label}\n                      </Button>\n                    )}\n                  </div>\n                )}\n              </li>\n            )\n          })}\n        </ul>\n      </CardContent>\n    </Card>\n  )\n}\n\nexport { OnboardingChecklist }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/onboarding-checklist.ts",
      "content": "export type OnboardingChecklistAction = {\n  label: string\n  onClick?: (event?: unknown) => void\n  href?: string\n  disabled?: boolean\n}\n\nexport type OnboardingChecklistItem = {\n  id: string\n  title: string\n  description?: unknown\n  action?: OnboardingChecklistAction\n  disabled?: boolean\n}\n\nexport type OnboardingChecklistProgress = {\n  total: number\n  completedCount: number\n  percentage: number\n  isComplete: boolean\n  ratioText: string\n}\n\nexport function calculateProgress(\n  total: number,\n  completedCount: number\n): OnboardingChecklistProgress {\n  const safeTotal = Math.max(0, total)\n  const safeCompleted = Math.min(safeTotal, Math.max(0, completedCount))\n  const percentage =\n    safeTotal === 0 ? 0 : Math.round((safeCompleted / safeTotal) * 100)\n  const isComplete = safeTotal > 0 && safeCompleted === safeTotal\n\n  return {\n    total: safeTotal,\n    completedCount: safeCompleted,\n    percentage,\n    isComplete,\n    ratioText: `${safeCompleted} of ${safeTotal} done`,\n  }\n}\n\nexport function toggleItem(completed: readonly string[], id: string): string[] {\n  if (completed.includes(id)) {\n    return completed.filter((item) => item !== id)\n  }\n  return [...completed, id]\n}\n\nexport function isItemCompleted(\n  completed: readonly string[],\n  id: string\n): boolean {\n  return completed.includes(id)\n}\n\nexport function getCompletedCount(\n  items: readonly { id: string }[],\n  completed: readonly string[]\n): number {\n  const validIds = new Set(items.map((item) => item.id))\n  return new Set(completed.filter((id) => validIds.has(id))).size\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}