{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/checkbox-group",
  "title": "Checkbox Group",
  "description": "Checkbox group with min/max selection",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "https://termcn.dev/r/ink/terminal-symbols.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/checkbox-group.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useState } from \"react\";\n\nimport { useInteraction } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport { resolveTerminalSymbol } from \"@/lib/terminal-symbols\";\n\nexport interface CheckboxGroupOption {\n  value: string;\n  label: string;\n  disabled?: boolean;\n}\n\nexport interface CheckboxGroupProps {\n  label?: string;\n  options: CheckboxGroupOption[];\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (values: string[]) => void;\n  onChange?: (values: string[]) => void;\n  min?: number;\n  max?: number;\n  id?: string;\n  autoFocus?: boolean;\n  isActive?: boolean;\n  disabled?: boolean;\n  \"aria-label\"?: string;\n}\n\nexport const CheckboxGroup = ({\n  label,\n  options,\n  value: controlledValue,\n  defaultValue = [],\n  onValueChange,\n  onChange,\n  min,\n  max,\n  id,\n  autoFocus = false,\n  isActive = true,\n  disabled = false,\n  \"aria-label\": ariaLabel,\n}: CheckboxGroupProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const [activeIndex, setActiveIndex] = useState(0);\n  const [internalSelected, setInternalSelected] =\n    useState<string[]>(defaultValue);\n  const [error, setError] = useState<string | undefined>();\n\n  const selected = controlledValue ?? internalSelected;\n\n  const validateAndUpdate = (next: string[]) => {\n    if (min !== undefined && next.length < min) {\n      setError(`Select at least ${min} option${min === 1 ? \"\" : \"s\"}.`);\n    } else if (max !== undefined && next.length > max) {\n      setError(`Select at most ${max} option${max === 1 ? \"\" : \"s\"}.`);\n      return;\n    } else {\n      setError(undefined);\n    }\n    if (controlledValue === undefined) {\n      setInternalSelected(next);\n    }\n    (onValueChange ?? onChange)?.(next);\n  };\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (key.upArrow) {\n        setActiveIndex((i) => {\n          let next = i - 1;\n          while (next >= 0 && options[next]?.disabled) {\n            next -= 1;\n          }\n          return next < 0 ? i : next;\n        });\n      } else if (key.downArrow) {\n        setActiveIndex((i) => {\n          let next = i + 1;\n          while (next < options.length && options[next]?.disabled) {\n            next += 1;\n          }\n          return next >= options.length ? i : next;\n        });\n      } else if (key.home) {\n        const firstEnabled = options.findIndex((option) => !option.disabled);\n        if (firstEnabled !== -1) {\n          setActiveIndex(firstEnabled);\n        }\n      } else if (key.end) {\n        const lastEnabled = options.findLastIndex((option) => !option.disabled);\n        if (lastEnabled !== -1) {\n          setActiveIndex(lastEnabled);\n        }\n      } else if (input === \" \") {\n        const opt = options[activeIndex];\n        if (!opt || opt.disabled) {\n          return;\n        }\n        const isSelected = selected.includes(opt.value);\n        const next = isSelected\n          ? selected.filter((v) => v !== opt.value)\n          : [...selected, opt.value];\n        validateAndUpdate(next);\n      }\n    },\n    { autoFocus, disabled, id, isActive }\n  );\n\n  return (\n    <Box\n      aria-role=\"listbox\"\n      aria-state={{ disabled, multiselectable: true }}\n      flexDirection=\"column\"\n    >\n      <Text\n        aria-label={ariaLabel ?? label ?? \"Checkbox group\"}\n        bold={Boolean(label)}\n        color={theme.colors.foreground}\n      >\n        {label ?? \"\"}\n      </Text>\n      {options.map((opt, idx) => {\n        const isActive = idx === activeIndex;\n        const isSelected = selected.includes(opt.value);\n        const icon = isSelected\n          ? resolveTerminalSymbol(unicode, \"◉\", \"[x]\")\n          : resolveTerminalSymbol(unicode, \"○\", \"[ ]\");\n\n        let iconColor: string;\n        if (opt.disabled) {\n          iconColor = theme.colors.mutedForeground;\n        } else if (isSelected) {\n          iconColor = theme.colors.primary;\n        } else {\n          iconColor = theme.colors.foreground;\n        }\n\n        let optLabelColor: string;\n        if (opt.disabled) {\n          optLabelColor = theme.colors.mutedForeground;\n        } else if (isActive) {\n          optLabelColor = theme.colors.primary;\n        } else {\n          optLabelColor = theme.colors.foreground;\n        }\n\n        return (\n          <Box\n            key={idx}\n            aria-label={opt.label}\n            aria-role=\"option\"\n            aria-state={{\n              disabled: disabled || opt.disabled,\n              selected: isSelected,\n            }}\n            gap={1}\n          >\n            <Text\n              aria-hidden\n              color={isActive ? theme.colors.primary : undefined}\n            >\n              {isActive ? resolveTerminalSymbol(unicode, \"›\", \">\") : \" \"}\n            </Text>\n            <Text aria-hidden color={iconColor} dimColor={opt.disabled}>\n              {icon}\n            </Text>\n            <Text\n              aria-hidden\n              color={optLabelColor}\n              bold={isActive}\n              dimColor={opt.disabled}\n            >\n              {opt.label}\n            </Text>\n          </Box>\n        );\n      })}\n      {error && <Text color={theme.colors.error}>{error}</Text>}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "selection"
  ],
  "type": "registry:ui"
}