{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox-card",
  "type": "registry:ui",
  "title": "Checkbox Card",
  "description": "Selectable cards whose corner check pops in, with a select-all that counts.",
  "categories": [
    "selection"
  ],
  "dependencies": [
    "@base-ui/react",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/checkbox.json"
  ],
  "files": [
    {
      "path": "components/ui/checkbox-card.tsx",
      "content": "\"use client\";\nimport { Checkbox as BaseCheckbox } from \"@base-ui/react/checkbox\";\nimport { CheckboxGroup as BaseCheckboxGroup } from \"@base-ui/react/checkbox-group\";\nimport NumberFlow from \"@number-flow/react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useId } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease, spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\n\nexport type CheckboxCardGroupProps = Omit<BaseCheckboxGroup.Props, \"className\" | \"value\" | \"defaultValue\" | \"onValueChange\"> & {\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  /** Adds a select-all row above the cards. Needs `allValues`. */\n  selectAll?: boolean;\n  selectAllLabel?: React.ReactNode;\n  /** Cards per row once the group is wider than 384px. Always one column below that. */\n  columns?: 1 | 2 | 3;\n  className?: string;\n};\n\nexport function CheckboxCardGroup({\n  value: valueProp,\n  defaultValue = [],\n  onValueChange,\n  selectAll = false,\n  selectAllLabel = \"Select all\",\n  allValues,\n  columns = 2,\n  className,\n  children,\n  ...rest\n}: CheckboxCardGroupProps) {\n  // Held here so the select-all parent always has a controlled group to read.\n  const [value, setValue] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });\n  const total = allValues?.length ?? 0;\n  const count = allValues ? value.filter((v) => allValues.includes(v)).length : value.length;\n\n  return (\n    <BaseCheckboxGroup\n      value={value}\n      onValueChange={(next) => setValue(next)}\n      allValues={allValues}\n      className={cn(\"@container flex w-full flex-col gap-2.5\", className)}\n      {...rest}\n    >\n      {selectAll && allValues && (\n        <div className=\"flex min-h-7 items-center justify-between gap-3\">\n          <Checkbox parent size=\"sm\" label={selectAllLabel} />\n          {/* Fixed line box: the rolling digits sit inline-block and would nudge the baseline. */}\n          <span className=\"flex h-5 shrink-0 items-center text-[12px] tabular text-fg-3\">\n            <span>\n              {count === 0 ? (\n                \"None selected\"\n              ) : (\n                <>\n                  <NumberFlow value={count} className=\"text-fg-2\" /> of {total} selected\n                </>\n              )}\n            </span>\n          </span>\n        </div>\n      )}\n      <div\n        className={cn(\n          \"grid gap-2\",\n          columns === 2 && \"@sm:grid-cols-2\",\n          columns === 3 && \"@sm:grid-cols-2 @xl:grid-cols-3\",\n        )}\n      >\n        {children}\n      </div>\n    </BaseCheckboxGroup>\n  );\n}\n\nexport type CheckboxCardProps = Omit<BaseCheckbox.Root.Props, \"className\" | \"children\" | \"title\" | \"value\" | \"parent\"> & {\n  /** Identifies the card inside the group. */\n  value: string;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** A 14px icon, drawn in a tile at the start of the card. */\n  icon?: React.ReactNode;\n  /** Small trailing text beside the title, like a plan name or \"Beta\". */\n  badge?: React.ReactNode;\n  className?: string;\n};\n\nexport function CheckboxCard({ value, title, description, icon, badge, className, ...rest }: CheckboxCardProps) {\n  const titleId = useId();\n  const descriptionId = useId();\n\n  return (\n    <BaseCheckbox.Root\n      value={value}\n      render={<div />}\n      aria-labelledby={titleId}\n      aria-describedby={description != null ? descriptionId : undefined}\n      className={(state) =>\n        cn(\n          \"group/card relative flex min-w-0 select-none items-start gap-3 rounded-xl border p-3 pr-9 text-left outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"transition-[background-color,border-color,box-shadow,scale] duration-150 ease-out-expo\",\n          state.checked\n            ? \"border-fg-3 bg-hover shadow-[inset_0_0_0_0.5px_var(--fg-3)]\"\n            : \"border-line-2 bg-raised\",\n          state.disabled\n            ? \"cursor-not-allowed opacity-50\"\n            : !state.readOnly && cn(\"motion-safe:active:scale-[0.985] active:duration-100\", !state.checked && \"hover:border-fg-4\"),\n          className,\n        )\n      }\n      {...rest}\n    >\n      {icon != null && (\n        <span\n          aria-hidden\n          className=\"grid size-8 shrink-0 place-items-center rounded-lg border border-line bg-frame text-fg-2 transition-colors duration-150 group-data-checked/card:text-fg [&_svg]:size-3.5\"\n        >\n          {icon}\n        </span>\n      )}\n      <span className=\"flex min-w-0 flex-col gap-0.5 pt-px\">\n        <span className=\"flex min-w-0 items-center gap-1.5\">\n          <span id={titleId} className=\"truncate text-[13px] font-medium leading-5 tracking-[-0.005em] text-fg\">\n            {title}\n          </span>\n          {badge != null && (\n            <span className=\"shrink-0 rounded-full border border-line-2 px-1.5 text-[10.5px] leading-4 text-fg-2\">{badge}</span>\n          )}\n        </span>\n        {description != null && (\n          <span id={descriptionId} className=\"line-clamp-2 text-[12.5px] leading-[18px] text-fg-3\">\n            {description}\n          </span>\n        )}\n      </span>\n      <BaseCheckbox.Indicator\n        keepMounted\n        className=\"pointer-events-none absolute right-3 top-3\"\n        render={(props, state) => (\n          <span {...props}>\n            <CornerCheck checked={state.checked} idle={!state.disabled && !state.readOnly} />\n          </span>\n        )}\n      />\n    </BaseCheckbox.Root>\n  );\n}\n\n/** An empty ring that fills and pops, then draws its tick. */\nfunction CornerCheck({ checked, idle }: { checked: boolean; idle: boolean }) {\n  const reduce = useReducedMotion();\n  return (\n    <span className=\"relative grid size-4 place-items-center rounded-full\">\n      <span\n        className={cn(\n          \"absolute inset-0 rounded-full border transition-[border-color,opacity] duration-150\",\n          checked ? \"border-transparent\" : cn(\"border-fg-4\", idle && \"group-hover/card:border-fg-3\"),\n        )}\n      />\n      <motion.span\n        className=\"absolute inset-0 grid place-items-center rounded-full bg-fg text-frame\"\n        initial={false}\n        animate={checked ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.5 }}\n        // Reduced motion keeps the fade and snaps the scale. The targets stay the same so SSR matches.\n        transition={reduce ? { opacity: { duration: 0.12 }, scale: { duration: 0 } } : checked ? spring.pop : { duration: 0.12, ease: ease.in }}\n      >\n        <svg width=\"10\" height=\"10\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden focusable=\"false\">\n          <motion.path\n            d=\"M3.5 8.5 L6.75 11.5 L12.5 4.75\"\n            stroke=\"currentColor\"\n            strokeWidth={2.4}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            initial={false}\n            animate={{ pathLength: checked ? 1 : 0, opacity: checked ? 1 : 0 }}\n            transition={\n              reduce\n                ? { duration: 0 }\n                : checked\n                  ? { pathLength: { duration: 0.22, ease: ease.out, delay: 0.06 }, opacity: { duration: 0.01, delay: 0.06 } }\n                  : { duration: 0.08 }\n            }\n          />\n        </svg>\n      </motion.span>\n    </span>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/checkbox-card"
}
