{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group",
  "type": "registry:ui",
  "title": "Toggle Group",
  "description": "Single or multiple toggles; one wash slides, neighboring washes join.",
  "categories": [
    "actions"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/use-controllable-state.json",
    "https://www.stealth.pm/r/icon-button.json"
  ],
  "files": [
    {
      "path": "components/ui/toggle-group.tsx",
      "content": "\"use client\";\nimport { Toggle } from \"@base-ui/react/toggle\";\nimport { ToggleGroup as BaseToggleGroup } from \"@base-ui/react/toggle-group\";\nimport { AnimatePresence, LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { createContext, useContext, useId } from \"react\";\nimport { ActionTooltip, type TooltipSide } from \"@/components/ui/icon-button\";\nimport { cn } from \"@/lib/cn\";\nimport { spring } from \"@/lib/motion\";\nimport { useControllableState } from \"@/lib/use-controllable-state\";\n\ntype Size = \"sm\" | \"md\" | \"lg\";\ntype Ctx = { value: string[]; multiple: boolean; size: Size; vertical: boolean };\nconst GroupContext = createContext<Ctx>({ value: [], multiple: false, size: \"md\", vertical: false });\n\nexport type ToggleGroupProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\" | \"dir\"> & {\n  value?: string[];\n  defaultValue?: string[];\n  onValueChange?: (value: string[]) => void;\n  /** Let several items be on at once (bold + italic). Single by default (alignment, view). */\n  multiple?: boolean;\n  /** Single mode: one item always stays on; pressing it again does nothing. */\n  required?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n  size?: Size;\n  disabled?: boolean;\n  /** Names the group for screen readers (\"Text alignment\"). */\n  \"aria-label\"?: string;\n};\n\nconst heights: Record<Size, { item: string; square: string; text: string; radius: string; wash: string }> = {\n  sm: { item: \"h-[22px] gap-1 px-1.5\", square: \"size-[22px]\", text: \"text-[12px] [&_svg]:size-3.5\", radius: \"rounded-md\", wash: \"rounded-[4px]\" },\n  md: { item: \"h-7 gap-1.5 px-2.5 has-[svg]:pl-2\", square: \"size-7\", text: \"text-[12.5px] [&_svg]:size-4\", radius: \"rounded-lg\", wash: \"rounded-md\" },\n  lg: { item: \"h-8 gap-2 px-3 has-[svg]:pl-2.5\", square: \"size-8\", text: \"text-[13px] [&_svg]:size-4\", radius: \"rounded-lg\", wash: \"rounded-md\" },\n};\n\n/**\n * A set of toggles sharing one state. In single mode one wash slides between the\n * items; in multiple mode each item gets its own, and neighbors join into one pill.\n */\nexport function ToggleGroup({\n  value: valueProp,\n  defaultValue = [],\n  onValueChange,\n  multiple = false,\n  required = false,\n  orientation = \"horizontal\",\n  size = \"md\",\n  disabled,\n  className,\n  children,\n  ...rest\n}: ToggleGroupProps) {\n  const [value, setValue] = useControllableState({ value: valueProp, defaultValue, onChange: onValueChange });\n  const id = useId();\n  const vertical = orientation === \"vertical\";\n\n  return (\n    <GroupContext.Provider value={{ value, multiple, size, vertical }}>\n      <LayoutGroup id={id}>\n        <BaseToggleGroup\n          value={value}\n          onValueChange={(next) => {\n            // Required single groups ignore the press that would leave nothing on.\n            if (required && !multiple && next.length === 0) return;\n            setValue(next);\n          }}\n          multiple={multiple}\n          orientation={orientation}\n          disabled={disabled}\n          className={cn(\n            \"inline-flex shrink-0 border border-line-2 bg-raised p-0.5 shadow-[var(--shadow)]\",\n            \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n            vertical ? \"flex-col\" : \"flex-row items-center\",\n            heights[size].radius,\n            className,\n          )}\n          {...(rest as BaseToggleGroup.Props)}\n        >\n          {children}\n        </BaseToggleGroup>\n      </LayoutGroup>\n    </GroupContext.Provider>\n  );\n}\n\nexport type ToggleGroupItemProps = Omit<React.ComponentProps<\"button\">, \"value\" | \"className\"> & {\n  className?: string;\n  value: string;\n  /** Makes the item an icon-only square: its accessible name and tooltip. */\n  label?: string;\n  tooltip?: boolean | React.ReactNode;\n  shortcut?: string;\n  tooltipSide?: TooltipSide;\n  children: React.ReactNode;\n};\n\nexport function ToggleGroupItem({\n  value,\n  label,\n  tooltip = true,\n  shortcut,\n  tooltipSide = \"top\",\n  disabled,\n  className,\n  children,\n  ...rest\n}: ToggleGroupItemProps) {\n  const group = useContext(GroupContext);\n  const reduce = useReducedMotion();\n  const on = group.value.includes(value);\n  const iconOnly = label !== undefined;\n  const h = heights[group.size];\n\n  const wash = on && (\n    <motion.span\n      key=\"wash\"\n      aria-hidden\n      data-wash=\"\"\n      // Single mode: one wash, handed from item to item. Multiple: each item owns one.\n      layoutId={group.multiple ? undefined : \"wash\"}\n      className={cn(\n        \"absolute inset-0 -z-10 bg-fg/[0.08]\",\n        // Only the per-item washes transition their corners; the shared one is moved by Motion.\n        group.multiple && \"transition-[border-radius] duration-150 ease-out-quart\",\n        h.wash,\n      )}\n      initial={group.multiple ? (reduce ? { opacity: 0 } : { opacity: 0, scale: 0.85 }) : false}\n      animate={{ opacity: 1, scale: 1 }}\n      exit={reduce ? { opacity: 0, transition: { duration: 0.1 } } : { opacity: 0, scale: 0.9, transition: { duration: 0.12 } }}\n      transition={reduce ? { duration: 0 } : group.multiple ? spring.pop : spring.snappy}\n    />\n  );\n\n  const item = (\n    <Toggle\n      value={value}\n      disabled={disabled}\n      aria-label={label}\n      className={cn(\n        \"relative isolate inline-flex shrink-0 select-none items-center justify-center font-medium tracking-[-0.005em] outline-none\",\n        \"touch-manipulation [-webkit-tap-highlight-color:transparent]\",\n        \"text-fg-3 transition-[color,scale] duration-150 ease-out-quart hover:text-fg-2 data-pressed:text-fg\",\n        \"active:scale-[0.95] active:duration-75\",\n        \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-fg-3\",\n        \"data-disabled:pointer-events-none data-disabled:opacity-40\",\n        // Neighboring washes in multiple mode join: the touching corners go square.\n        group.vertical\n          ? \"[&:has(+[data-pressed])>[data-wash]]:rounded-b-none [[data-pressed]+&>[data-wash]]:rounded-t-none\"\n          : \"[&:has(+[data-pressed])>[data-wash]]:rounded-e-none [[data-pressed]+&>[data-wash]]:rounded-s-none\",\n        group.vertical && !iconOnly && \"w-full justify-start\",\n        // Taller to a finger, along the axis that doesn't overlap a neighbor.\n        group.vertical\n          ? \"pointer-coarse:after:absolute pointer-coarse:after:inset-y-0 pointer-coarse:after:-inset-x-2.5\"\n          : \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-2.5\",\n        iconOnly ? h.square : h.item,\n        h.text,\n        h.wash,\n        className,\n      )}\n      {...(rest as Toggle.Props)}\n    >\n      <AnimatePresence initial={false}>{wash}</AnimatePresence>\n      {children}\n    </Toggle>\n  );\n\n  if (!iconOnly || tooltip === false || tooltip == null) return item;\n  return (\n    <ActionTooltip content={tooltip === true ? label : tooltip} shortcut={shortcut} side={tooltipSide} disabled={disabled}>\n      {item}\n    </ActionTooltip>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/toggle-group"
}
