{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group-multi",
  "title": "Group-multi toggle",
  "description": "Styled toggle: the Group-multi set. 5 decorative toggle variations (MultiToggleGroup, CountToggleGroup, ChipsToggleGroup, AllToggleGroup, MixedToggleGroup) on the ai2 token system, driven by CSS token transitions and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/toggle-group-multi.tsx",
      "content": "\"use client\"\r\n\r\nimport * as React from \"react\"\r\nimport { Bold, Check, Italic, Strikethrough, Underline, X } from \"lucide-react\"\r\n\r\nimport { cn } from \"@/lib/utils\"\r\n\r\n/* Multi toggle group family: 5 decorative MULTI-select controls. The shared idea is\n   options that turn on and off independently of each other: every button carries its\n   own aria-pressed state, the selection is an array, and a click flips only that\n   option. The variants change how the multi-selection reads (checked buttons, a\n   counter, removable chips, select-all, a mix of icon+label). Color comes ONLY from\n   semantic tokens; alpha via color-mix. This family is entirely static and carries\n   no motion. The state is held at the root, so no subtree ever unmounts and loses\n   the selection. */\r\n\r\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\r\n\r\ntype Option = { value: string; label?: React.ReactNode; icon?: React.ReactNode }\r\n\r\ntype ToggleGroupProps = {\r\n  className?: string\r\n  size?: StyledSize\r\n  options?: Option[]\r\n  value?: string[]\r\n  defaultValue?: string[]\r\n  onValueChange?: (v: string[]) => void\r\n}\r\n\r\nconst sizeBtn: Record<StyledSize, string> = {\r\n  sm: \"h-8 px-3 text-xs\",\r\n  md: \"h-9 px-4 text-sm\",\r\n  lg: \"h-10 px-5 text-sm\",\r\n  xl: \"h-11 px-6 text-base\",\r\n}\r\n\r\nconst sizeIconBtn: Record<StyledSize, string> = {\r\n  sm: \"size-8 text-xs\",\r\n  md: \"size-9 text-sm\",\r\n  lg: \"size-10 text-sm\",\r\n  xl: \"size-11 text-base\",\r\n}\r\n\r\nconst btnBase =\r\n  \"relative z-10 inline-flex select-none items-center justify-center gap-2 whitespace-nowrap font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\r\n\r\nconst FORMAT_OPTIONS: Option[] = [\r\n  { value: \"bold\", label: \"Bold\", icon: <Bold /> },\r\n  { value: \"italic\", label: \"Italic\", icon: <Italic /> },\r\n  { value: \"underline\", label: \"Underline\", icon: <Underline /> },\r\n  { value: \"strike\", label: \"Strikethrough\", icon: <Strikethrough /> },\r\n]\r\n\r\nfunction labelText(o: Option): string {\r\n  return typeof o.label === \"string\" ? o.label : o.value\r\n}\r\n\r\n/* The controlled/uncontrolled multi-selection state. When uncontrolled the first\n   option is on by default, so that the on/off difference is visible in a render with\n   no props. */\r\nfunction useMultiToggleGroup(props: ToggleGroupProps, fallback: Option[]) {\r\n  const { options, value, defaultValue, onValueChange } = props\r\n  const list = React.useMemo(() => (options && options.length ? options : fallback), [options, fallback])\r\n  const controlled = value !== undefined\r\n  const [internal, setInternal] = React.useState<string[]>(() => {\r\n    if (defaultValue) return defaultValue\r\n    return list[0] ? [list[0].value] : []\r\n  })\r\n  const selected = controlled ? value : internal\r\n\r\n  const emit = React.useCallback(\r\n    (next: string[]) => {\r\n      if (!controlled) setInternal(next)\r\n      onValueChange?.(next)\r\n    },\r\n    [controlled, onValueChange]\r\n  )\r\n\r\n  const toggle = React.useCallback(\r\n    (v: string) => {\r\n      emit(selected.includes(v) ? selected.filter((s) => s !== v) : [...selected, v])\r\n    },\r\n    [selected, emit]\r\n  )\r\n\r\n  const isOn = React.useCallback((v: string) => selected.includes(v), [selected])\r\n  return { list, selected, isOn, toggle, emit }\r\n}\r\n\r\n/* Multi: a connected multi-select bar; an option that is on carries a primary tint\n   and a marker. */\r\nexport function MultiToggleGroup({ className, size = \"md\", ...props }: ToggleGroupProps) {\r\n  const { list, isOn, toggle } = useMultiToggleGroup(props, FORMAT_OPTIONS)\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label=\"Text formatting, multi select\"\r\n        className={cn(\"inline-flex flex-wrap items-center gap-1 rounded-lg border border-border bg-background p-1\", className)}\r\n      >\r\n        {list.map((o) => {\r\n          const on = isOn(o.value)\r\n          return (\r\n            <button\r\n              key={o.value}\r\n              type=\"button\"\r\n              aria-pressed={on}\r\n              data-slot=\"styled-toggle-group-item\"\r\n              onClick={() => toggle(o.value)}\r\n              className={cn(\r\n                btnBase,\r\n                sizeBtn[size],\r\n                \"rounded-md\",\r\n                on\r\n                  ? \"bg-[color-mix(in_oklab,var(--color-primary)_14%,transparent)] text-primary\"\r\n                  : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\r\n              )}\r\n            >\r\n              {on ? <Check /> : o.icon}\r\n              {o.label}\r\n            </button>\r\n          )\r\n        })}\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n\r\n/* Count: coklu-secim cubugu + secili sayisini okuyan canli bir sayac rozeti. */\r\nexport function CountToggleGroup({ className, size = \"md\", ...props }: ToggleGroupProps) {\r\n  const { list, selected, isOn, toggle } = useMultiToggleGroup(props, FORMAT_OPTIONS)\r\n  const countId = React.useId()\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label=\"Text formatting, with a selected counter\"\r\n        aria-describedby={countId}\r\n        className={cn(\"inline-flex flex-wrap items-center gap-2\", className)}\r\n      >\r\n        <div className=\"inline-flex items-center gap-1 rounded-lg border border-border bg-background p-1\">\r\n          {list.map((o) => {\r\n            const on = isOn(o.value)\r\n            return (\r\n              <button\r\n                key={o.value}\r\n                type=\"button\"\r\n                aria-pressed={on}\r\n                aria-label={labelText(o)}\r\n                data-slot=\"styled-toggle-group-item\"\r\n                onClick={() => toggle(o.value)}\r\n                className={cn(\r\n                  btnBase,\r\n                  sizeIconBtn[size],\r\n                  \"rounded-md\",\r\n                  on ? \"bg-primary text-primary-foreground\" : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\r\n                )}\r\n              >\r\n                {o.icon ?? o.label}\r\n              </button>\r\n            )\r\n          })}\r\n        </div>\r\n        <span\r\n          id={countId}\r\n          aria-live=\"polite\"\r\n          className=\"inline-flex h-6 items-center rounded-full bg-muted px-2 text-xs font-medium tabular-nums text-muted-foreground\"\r\n        >\r\n          {selected.length} of {list.length} selected\r\n        </span>\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n\r\n/* Chips: a chip series; a selected chip shows a primary fill and a remove marker. */\r\nexport function ChipsToggleGroup({ className, size = \"md\", ...props }: ToggleGroupProps) {\r\n  const { list, isOn, toggle } = useMultiToggleGroup(props, FORMAT_OPTIONS)\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label=\"Text formatting, removable chips\"\r\n        className={cn(\"inline-flex flex-wrap items-center gap-2\", className)}\r\n      >\r\n        {list.map((o) => {\r\n          const on = isOn(o.value)\r\n          return (\r\n            <button\r\n              key={o.value}\r\n              type=\"button\"\r\n              aria-pressed={on}\r\n              data-slot=\"styled-toggle-group-item\"\r\n              onClick={() => toggle(o.value)}\r\n              className={cn(\r\n                btnBase,\r\n                sizeBtn[size],\r\n                \"rounded-full border\",\r\n                on\r\n                  ? \"border-transparent bg-primary text-primary-foreground\"\r\n                  : \"border-border bg-background text-muted-foreground hover:text-foreground\"\r\n              )}\r\n            >\r\n              {o.label}\r\n              {on ? <X /> : null}\r\n            </button>\r\n          )\r\n        })}\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n\r\n/* All: multi-select + a select-all/clear button. The all button is not a toggle but\n   a bulk action; that is why it states the real state in its text rather than using\n   aria-pressed. */\r\nexport function AllToggleGroup({ className, size = \"md\", ...props }: ToggleGroupProps) {\r\n  const { list, selected, isOn, toggle, emit } = useMultiToggleGroup(props, FORMAT_OPTIONS)\r\n  const allOn = selected.length === list.length\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label=\"Text formatting, with a select all action\"\r\n        className={cn(\"inline-flex flex-wrap items-center gap-1 rounded-lg border border-border bg-muted p-1\", className)}\r\n      >\r\n        <button\r\n          type=\"button\"\r\n          data-slot=\"styled-toggle-group-action\"\r\n          onClick={() => emit(allOn ? [] : list.map((o) => o.value))}\r\n          className={cn(\r\n            btnBase,\r\n            sizeBtn[size],\r\n            \"rounded-md border border-border bg-background text-foreground hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)]\"\r\n          )}\r\n        >\r\n          {allOn ? \"Clear all\" : \"Select all\"}\r\n        </button>\r\n        <span aria-hidden=\"true\" className=\"mx-1 h-5 w-px shrink-0 bg-border\" />\r\n        {list.map((o) => {\r\n          const on = isOn(o.value)\r\n          return (\r\n            <button\r\n              key={o.value}\r\n              type=\"button\"\r\n              aria-pressed={on}\r\n              data-slot=\"styled-toggle-group-item\"\r\n              onClick={() => toggle(o.value)}\r\n              className={cn(\r\n                btnBase,\r\n                sizeBtn[size],\r\n                \"rounded-md\",\r\n                on ? \"bg-background text-foreground shadow-sm\" : \"text-muted-foreground hover:text-foreground\"\r\n              )}\r\n            >\r\n              {o.icon}\r\n              {o.label}\r\n            </button>\r\n          )\r\n        })}\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n\r\n/* Mixed: icon-only and icon+label buttons in the same bar; a selected one moves to\n   the filled tone. For multi-select bars of mixed width. */\r\nexport function MixedToggleGroup({ className, size = \"md\", ...props }: ToggleGroupProps) {\r\n  const { list, isOn, toggle } = useMultiToggleGroup(props, FORMAT_OPTIONS)\r\n  return (\r\n    <div\r\n      data-slot=\"styled-toggle-group-viewport\"\r\n      className=\"-m-1 flex max-w-full overflow-x-auto p-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\r\n    >\r\n      <div\r\n        data-slot=\"styled-toggle-group\"\r\n        role=\"group\"\r\n        aria-label=\"Text formatting, mixed icon and label buttons\"\r\n        className={cn(\"inline-flex flex-wrap items-center gap-1\", className)}\r\n      >\r\n        {list.map((o, i) => {\r\n          const on = isOn(o.value)\r\n          const iconOnly = i % 2 === 1\r\n          return (\r\n            <button\r\n              key={o.value}\r\n              type=\"button\"\r\n              aria-pressed={on}\r\n              aria-label={iconOnly ? labelText(o) : undefined}\r\n              data-slot=\"styled-toggle-group-item\"\r\n              onClick={() => toggle(o.value)}\r\n              className={cn(\r\n                btnBase,\r\n                iconOnly ? sizeIconBtn[size] : sizeBtn[size],\r\n                \"rounded-md border\",\r\n                on\r\n                  ? \"border-transparent bg-success text-success-foreground\"\r\n                  : \"border-border bg-background text-muted-foreground hover:text-foreground\"\r\n              )}\r\n            >\r\n              {o.icon}\r\n              {iconOnly ? null : o.label}\r\n            </button>\r\n          )\r\n        })}\r\n      </div>\r\n    </div>\r\n  )\r\n}\r\n",
      "type": "registry:component",
      "target": "components/ui/toggle-group-multi.tsx"
    }
  ],
  "categories": [
    "styled",
    "toggle"
  ],
  "type": "registry:component"
}