{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group",
  "title": "Toggle Group",
  "description": "Toggle buttons sharing one highlight: it travels between items in single mode, and pressed neighbours fuse into one shape in multiple mode. Base UI ToggleGroup behaviour.",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-indicator.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/toggle-group/toggle-group.tsx",
      "content": "\"use client\";\n\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup as BaseToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { AnimatePresence } from \"motion/react\";\nimport { createContext, use, useState, type ReactNode } from \"react\";\nimport { exitFast, reducedFade, snappy, useSafeReducedMotion } from \"../lib/motion\";\nimport { Indicator, IndicatorGroup } from \"../_primitives/indicator/indicator\";\n\ntype GroupState = {\n  pressed: readonly string[];\n  multiple: boolean;\n  /** The pressed set was empty before this change, so the highlight grows in instead of travelling. */\n  fromEmpty: boolean;\n  orientation: \"horizontal\" | \"vertical\";\n  variant: \"track\" | \"chips\";\n};\n\nconst ToggleGroupContext = createContext<GroupState | null>(null);\n\nexport type ToggleGroupProps = {\n  /** Pressed values (controlled). */\n  value?: readonly string[];\n  /** Initially pressed values (uncontrolled). */\n  defaultValue?: readonly string[];\n  onValueChange?: (value: string[]) => void;\n  /**\n   * `false` (default): at most one item is pressed and the highlight travels between them,\n   * like text alignment. `true`: any number, like bold/italic/underline; pressed neighbours\n   * fuse into one continuous highlight.\n   */\n  multiple?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n  /**\n   * `track` (default): items share one 32px bordered track and one highlight. `chips`: a row\n   * of separate bordered chips, each filling when pressed, for filter rows that may scroll.\n   */\n  variant?: \"track\" | \"chips\";\n  disabled?: boolean;\n  /** Accessible name for the group, e.g. \"Text formatting\". */\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n  children: ReactNode;\n};\n\n/**\n * A row of toggle buttons sharing one highlight. Base UI's ToggleGroup supplies the\n * behaviour: one tab stop, arrow keys between items, `aria-pressed` on each.\n * 32px tall like every grouped control track (2px padding, 1px border, items stretch to\n * 26px), concentric: the highlight's radius is the track's minus its padding and border.\n * A press tints the item (multi or toggle); Segmented's raised thumb is for single choice.\n */\nexport function ToggleGroup({\n  value,\n  defaultValue = [],\n  onValueChange,\n  multiple = false,\n  orientation = \"horizontal\",\n  variant = \"track\",\n  disabled,\n  className = \"\",\n  children,\n  ...aria\n}: ToggleGroupProps) {\n  const [uncontrolled, setUncontrolled] = useState<readonly string[]>(defaultValue);\n  const pressed = value ?? uncontrolled;\n  // Store what was pressed last render (React's \"info from previous renders\" pattern).\n  const [prev, setPrev] = useState({ pressed, fromEmpty: true });\n  let fromEmpty = prev.fromEmpty;\n  if (prev.pressed !== pressed) {\n    fromEmpty = prev.pressed.length === 0;\n    setPrev({ pressed, fromEmpty });\n  }\n  const vertical = orientation === \"vertical\";\n\n  return (\n    <ToggleGroupContext value={{ pressed, multiple, fromEmpty, orientation, variant }}>\n      <BaseToggleGroup\n        value={pressed as string[]}\n        onValueChange={(next) => {\n          if (value === undefined) setUncontrolled(next);\n          onValueChange?.(next);\n        }}\n        multiple={multiple}\n        orientation={orientation}\n        disabled={disabled}\n        {...aria}\n        className={`${\n          variant === \"chips\"\n            ? `flex gap-1.5 ${vertical ? \"flex-col items-start\" : \"items-center\"}`\n            : `inline-flex shrink-0 rounded-control-sm border border-line-subtle bg-surface p-0.5 [--border-w:1px] [--pad:2px] [--radius-outer:var(--eased-radius-control-sm)] ${\n                vertical ? \"flex-col\" : \"h-control-sm items-stretch\"\n              }`\n        } ${className}`}\n      >\n        <IndicatorGroup>{children}</IndicatorGroup>\n      </BaseToggleGroup>\n    </ToggleGroupContext>\n  );\n}\n\nexport type ToggleGroupItemProps = {\n  value: string;\n  /** An icon, a short label, or both. */\n  children: ReactNode;\n  /** Required for icon-only items, e.g. \"Bold\". */\n  \"aria-label\"?: string;\n  disabled?: boolean;\n  className?: string;\n};\n\n/** One toggle. Its label never changes weight or size; only its colour and the highlight move. */\nexport function ToggleGroupItem({ value, children, disabled, className = \"\", ...aria }: ToggleGroupItemProps) {\n  const group = use(ToggleGroupContext);\n  if (!group) throw new Error(\"<ToggleGroupItem> must be rendered inside a <ToggleGroup>.\");\n  const reduced = useSafeReducedMotion();\n  const pressed = group.pressed.includes(value);\n  const vertical = group.orientation === \"vertical\";\n  const iconOnly = aria[\"aria-label\"] !== undefined;\n\n  // Chips: each its own bordered 32px control that fills when pressed; no shared highlight.\n  if (group.variant === \"chips\") {\n    return (\n      <Toggle\n        value={value}\n        disabled={disabled}\n        {...aria}\n        className={(state) =>\n          `inline-flex h-control-sm shrink-0 items-center justify-center gap-1.5 rounded-control-sm border text-ui-sm font-medium whitespace-nowrap select-none transition-colors duration-150 ease-standard focus-ring [&_svg]:size-4 [&_svg]:shrink-0 ${\n            iconOnly ? \"w-control-sm\" : \"px-3\"\n          } ${\n            state.disabled\n              ? \"cursor-not-allowed border-line-subtle text-text-disabled\"\n              : state.pressed\n                ? \"cursor-pointer border-transparent bg-surface-active text-text\"\n                : \"cursor-pointer border-line-subtle text-text-secondary hover-fine:bg-surface-hover hover-fine:text-text\"\n          } ${className}`\n        }\n      >\n        {children}\n      </Toggle>\n    );\n  }\n\n  return (\n    <Toggle\n      value={value}\n      disabled={disabled}\n      {...aria}\n      render={(props, state) => (\n        <button type=\"button\" {...props}>\n          {group.multiple ? (\n            <AnimatePresence initial={false}>\n              {pressed && (\n                <Indicator\n                  key=\"on\"\n                  name={value}\n                  initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit={{ opacity: 0, scale: reduced ? 1 : 0.96, transition: reduced ? reducedFade : exitFast }}\n                  transition={reduced ? reducedFade : { ...snappy, opacity: { duration: 0.12 } }}\n                  className=\"inset-0 radius-nested bg-surface-active\"\n                />\n              )}\n            </AnimatePresence>\n          ) : (\n            // Travelling hands over instantly; clearing the group fades the highlight out.\n            <AnimatePresence initial={false} custom={group.pressed.length === 0}>\n              {pressed && (\n                <Indicator\n                  key=\"on\"\n                  custom={group.pressed.length === 0}\n                  variants={{\n                    gone: (cleared: boolean) =>\n                      cleared\n                        ? { opacity: 0, scale: reduced ? 1 : 0.96, transition: reduced ? reducedFade : exitFast }\n                        : { opacity: 0, transition: { duration: 0 } },\n                  }}\n                  initial={group.fromEmpty && !reduced ? { opacity: 0, scale: 0.9 } : false}\n                  animate={{ opacity: 1, scale: 1 }}\n                  exit=\"gone\"\n                  className=\"inset-0 radius-nested bg-surface-active\"\n                />\n              )}\n            </AnimatePresence>\n          )}\n          {/* The bridge: fills the seam to the next pressed item so a run reads as one shape. */}\n          {group.multiple && (\n            <span\n              aria-hidden\n              className={`pointer-events-none absolute bg-surface-active opacity-0 transition-opacity duration-(--eased-duration-fast) ease-standard reduced:duration-(--eased-duration-instant) ${\n                vertical ? \"inset-x-0 -bottom-1.5 h-3\" : \"inset-y-0 -right-1.5 w-3\"\n              } ${state.pressed ? \"[:has(+[data-pressed])>&]:opacity-100\" : \"\"}`}\n            />\n          )}\n          <span className=\"relative flex items-center gap-1.5 [&_svg]:size-4 [&_svg]:shrink-0\">{children}</span>\n        </button>\n      )}\n      className={(state) =>\n        `relative flex min-w-7 shrink-0 items-center justify-center radius-nested text-ui-sm font-medium whitespace-nowrap select-none transition-colors duration-150 ease-standard focus-ring ${\n          iconOnly ? \"px-1.5\" : \"px-2.5\"\n        } ${vertical ? \"h-7\" : \"\"} ${\n          state.disabled\n            ? \"cursor-not-allowed text-text-disabled\"\n            : state.pressed\n              ? \"cursor-pointer text-text\"\n              : \"cursor-pointer text-text-muted hover-fine:bg-surface-hover hover-fine:text-text-secondary\"\n        } ${className}`\n      }\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/toggle-group/toggle-group.tsx"
    }
  ],
  "docs": "Import ToggleGroup and ToggleGroupItem from components/eased/toggle-group/toggle-group. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}