{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle-group",
  "type": "registry:ui",
  "title": "Toggle Group",
  "description": "Toggle Group is exclusive segmented selection (radiogroup) with joined chrome — Segmented Control is this presentation, not a separate component.",
  "author": "Skrewww",
  "dependencies": [],
  "registryDependencies": [
    "@skrewww/foundation"
  ],
  "docs": "Host requirements (assumed already present, not installed by this command): react, react-dom.",
  "files": [
    {
      "path": "components/ui/ToggleGroup.tsx",
      "content": "/** @skrewww-component toggle-group */\n\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { useControllableState } from \"@/lib/use-controllable\";\nimport { handleToggleGroupKeyDown } from \"@/components/ui/internal/toggle-group-keyboard\";\nimport styles from \"@/components/ui/toggle-group.module.css\";\n\nexport type ToggleGroupSize = \"sm\" | \"md\" | \"lg\";\nexport type ToggleGroupOrientation = \"horizontal\" | \"vertical\";\n\ntype ToggleGroupContextValue = {\n  value: string;\n  setValue: (next: string) => void;\n  disabled: boolean;\n  orientation: ToggleGroupOrientation;\n  size: ToggleGroupSize;\n  registerItem: (value: string) => () => void;\n  orderedValues: string[];\n};\n\nconst ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null);\n\nfunction useToggleGroupContext(component: string): ToggleGroupContextValue {\n  const context = useContext(ToggleGroupContext);\n  if (!context) {\n    throw new Error(`${component} must be used within ToggleGroup`);\n  }\n  return context;\n}\n\nexport type ToggleGroupProps = {\n  /**\n   * Selected item value. Prefer a concrete default for radiogroup UX.\n   * Empty string means none selected yet (first enabled item remains tabbable).\n   */\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  disabled?: boolean;\n  orientation?: ToggleGroupOrientation;\n  size?: ToggleGroupSize;\n  \"aria-label\"?: string;\n  \"aria-labelledby\"?: string;\n  className?: string;\n  children: ReactNode;\n};\n\n/**\n * Exclusive selection control with segmented / joined chrome.\n *\n * Single-selection only (`role=\"radiogroup\"` + `role=\"radio\"`).\n * “Segmented Control” is this presentation — not a separate Skrewww component.\n *\n * Not Button Group (peer actions), not Radio Group (form field radios),\n * not Tabs (content panels).\n */\nexport function ToggleGroup({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  disabled = false,\n  orientation = \"horizontal\",\n  size = \"md\",\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n  className,\n  children,\n}: ToggleGroupProps) {\n  const [current, setCurrent] = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  });\n  const [orderedValues, setOrderedValues] = useState<string[]>([]);\n\n  const registerItem = useCallback((itemValue: string) => {\n    setOrderedValues((prev) => (prev.includes(itemValue) ? prev : [...prev, itemValue]));\n    return () => {\n      setOrderedValues((prev) => prev.filter((entry) => entry !== itemValue));\n    };\n  }, []);\n\n  const contextValue = useMemo(\n    () => ({\n      value: current,\n      setValue: setCurrent,\n      disabled,\n      orientation,\n      size,\n      registerItem,\n      orderedValues,\n    }),\n    [current, disabled, orientation, orderedValues, registerItem, setCurrent, size],\n  );\n\n  return (\n    <ToggleGroupContext.Provider value={contextValue}>\n      <div\n        role=\"radiogroup\"\n        aria-label={ariaLabel}\n        aria-labelledby={ariaLabelledBy}\n        aria-orientation={orientation}\n        aria-disabled={disabled || undefined}\n        className={cn(styles.group, orientation === \"vertical\" && styles.vertical, className)}\n      >\n        {children}\n      </div>\n    </ToggleGroupContext.Provider>\n  );\n}\n\nexport type ToggleGroupItemProps = {\n  value: string;\n  disabled?: boolean;\n  children: ReactNode;\n  className?: string;\n};\n\nconst sizeClass: Record<ToggleGroupSize, string> = {\n  sm: styles.sizeSm,\n  md: styles.sizeMd,\n  lg: styles.sizeLg,\n};\n\nexport function ToggleGroupItem({\n  value,\n  disabled = false,\n  children,\n  className,\n}: ToggleGroupItemProps) {\n  const { value: selectedValue, setValue, disabled: groupDisabled, orientation, size, registerItem, orderedValues } =\n    useToggleGroupContext(\"ToggleGroupItem\");\n  const isDisabled = groupDisabled || disabled;\n  const checked = selectedValue === value;\n\n  useEffect(() => registerItem(value), [registerItem, value]);\n\n  const firstTabStopValue = orderedValues[0];\n  const isTabStop =\n    !isDisabled && (checked || (selectedValue === \"\" && value === firstTabStopValue));\n\n  function select() {\n    if (isDisabled) return;\n    setValue(value);\n  }\n\n  function handleKeyDown(event: KeyboardEvent<HTMLButtonElement>) {\n    if (isDisabled) return;\n    handleToggleGroupKeyDown(event, orientation, (next) => {\n      setValue(next);\n    });\n  }\n\n  return (\n    <button\n      type=\"button\"\n      role=\"radio\"\n      data-value={value}\n      aria-checked={checked}\n      disabled={isDisabled}\n      tabIndex={isTabStop ? 0 : -1}\n      className={cn(styles.item, sizeClass[size], className)}\n      onClick={select}\n      onKeyDown={handleKeyDown}\n    >\n      {children}\n    </button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/ToggleGroup.tsx"
    },
    {
      "path": "components/ui/toggle-group.module.css",
      "content": ".group {\n  box-sizing: border-box;\n  display: inline-flex;\n  flex-direction: row;\n  align-items: stretch;\n  gap: 0.125rem; /* spacing/2 — same joined gap as Button Group */\n  max-width: 100%;\n  border: 1px solid var(--semantic-border-default);\n  border-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  background: var(--semantic-border-default);\n}\n\n.vertical {\n  flex-direction: column;\n}\n\n.item {\n  box-sizing: border-box;\n  position: relative;\n  display: inline-flex;\n  flex: 1 1 auto;\n  align-items: center;\n  justify-content: center;\n  margin: 0;\n  border: 1px solid transparent;\n  border-radius: 0;\n  background-color: var(--surface-fill-control);\n  background-image: var(--component-surface-gradient-overlay);\n  backdrop-filter: var(--glass-backdrop-filter-sm, none);\n  color: var(--semantic-text-primary);\n  font-family: inherit;\n  font-weight: 500;\n  line-height: 1.2;\n  white-space: nowrap;\n  cursor: pointer;\n  transition:\n    background-color 0.15s ease,\n    color 0.15s ease,\n    box-shadow 0.15s ease;\n}\n\n.group > .item:first-child {\n  border-start-start-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-end-start-radius: var(--component-button-radius-control, var(--shape-radius-control));\n}\n\n.group > .item:last-child {\n  border-start-end-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-end-end-radius: var(--component-button-radius-control, var(--shape-radius-control));\n}\n\n.vertical > .item:first-child {\n  border-start-start-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-start-end-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-end-start-radius: 0;\n  border-end-end-radius: 0;\n}\n\n.vertical > .item:last-child {\n  border-end-start-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-end-end-radius: var(--component-button-radius-control, var(--shape-radius-control));\n  border-start-start-radius: 0;\n  border-start-end-radius: 0;\n}\n\n.group > .item:only-child {\n  border-radius: var(--component-button-radius-control, var(--shape-radius-control));\n}\n\n/* Vertical + Pill: a full 9999px control radius on a narrow, stacked item\n   reads as circular rather than pill-shaped. Cap at Pill mode's own\n   container radius — already this system's \"capped, not full stadium\"\n   value (see [data-skrewww-shape=\"pill\"] in styles/tokens.css) — instead\n   of inventing a new number. Horizontal Pill is unaffected and keeps the\n   true stadium shape. */\n[data-skrewww-shape=\"pill\"] .group.vertical {\n  border-radius: var(--shape-radius-container);\n}\n\n[data-skrewww-shape=\"pill\"] .vertical > .item:first-child {\n  border-start-start-radius: var(--shape-radius-container);\n  border-start-end-radius: var(--shape-radius-container);\n}\n\n[data-skrewww-shape=\"pill\"] .vertical > .item:last-child {\n  border-end-start-radius: var(--shape-radius-container);\n  border-end-end-radius: var(--shape-radius-container);\n}\n\n[data-skrewww-shape=\"pill\"] .group.vertical > .item:only-child {\n  border-radius: var(--shape-radius-container);\n}\n\n.sizeSm {\n  min-height: var(--control-height-sm);\n  padding: 0 var(--control-padding-x-sm);\n  font-size: var(--control-font-size-sm);\n}\n\n.sizeMd {\n  min-height: var(--control-height-md);\n  padding: 0 var(--control-padding-x-md);\n  font-size: var(--control-font-size-md);\n}\n\n.sizeLg {\n  min-height: var(--control-height-lg);\n  padding: 0 var(--control-padding-x-lg);\n  font-size: var(--control-font-size-lg);\n}\n\n.item:hover:not(:disabled):not([aria-checked=\"true\"]) {\n  background-color: var(--semantic-surface-subtle);\n}\n\n.item[aria-checked=\"true\"] {\n  color: var(--semantic-text-primary);\n  background-color: var(--semantic-surface-elevated, var(--semantic-surface-default));\n  box-shadow: inset 0 0 0 1px var(--semantic-border-strong, var(--semantic-border-default));\n  z-index: 1;\n}\n\n.item:focus {\n  outline: none;\n}\n\n.item:focus-visible {\n  z-index: 2;\n  outline: 2px solid var(--semantic-focus-ring);\n  outline-offset: 0;\n}\n\n.item:disabled {\n  cursor: not-allowed;\n  opacity: var(--opacity-disabled);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .item {\n    transition: none;\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/components/ui/toggle-group.module.css"
    },
    {
      "path": "lib/cn.ts",
      "content": "export function cn(...classes: Array<string | false | null | undefined>): string {\n  return classes.filter(Boolean).join(\" \");\n}\n",
      "type": "registry:lib",
      "target": "~/lib/cn.ts"
    },
    {
      "path": "lib/use-controllable.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport function useControllableState<T>({\n  value,\n  defaultValue,\n  onChange,\n  controlled: controlledProp,\n  valueProvided,\n}: {\n  value?: T;\n  defaultValue?: T;\n  onChange?: (next: T) => void;\n  /** Explicit controlled override — prefer `valueProvided` from the component root. */\n  controlled?: boolean;\n  /** True when the parent passed a `value` prop, including `undefined`. */\n  valueProvided?: boolean;\n}) {\n  const [uncontrolled, setUncontrolled] = useState(defaultValue as T);\n  const isControlled = controlledProp ?? valueProvided ?? value !== undefined;\n  const current = isControlled ? (value as T) : uncontrolled;\n  const wasControlledRef = useRef(isControlled);\n\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\") return;\n    if (wasControlledRef.current !== isControlled) {\n      console.warn(\n        \"[Skrewww] A component changed between controlled and uncontrolled modes. Keep `value` or `defaultValue` usage consistent across renders.\",\n      );\n    }\n    wasControlledRef.current = isControlled;\n  }, [isControlled]);\n\n  const setValue = useCallback(\n    (next: T) => {\n      if (!isControlled) setUncontrolled(next);\n      onChange?.(next);\n    },\n    [isControlled, onChange],\n  );\n\n  return [current, setValue] as const;\n}\n",
      "type": "registry:lib",
      "target": "~/lib/use-controllable.ts"
    },
    {
      "path": "components/ui/internal/toggle-group-keyboard.ts",
      "content": "/**\n * Keyboard navigation for Toggle Group (radiogroup / radio).\n * Arrow keys move focus and select (automatic activation).\n */\n\nexport function getToggleGroupRadios(container: HTMLElement | null): HTMLElement[] {\n  if (!container) return [];\n  return Array.from(container.querySelectorAll<HTMLElement>('[role=\"radio\"]')).filter(\n    (element) =>\n      !element.hasAttribute(\"disabled\") && element.getAttribute(\"aria-disabled\") !== \"true\",\n  );\n}\n\nexport function handleToggleGroupKeyDown(\n  event: React.KeyboardEvent<HTMLElement>,\n  orientation: \"horizontal\" | \"vertical\",\n  onSelect: (value: string) => void,\n) {\n  const target = event.currentTarget;\n  if (target.getAttribute(\"role\") !== \"radio\") return;\n\n  const container = target.closest('[role=\"radiogroup\"]') as HTMLElement | null;\n  const items = getToggleGroupRadios(container);\n  const currentIndex = items.indexOf(target);\n  if (currentIndex === -1 || items.length === 0) return;\n\n  const previousKey = orientation === \"vertical\" ? \"ArrowUp\" : \"ArrowLeft\";\n  const nextKey = orientation === \"vertical\" ? \"ArrowDown\" : \"ArrowRight\";\n\n  let nextIndex = currentIndex;\n  if (event.key === previousKey) {\n    event.preventDefault();\n    nextIndex = (currentIndex - 1 + items.length) % items.length;\n  } else if (event.key === nextKey) {\n    event.preventDefault();\n    nextIndex = (currentIndex + 1) % items.length;\n  } else if (event.key === \"Home\") {\n    event.preventDefault();\n    nextIndex = 0;\n  } else if (event.key === \"End\") {\n    event.preventDefault();\n    nextIndex = items.length - 1;\n  } else if (event.key === \" \" || event.key === \"Enter\") {\n    event.preventDefault();\n    const value = target.getAttribute(\"data-value\");\n    if (value != null) onSelect(value);\n    return;\n  } else {\n    return;\n  }\n\n  const next = items[nextIndex];\n  next?.focus();\n  const value = next?.getAttribute(\"data-value\");\n  if (value != null) onSelect(value);\n}\n",
      "type": "registry:lib",
      "target": "~/components/ui/internal/toggle-group-keyboard.ts"
    }
  ]
}
