{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tag-group",
  "title": "Tag Group",
  "description": "Set of tags supporting selection and removal.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/tag-group/tag-group.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/** Selection behaviour for a TagGroup. */\nexport type TagSelectionMode = \"multiple\" | \"none\" | \"single\";\n\ntype TagGroupContextValue = {\n  disabled: boolean;\n  select: (value: string) => void;\n  selectedValues: string[];\n  selectionMode: TagSelectionMode;\n};\n\nconst TagGroupContext = React.createContext<null | TagGroupContextValue>(null);\n\nfunction useTagGroupContext(): TagGroupContextValue {\n  const context = React.use(TagGroupContext);\n  if (!context) {\n    throw new Error(\"TagGroupItem must be used within a TagGroup\");\n  }\n  return context;\n}\n\ntype TagSelectionOptions = {\n  defaultValue: string[];\n  onValueChange?: (value: string[]) => void;\n  selectionMode: TagSelectionMode;\n  value?: string[];\n};\n\nfunction nextSelection(\n  current: string[],\n  item: string,\n  selectionMode: TagSelectionMode,\n): string[] {\n  if (selectionMode === \"single\") {\n    return current.includes(item) ? [] : [item];\n  }\n\n  return current.includes(item)\n    ? current.filter((entry) => entry !== item)\n    : [...current, item];\n}\n\nfunction useTagSelection({\n  defaultValue,\n  onValueChange,\n  selectionMode,\n  value,\n}: TagSelectionOptions) {\n  const [internalValue, setInternalValue] = React.useState(defaultValue);\n  const selectedValues = value ?? internalValue;\n\n  const select = (item: string) => {\n    if (selectionMode === \"none\") {\n      return;\n    }\n\n    const next = nextSelection(selectedValues, item, selectionMode);\n    if (value === undefined) {\n      setInternalValue(next);\n    }\n\n    onValueChange?.(next);\n  };\n\n  return { select, selectedValues };\n}\n\n/** Labelled set of tags supporting selection and removal. */\nexport type TagGroupProps = {\n  children: React.ReactNode;\n  className?: string;\n  defaultValue?: string[];\n  disabled?: boolean;\n  label?: string;\n  onValueChange?: (value: string[]) => void;\n  selectionMode?: TagSelectionMode;\n  value?: string[];\n};\n\nconst TagGroup = ({\n  children,\n  className,\n  defaultValue = [],\n  disabled = false,\n  label,\n  onValueChange,\n  ref,\n  selectionMode = \"none\",\n  value,\n}: TagGroupProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const { select, selectedValues } = useTagSelection({\n    defaultValue,\n    onValueChange,\n    selectionMode,\n    value,\n  });\n  const context = React.useMemo<TagGroupContextValue>(\n    () => ({ disabled, select, selectedValues, selectionMode }),\n    [disabled, select, selectedValues, selectionMode],\n  );\n\n  return (\n    <TagGroupContext.Provider value={context}>\n      <div\n        aria-label={label}\n        className={cn(\"flex flex-wrap items-center gap-2\", className)}\n        ref={ref}\n        role=\"group\"\n      >\n        {children}\n      </div>\n    </TagGroupContext.Provider>\n  );\n};\nTagGroup.displayName = \"TagGroup\";\n\n/** Single chip within a TagGroup, optionally selectable and removable. */\nexport type TagGroupItemProps = {\n  children: React.ReactNode;\n  className?: string;\n  onRemove?: () => void;\n  value: string;\n};\n\nconst TagGroupItem = ({\n  children,\n  className,\n  onRemove,\n  ref,\n  value,\n}: TagGroupItemProps & { ref?: React.Ref<HTMLSpanElement> }) => {\n  const { disabled, select, selectedValues, selectionMode } =\n    useTagGroupContext();\n  const selectable = selectionMode !== \"none\";\n  const selected = selectedValues.includes(value);\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1 rounded-full border px-3 py-1 text-sm transition-colors\",\n        selected\n          ? \"border-transparent bg-primary text-primary-foreground\"\n          : \"border-border bg-muted text-foreground\",\n        disabled && \"pointer-events-none opacity-50\",\n        className,\n      )}\n      data-selected={selected || undefined}\n      ref={ref}\n    >\n      {selectable ? (\n        <button\n          aria-pressed={selected}\n          className=\"outline-none focus-visible:underline\"\n          disabled={disabled}\n          onClick={() => {\n            select(value);\n          }}\n          type=\"button\"\n        >\n          {children}\n        </button>\n      ) : (\n        <span>{children}</span>\n      )}\n      {onRemove ? (\n        <button\n          aria-label=\"Remove tag\"\n          className=\"rounded-sm opacity-70 outline-none transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring\"\n          disabled={disabled}\n          onClick={onRemove}\n          type=\"button\"\n        >\n          <X className=\"size-3.5\" />\n        </button>\n      ) : null}\n    </span>\n  );\n};\nTagGroupItem.displayName = \"TagGroupItem\";\n\nexport { TagGroup, TagGroupItem };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.2.1",
  "stability": "stable"
}
