{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox-group",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "checkbox-group.tsx",
      "content": "\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\n/** Soft settle — fluid, low bounce (water-like). */\nconst springFlow = {\n  type: \"spring\" as const,\n  stiffness: 280,\n  damping: 24,\n  mass: 0.65,\n};\n\nconst springTap = {\n  type: \"spring\" as const,\n  stiffness: 520,\n  damping: 36,\n  mass: 0.35,\n};\n\n/** Snappy tick — must feel instant after click. */\nconst springCheck = {\n  type: \"spring\" as const,\n  stiffness: 720,\n  damping: 34,\n  mass: 0.38,\n};\n\nexport interface CheckboxGroupOption {\n  label: string;\n  value: string;\n  description?: string;\n  disabled?: boolean;\n  disabledReason?: string;\n  group?: string;\n}\n\ninterface CheckboxGroupProps {\n  options: CheckboxGroupOption[];\n  value?: string[];\n  onChange?: (value: string[]) => void;\n  className?: string;\n  maxVisible?: number;\n  showMoreLabel?: string;\n  showLessLabel?: string;\n}\n\ninterface CheckboxGroupSection {\n  key: string;\n  label?: string;\n  options: CheckboxGroupOption[];\n}\n\nfunction buildSections(options: CheckboxGroupOption[]): CheckboxGroupSection[] {\n  return options.reduce<CheckboxGroupSection[]>((sections, option, index) => {\n    const label = option.group?.trim();\n    const previousSection = sections.at(-1);\n\n    if (previousSection && previousSection.label === label) {\n      previousSection.options.push(option);\n      return sections;\n    }\n\n    sections.push({\n      key: label ? `${label}-${index}` : `section-${index}`,\n      label,\n      options: [option],\n    });\n\n    return sections;\n  }, []);\n}\n\nfunction getOrderedValues(\n  options: CheckboxGroupOption[],\n  nextSelected: string[]\n): string[] {\n  const nextSet = new Set(nextSelected);\n  const visibleValues = new Set(options.map((option) => option.value));\n\n  const orderedVisibleValues = options\n    .filter((option) => nextSet.has(option.value))\n    .map((option) => option.value);\n\n  const extraValues = nextSelected.filter((value) => !visibleValues.has(value));\n\n  return [...orderedVisibleValues, ...extraValues];\n}\n\nfunction CheckboxGroup({\n  options,\n  value = [],\n  onChange,\n  className,\n  maxVisible,\n  showMoreLabel = \"Show more\",\n  showLessLabel = \"Show less\",\n}: CheckboxGroupProps) {\n  const [optimisticValue, setOptimisticValue] = useState(value);\n  const [expanded, setExpanded] = useState(false);\n\n  useEffect(() => {\n    setOptimisticValue(value);\n  }, [value]);\n\n  const canCollapse =\n    typeof maxVisible === \"number\" &&\n    maxVisible > 0 &&\n    options.length > maxVisible;\n  const forcedExpanded =\n    canCollapse &&\n    !expanded &&\n    options\n      .slice(maxVisible)\n      .some((option) => optimisticValue.includes(option.value));\n  const isExpanded = expanded || forcedExpanded;\n  const visibleOptions =\n    canCollapse && !isExpanded ? options.slice(0, maxVisible) : options;\n  const hiddenCount = canCollapse ? options.length - visibleOptions.length : 0;\n  const sections = buildSections(visibleOptions);\n\n  const toggle = (optionValue: string) => {\n    const nextSelected = optimisticValue.includes(optionValue)\n      ? optimisticValue.filter((v) => v !== optionValue)\n      : [...optimisticValue, optionValue];\n    const next = getOrderedValues(options, nextSelected);\n\n    setOptimisticValue(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\n        componentThemeClassName,\n        \"flex flex-col gap-1.5\",\n        className\n      )}\n    >\n      {sections.map((section) => (\n        <div className=\"flex flex-col gap-1\" key={section.key}>\n          {section.label ? (\n            <p className=\"px-4 pt-2 font-medium text-[11px] text-muted-foreground/80 uppercase tracking-[0.16em]\">\n              {section.label}\n            </p>\n          ) : null}\n\n          {section.options.map((option) => {\n            const checked = optimisticValue.includes(option.value);\n            const disabled = option.disabled;\n\n            return (\n              <motion.button\n                className={cn(\n                  \"group relative flex items-center gap-3 rounded-lg px-4 py-3 text-left\",\n                  \"transition-[background-color] duration-480 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n                  \"hover:bg-accent/60 dark:hover:bg-accent/60\",\n                  \"active:bg-neutral-100 dark:active:bg-neutral-800/78\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n                  \"disabled:cursor-not-allowed disabled:opacity-50\",\n                  \"disabled:active:bg-transparent disabled:hover:bg-transparent\",\n                  \"dark:disabled:active:bg-transparent dark:disabled:hover:bg-transparent\"\n                )}\n                disabled={disabled}\n                key={option.value}\n                onClick={() => toggle(option.value)}\n                transition={springTap}\n                type=\"button\"\n                whileTap={\n                  disabled\n                    ? undefined\n                    : {\n                        scale: 0.985,\n                        y: 0,\n                        transition: springTap,\n                      }\n                }\n              >\n                <div className=\"flex h-[18px] w-[18px] shrink-0 items-center justify-center\">\n                  <motion.div\n                    className={cn(\n                      \"flex h-[18px] w-[18px] items-center justify-center rounded transition-[border-color] duration-420 ease-[cubic-bezier(0.25,0.85,0.3,1)]\",\n                      checked\n                        ? \"border-0 bg-transparent\"\n                        : [\n                            \"border-2 bg-transparent\",\n                            \"border-neutral-300/90 group-hover:border-neutral-500/85\",\n                            \"dark:border-neutral-600 dark:group-hover:border-neutral-400\",\n                          ]\n                    )}\n                    layout=\"position\"\n                    transition={springFlow}\n                  >\n                    <AnimatePresence initial={false} mode=\"sync\">\n                      {checked ? (\n                        <motion.div\n                          animate={{\n                            opacity: 1,\n                            rotate: 0,\n                            scale: 1,\n                          }}\n                          className=\"flex items-center justify-center\"\n                          exit={{\n                            opacity: 0,\n                            rotate: -8,\n                            scale: 0.25,\n                            filter: \"blur(4px)\",\n                            transition: {\n                              duration: 0.12,\n                              ease: [0.4, 0, 1, 1],\n                            },\n                          }}\n                          initial={{\n                            opacity: 0,\n                            rotate: -5,\n                            scale: 0.25,\n                            filter: \"blur(4px)\",\n                          }}\n                          key=\"check\"\n                          transition={springCheck}\n                        >\n                          <Check\n                            className=\"h-4 w-4 text-primary\"\n                            strokeWidth={3}\n                          />\n                        </motion.div>\n                      ) : null}\n                    </AnimatePresence>\n                  </motion.div>\n                </div>\n\n                <div\n                  className={cn(\n                    \"flex min-w-0 flex-col transition-transform duration-520 ease-[cubic-bezier(0.22,1,0.36,1)]\",\n                    \"group-hover:translate-x-0.5\"\n                  )}\n                >\n                  <span className=\"font-medium text-foreground text-sm\">\n                    {option.label}\n                  </span>\n                  {option.description && (\n                    <span className=\"text-muted-foreground text-xs\">\n                      {option.description}\n                    </span>\n                  )}\n                  {disabled && option.disabledReason ? (\n                    <span className=\"text-[11px] text-muted-foreground/90\">\n                      {option.disabledReason}\n                    </span>\n                  ) : null}\n                </div>\n              </motion.button>\n            );\n          })}\n        </div>\n      ))}\n\n      {canCollapse && !forcedExpanded ? (\n        <button\n          className={cn(\n            \"self-start rounded-md px-4 py-2 font-medium text-muted-foreground text-sm transition-colors hover:text-foreground\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n          )}\n          onClick={() => setExpanded((previous) => !previous)}\n          type=\"button\"\n        >\n          {expanded ? showLessLabel : `${showMoreLabel} (${hiddenCount})`}\n        </button>\n      ) : null}\n    </div>\n  );\n}\n\nexport { CheckboxGroup };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Checkbox group",
  "description": "Multi-select option list with bordered empty state and a primary tick only when checked, plus hover/tap motion on each row. Motion + Lucide."
}
