{
  "name": "tags",
  "layer": "base",
  "type": "component",
  "title": "Tag components",
  "description": "Free and open-source React tag components built for modern applications and websites. These tags are built using React Aria and styled with Tailwind CSS.",
  "files": [
    {
      "path": "components/base/tags/base-components/tag-checkbox.tsx",
      "target": "components/base/tags/base-components/tag-checkbox.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { cx } from \"@/utils/cx\";\n\ninterface TagCheckboxProps {\n    /** Size of the checkbox, matching the parent tag's size. @default \"sm\" */\n    size?: \"sm\" | \"md\" | \"lg\";\n    /** Optional additional CSS class names. */\n    className?: string;\n    /** Whether the checkbox is focused. */\n    isFocused?: boolean;\n    /** Whether the checkbox is selected. */\n    isSelected?: boolean;\n    /** Whether the checkbox is disabled. */\n    isDisabled?: boolean;\n}\n\nexport const TagCheckbox = ({ className, isFocused, isSelected, isDisabled, size = \"sm\" }: TagCheckboxProps) => {\n    return (\n        <div\n            className={cx(\n                \"bg-primary ring-primary flex cursor-pointer appearance-none items-center justify-center rounded ring-1 ring-inset\",\n                size === \"sm\" && \"size-3.5\",\n                size === \"md\" && \"size-4\",\n                size === \"lg\" && \"size-4.5\",\n                isSelected && \"bg-brand-solid ring-brand-solid\",\n                isDisabled && \"cursor-not-allowed opacity-50\",\n                isDisabled && !isSelected && \"bg-tertiary\",\n                isFocused && \"outline-focus-ring outline-2 outline-offset-2\",\n                className,\n            )}\n        >\n            <svg\n                aria-hidden=\"true\"\n                viewBox=\"0 0 14 14\"\n                fill=\"none\"\n                className={cx(\n                    \"text-fg-white transition-inherit-all pointer-events-none absolute opacity-0\",\n                    size === \"sm\" && \"size-2.5\",\n                    size === \"md\" && \"size-3\",\n                    size === \"lg\" && \"size-3.5\",\n                    isSelected && \"opacity-100\",\n                )}\n            >\n                <path d=\"M11.6666 3.5L5.24992 9.91667L2.33325 7\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n        </div>\n    );\n};\nTagCheckbox.displayName = \"TagCheckbox\";\n",
      "dependencies": []
    },
    {
      "path": "components/base/tags/base-components/tag-close-x.tsx",
      "target": "components/base/tags/base-components/tag-close-x.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport type { RefAttributes } from \"react\";\nimport { Button as AriaButton, type ButtonProps as AriaButtonProps } from \"react-aria-components\";\nimport { XClose } from \"@properui/icons\";\nimport { cx } from \"@/utils/cx\";\n\ninterface TagCloseXProps extends AriaButtonProps, RefAttributes<HTMLButtonElement> {\n    /** Size of the close button, matching the parent tag's size. @default \"md\" */\n    size?: \"sm\" | \"md\" | \"lg\";\n    /** Optional additional CSS class names. */\n    className?: string;\n}\n\nconst styles = {\n    sm: { root: \"p-0.5\", icon: \"size-2.5 stroke-[3.6px]\" },\n    md: { root: \"p-0.5\", icon: \"size-3 stroke-[2.86px]\" },\n    lg: { root: \"p-0.75\", icon: \"size-3.5 stroke-3\" },\n};\n\nexport const TagCloseX = ({ size = \"md\", className, ...otherProps }: TagCloseXProps) => {\n    return (\n        <AriaButton\n            slot=\"remove\"\n            aria-label=\"Remove this tag\"\n            className={cx(\n                \"text-fg-quaternary hover:bg-primary_hover hover:text-fg-quaternary_hover focus-visible:outline-focus-ring flex cursor-pointer rounded-[3px] outline-transparent transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed\",\n                styles[size].root,\n                className,\n            )}\n            {...otherProps}\n        >\n            <XClose aria-hidden=\"true\" className={cx(\"transition-inherit-all\", styles[size].icon)} />\n        </AriaButton>\n    );\n};\n",
      "dependencies": [
        "@properui/icons",
        "react",
        "react-aria-components"
      ]
    },
    {
      "path": "components/base/tags/tags.tsx",
      "target": "components/base/tags/tags.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport { type ImgHTMLAttributes, type PropsWithChildren, type RefAttributes, createContext, useContext, useState } from \"react\";\nimport {\n    Tag as AriaTag,\n    TagGroup as AriaTagGroup,\n    type TagGroupProps as AriaTagGroupProps,\n    TagList as AriaTagList,\n    type TagProps as AriaTagProps,\n} from \"react-aria-components\";\nimport { User01 } from \"@properui/icons\";\nimport { cx, sortCx } from \"@/utils/cx\";\nimport { TagCheckbox } from \"./base-components/tag-checkbox\";\nimport { TagCloseX } from \"./base-components/tag-close-x\";\n\n// TODO(orchestrator): candidate for components/foundations/dot-icon — that file does not exist\n// yet in this repo, so the status dot is duplicated locally here. Once foundations/dot-icon.tsx\n// is ported, replace this with `import { Dot } from \"../../foundations/dot-icon\"`.\nconst Dot = ({ size = \"md\", className }: { size?: \"sm\" | \"md\"; className?: string }) => {\n    const wh = size === \"sm\" ? 8 : 10;\n    const c = size === \"sm\" ? 4 : 5;\n    const r = size === \"sm\" ? 2.5 : 4;\n\n    return (\n        <svg aria-hidden=\"true\" width={wh} height={wh} viewBox={`0 0 ${wh} ${wh}`} fill=\"none\" className={className}>\n            <circle cx={c} cy={c} r={r} fill=\"currentColor\" stroke=\"currentColor\" />\n        </svg>\n    );\n};\n\nexport const TagAvatar = ({\n    src,\n    alt,\n    contrastBorder = true,\n    className,\n}: ImgHTMLAttributes<HTMLImageElement> & { /** Whether to render a subtle contrast outline around the avatar. @default true */ contrastBorder?: boolean }) => {\n    const [isFailed, setIsFailed] = useState(false);\n\n    return (\n        <div\n            className={cx(\n                \"bg-tertiary relative inline-flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-full\",\n                contrastBorder && \"outline-[0.5px] -outline-offset-[0.5px] outline-black/16\",\n                className,\n            )}\n        >\n            {src && !isFailed ? (\n                <img data-avatar-img className=\"size-full object-cover\" src={src} alt={alt} onError={() => setIsFailed(true)} />\n            ) : (\n                <User01 className=\"text-fg-quaternary size-3 stroke-[2.25px]\" />\n            )}\n        </div>\n    );\n};\n\nexport interface TagItem {\n    /** Unique identifier for the tag, used by `TagList`'s `items` prop and selection/removal keys. */\n    id: string;\n    /** Visible text content of the tag. */\n    label: string;\n    /** Optional count badge shown at the end of the tag. */\n    count?: number;\n    /** Optional avatar image shown at the start of the tag. */\n    avatarSrc?: string;\n    /** Whether the avatar renders a subtle contrast outline. @default true */\n    avatarContrastBorder?: boolean;\n    /** Whether to show a status dot at the start of the tag. */\n    dot?: boolean;\n    /** Optional additional CSS class names for the status dot. */\n    dotClassName?: string;\n    /** Whether the tag is disabled. */\n    isDisabled?: boolean;\n    /** Called when this tag's remove (\"x\") button is pressed. */\n    onClose?: (id: string) => void;\n}\n\nconst TagGroupContext = createContext<{\n    selectionMode: \"none\" | \"single\" | \"multiple\";\n    size: \"sm\" | \"md\" | \"lg\";\n}>({\n    selectionMode: \"none\",\n    size: \"sm\",\n});\n\ninterface TagGroupProps extends AriaTagGroupProps, RefAttributes<HTMLDivElement> {\n    /** Accessible label for the tag group. */\n    label: string;\n    /** Size applied to every tag rendered inside this group. @default \"sm\" */\n    size?: \"sm\" | \"md\" | \"lg\";\n}\n\nexport const TagGroup = ({ label, selectionMode = \"none\", size = \"sm\", children, ...otherProps }: TagGroupProps) => {\n    return (\n        <TagGroupContext.Provider value={{ selectionMode, size }}>\n            <AriaTagGroup aria-label={label} selectionMode={selectionMode} disallowEmptySelection={selectionMode === \"single\"} {...otherProps}>\n                {children}\n            </AriaTagGroup>\n        </TagGroupContext.Provider>\n    );\n};\n\nexport const TagList = AriaTagList;\n\nconst styles = sortCx({\n    sm: {\n        root: {\n            base: \"px-2 py-0.75 text-xs font-medium\",\n            withCheckbox: \"ps-1.25\",\n            withAvatar: \"ps-1\",\n            withDot: \"ps-1.5\",\n            withCount: \"pe-1\",\n            withClose: \"pe-1\",\n        },\n        content: \"gap-1\",\n        count: \"px-1 text-xs font-medium\",\n    },\n    md: {\n        root: {\n            base: \"px-2.25 py-0.5 text-sm font-medium\",\n            withCheckbox: \"ps-1\",\n            withAvatar: \"ps-1.25\",\n            withDot: \"ps-1.75\",\n            withCount: \"pe-0.75\",\n            withClose: \"pe-1\",\n        },\n        content: \"gap-1.25\",\n        count: \"px-1.25 text-xs font-medium\",\n    },\n    lg: {\n        root: {\n            base: \"px-2.5 py-1 text-sm font-medium\",\n            withCheckbox: \"ps-1.25\",\n            withAvatar: \"ps-1.75\",\n            withDot: \"ps-2.25\",\n            withCount: \"pe-1\",\n            withClose: \"pe-1\",\n        },\n        content: \"gap-1.5\",\n        count: \"px-1.5 text-sm font-medium\",\n    },\n});\n\ninterface TagProps extends AriaTagProps, RefAttributes<object>, Omit<TagItem, \"label\" | \"id\"> {}\n\nexport const Tag = ({\n    id,\n    avatarSrc,\n    avatarContrastBorder = true,\n    dot,\n    dotClassName,\n    isDisabled,\n    count,\n    className,\n    children,\n    onClose,\n    // `TagProps`'s `RefAttributes<object>` types `ref` too loosely to forward on to `AriaTag`\n    // (which expects a ref to its actual row element); left unforwarded, as it always was before\n    // `...rest` started forwarding everything else.\n    ref: _ref,\n    ...rest\n}: PropsWithChildren<TagProps>) => {\n    const context = useContext(TagGroupContext);\n\n    const leadingContent = avatarSrc ? (\n        <TagAvatar src={avatarSrc} alt=\"Avatar\" contrastBorder={avatarContrastBorder} />\n    ) : dot ? (\n        <Dot className={cx(\"text-fg-success-secondary\", dotClassName)} size=\"sm\" />\n    ) : null;\n\n    return (\n        <AriaTag\n            {...rest}\n            id={id}\n            isDisabled={isDisabled}\n            textValue={typeof children === \"string\" ? children : undefined}\n            className={(state) =>\n                cx(\n                    \"bg-primary text-secondary ring-primary outline-focus-ring flex cursor-default items-center gap-0.75 rounded-md ring-1 transition duration-50 ease-linear ring-inset focus-visible:outline-2 focus-visible:outline-offset-2\",\n                    styles[context.size].root.base,\n\n                    // With avatar\n                    avatarSrc && styles[context.size].root.withAvatar,\n                    // With X button\n                    (onClose || state.allowsRemoving) && styles[context.size].root.withClose,\n                    // With dot\n                    dot && styles[context.size].root.withDot,\n                    // With count\n                    typeof count === \"number\" && styles[context.size].root.withCount,\n                    // With checkbox\n                    context.selectionMode !== \"none\" && styles[context.size].root.withCheckbox,\n                    // Disabled\n                    isDisabled && \"cursor-not-allowed\",\n\n                    typeof className === \"function\" ? className(state) : className,\n                )\n            }\n        >\n            {({ isSelected, isDisabled, allowsRemoving }) => (\n                <>\n                    <div className={cx(\"flex items-center gap-1\", styles[context.size].content)}>\n                        {context.selectionMode !== \"none\" && <TagCheckbox size={context.size} isSelected={isSelected} isDisabled={isDisabled} />}\n\n                        {leadingContent}\n\n                        {children}\n\n                        {typeof count === \"number\" && (\n                            <span className={cx(\"bg-tertiary flex items-center justify-center rounded-[3px] text-center\", styles[context.size].count)}>\n                                {count}\n                            </span>\n                        )}\n                    </div>\n\n                    {(onClose || allowsRemoving) && (\n                        <TagCloseX size={context.size} excludeFromTabOrder={allowsRemoving} onPress={() => id && onClose?.(id.toString())} />\n                    )}\n                </>\n            )}\n        </AriaTag>\n    );\n};\n",
      "dependencies": [
        "@properui/icons",
        "react",
        "react-aria-components"
      ]
    }
  ],
  "registryDependencies": [
    "cx"
  ],
  "optionalRegistryDependencies": [],
  "dependencies": [
    "@properui/icons",
    "react",
    "react-aria-components"
  ],
  "cssVars": [],
  "examples": [
    "checkbox-close-x-example",
    "checkbox-count-example",
    "checkbox-example",
    "close-x-example",
    "count-example",
    "sizes-example",
    "tags-example"
  ],
  "docs": "/components/tags",
  "intent": "Display, and let a user select or remove, short chip-style values (e.g. active filters or multi-value entry).",
  "avoid_when": [
    "values are read-only status labels rather than user-manipulable chips (use badges)"
  ],
  "composes_with": [
    "input",
    "select",
    "filter-bar"
  ],
  "a11y_contract": [
    "built on React Aria TagGroup/Tag: keyboard navigation and selection/removal semantics come for free; the group requires a `label` prop, passed through as its aria-label"
  ],
  "requires_data": [
    "a `label` describing the group of tags, and a list of tag values"
  ],
  "token_contract": [
    "bg-brand-solid",
    "bg-primary",
    "bg-primary_hover",
    "bg-tertiary",
    "text-fg-quaternary",
    "text-fg-quaternary_hover",
    "text-fg-success-secondary",
    "text-fg-white",
    "text-secondary"
  ],
  "changelog": [
    {
      "version": "0.3.0",
      "changes": [
        "Adds six new component groups. Under `base/`: `HoverCard` (a rich hover/focus-triggered preview built on `Popover`, for things a plain `Tooltip` can't hold, like a profile card with a Follow button), `Menubar` (an application-style `File`/`Edit`/`View` menu bar built on React Aria's `Toolbar` plus the same `Menu`/`MenuTrigger` primitives `Dropdown` uses, with hover-switching between open menus, submenus, and checkbox/radio items), `NumberInput` (a `NumberField`-based numeric input with stacked or inline increment/decrement buttons and `formatOptions` for currency/percent/unit display), and `TagInput` (type-to-add tags on Enter/comma, paste-splits- on-commas, `maxTags`, a `validate` callback, and `isReadOnly`). Under `application/`: `Stepper` (a controlled multi-step form wizard — distinct from the purely decorative `ProgressSteps` — with `canAdvance` validation, linear and non-linear navigation, and horizontal/vertical layouts) and `Timeline` (vertical, alternating and horizontal status timelines with `completed`/`current`/`upcoming` coloring)."
      ]
    },
    {
      "version": "0.2.0",
      "changes": [
        "Field API and accessible-name fixes from the agent feedback map (docs/spec/feedback/2026-09-11-agent-feedback-map.md items 2.17, 2.20, 2.22, and the JSDoc half of 2.11), batched across `input`, `select`, `checkbox`, `radio-buttons`, `badges`, `toggle`, `tags`, `buttons/button`, `date-picker`, `page-headers`, and `foundations/featured-icon`:"
      ]
    }
  ],
  "platforms": [
    "react",
    "next"
  ]
}
