{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"checkbox-group","type":"registry:ui","title":"Checkbox Group","description":"Several checkboxes under one label, to pick any number of options.","dependencies":[],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/checkbox.json","https://dashral.com/r/field.json"],"files":[{"path":"registry/dashral/checkbox-group.tsx","type":"registry:file","target":"components/dashral/checkbox-group.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { Checkbox } from \"./checkbox\"\nimport { FieldLabel, FieldWrapper, splitFieldProps, useFieldControl, type FieldClassNames, type FieldControlProps, type FieldOwnProps } from \"./field\"\n\ntype ControlColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\n// A ticked card: its border and a tint in the color, as RadioGroup's cards.\nconst cardColors: Record<ControlColor, string> = {\n  primary: \"has-[[data-state=checked]]:border-primary has-[[data-state=checked]]:bg-primary/5\",\n  neutral: \"has-[[data-state=checked]]:border-foreground has-[[data-state=checked]]:bg-foreground/5\",\n  info: \"has-[[data-state=checked]]:border-info has-[[data-state=checked]]:bg-info/5\",\n  success: \"has-[[data-state=checked]]:border-success has-[[data-state=checked]]:bg-success/5\",\n  warning: \"has-[[data-state=checked]]:border-warning has-[[data-state=checked]]:bg-warning/5\",\n  destructive: \"has-[[data-state=checked]]:border-destructive has-[[data-state=checked]]:bg-destructive/5\",\n  highlight: \"has-[[data-state=checked]]:border-highlight has-[[data-state=checked]]:bg-highlight/5\",\n}\n\ntype CheckboxOption =\n  | string\n  | { value: string; label?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode; disabled?: boolean }\n\ntype CheckboxGroupProps = FieldOwnProps & {\n  /** The checkboxes: strings, or { value, label, description, icon, disabled }. icon shows on cards. */\n  options: CheckboxOption[]\n  /** The ticked values, controlled. */\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[]) => void\n  /** \"cards\" draws each option as a bordered card you click anywhere on. */\n  variant?: \"default\" | \"cards\"\n  /** A checkbox above the rest that ticks or clears them all, half-ticked when some are. true for \"Select all\", or your words. */\n  selectAll?: boolean | React.ReactNode\n  /** At most this many: the rest are disabled once it's reached. */\n  max?: number\n  /** The color of a ticked checkbox (and card). */\n  color?: ControlColor\n  /** The form field's name: each ticked value is sent under it, as checkboxes are. */\n  name?: string\n  id?: string\n  disabled?: boolean\n  required?: boolean\n  \"aria-invalid\"?: React.AriaAttributes[\"aria-invalid\"]\n  /** The group's classes, e.g. columns: \"grid-cols-2\". */\n  className?: string\n  classNames?: FieldClassNames & { group?: string; item?: string; checkbox?: string }\n}\n\n/**\n * A set of checkboxes in one tag, RadioGroup's companion for picking several: label names the whole group, each option\n * can carry a description and an icon, variant=\"cards\" turns them into cards, and selectAll adds a box that ticks them\n * all. The value is the array of ticked values, in the options' order.\n */\nfunction CheckboxGroup(props: CheckboxGroupProps) {\n  const [field, control] = splitFieldProps(props)\n  return (\n    <FieldWrapper {...field} group>\n      <CheckboxGroupControl {...control} />\n    </FieldWrapper>\n  )\n}\n\nfunction CheckboxGroupControl({\n  options,\n  value,\n  defaultValue = [],\n  onValueChange,\n  variant = \"default\",\n  selectAll,\n  max,\n  color = \"primary\",\n  name,\n  className,\n  classNames,\n  ...rest\n}: Omit<CheckboxGroupProps, keyof FieldOwnProps>) {\n  const { t } = useLocale()\n  const { id, disabled, ...aria } = useFieldControl<FieldControlProps>({\n    id: rest.id,\n    disabled: rest.disabled,\n    required: rest.required,\n    \"aria-invalid\": rest[\"aria-invalid\"],\n  })\n  const groupId = React.useId()\n  const [internal, setInternal] = React.useState(defaultValue)\n  const current = value ?? internal\n  const normalized = options.map((option) => (typeof option === \"string\" ? { value: option } : option))\n  const enabled = normalized.filter((option) => !option.disabled)\n  const full = max !== undefined && current.length >= max\n\n  function set(next: string[]) {\n    // Kept in the options' order, whatever order they were ticked in.\n    const ordered = normalized.map((option) => option.value).filter((v) => next.includes(v))\n    if (value === undefined) setInternal(ordered)\n    onValueChange?.(ordered)\n  }\n  const toggle = (v: string, checked: boolean) => set(checked ? [...current, v] : current.filter((x) => x !== v))\n\n  const tickedEnabled = enabled.filter((option) => current.includes(option.value)).length\n  const allState: boolean | \"indeterminate\" = tickedEnabled === 0 ? false : tickedEnabled === enabled.length ? true : \"indeterminate\"\n\n  return (\n    <div\n      id={id}\n      role=\"group\"\n      data-slot=\"checkbox-group\"\n      aria-labelledby={aria[\"aria-labelledby\"]}\n      aria-describedby={aria[\"aria-describedby\"]}\n      className={cn(\"grid gap-3\", className, classNames?.group)}\n    >\n      {selectAll && (\n        // Across the whole group, columns or not, with its line under it.\n        <div className=\"col-span-full border-b pb-3\">\n          <Checkbox\n            label={selectAll === true ? t(\"selectAll\") : selectAll}\n            checked={allState}\n            disabled={disabled}\n            color={color}\n            onCheckedChange={() => {\n              // Half-ticked or clear: tick them all (up to max). Ticked: clear them (the disabled ones keep their state).\n              const locked = current.filter((v) => normalized.find((o) => o.value === v)?.disabled)\n              const all = enabled.map((o) => o.value)\n              set(allState === true ? locked : [...locked, ...(max !== undefined ? all.slice(0, Math.max(0, max - locked.length)) : all)])\n            }}\n          />\n        </div>\n      )}\n      {normalized.map((option) => {\n        const { value: v, label = v, description, icon } = option\n        const itemId = `${groupId}-${v}`\n        const checked = current.includes(v)\n        const off = disabled || option.disabled || (full && !checked)\n        const box = (extra?: string, describedBy?: string, withLabel = true) => (\n          <Checkbox\n            id={itemId}\n            name={name}\n            value={v}\n            checked={checked}\n            disabled={off}\n            color={color}\n            aria-invalid={aria[\"aria-invalid\"]}\n            aria-describedby={describedBy}\n            onCheckedChange={(next) => toggle(v, next === true)}\n            label={withLabel ? label : undefined}\n            description={withLabel ? description : undefined}\n            classNames={{ root: classNames?.item, checkbox: cn(extra, classNames?.checkbox) }}\n          />\n        )\n\n        if (variant === \"cards\") {\n          const descriptionId = description ? `${itemId}-description` : undefined\n          return (\n            <FieldLabel\n              key={v}\n              htmlFor={itemId}\n              data-slot=\"checkbox-card\"\n              data-disabled={off || undefined}\n              className={cn(\n                \"flex items-start gap-3 rounded-lg border p-4 font-normal transition-colors hover:bg-accent/50\",\n                \"has-focus-visible:ring-2 has-focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent\",\n                cardColors[color],\n                classNames?.item\n              )}\n            >\n              {icon && <span className=\"flex shrink-0 text-muted-foreground [&_svg]:size-5\">{icon}</span>}\n              <span className=\"grid min-w-0 flex-1 gap-1\">\n                <span className=\"leading-4 font-medium\">{label}</span>\n                {description && (\n                  <span id={descriptionId} className=\"text-xs text-muted-foreground\">\n                    {description}\n                  </span>\n                )}\n              </span>\n              {box(undefined, descriptionId, false)}\n            </FieldLabel>\n          )\n        }\n\n        return <React.Fragment key={v}>{box()}</React.Fragment>\n      })}\n    </div>\n  )\n}\n\nexport { CheckboxGroup, type CheckboxGroupProps, type CheckboxOption }\n"}],"cssVars":{"theme":{"color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)"},"light":{"info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b"},"dark":{"info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}