{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "title": "Segmented Control",
  "description": "A row of 2–4 mutually exclusive choices drawn as one moving pill on a shared track — the iOS-style segmented control, and what most dashboards use to switch a view or a range without navigating anywhere. Reach for it wherever a single setting has a handful of choices that all fit on screen at once: List/Grid/Board, Day/Week/Month or 24h/7d/30d above a chart, Light/Dark/System, °C/°F, Monthly/Yearly on a pricing page, Newest/Oldest, All/Active/Archived, Preview/Code on a docs example, Table/JSON on a response viewer. Common asks it answers: \"segmented control\", \"segmented button\", \"iOS segmented control\", \"pill toggle\", \"toggle switcher\", \"view switcher\", \"time range switcher\", \"chart period selector\", \"sort or filter toggle\", \"unit toggle\", \"tabs without panels\", \"Ant Design Segmented\", \"MUI ToggleButtonGroup\" — the control usually faked with a row of buttons and a useState. How it differs from the neighbours official shadcn/ui ships: tabs and toggle-group each pull in a Radix package (@radix-ui/react-tabs, @radix-ui/react-toggle-group), and button-group is a layout wrapper with no selection of its own. This is one file with no dependencies, and it is a real radio group — role=radiogroup on the track, role=radio and aria-checked on every segment — so assistive technology announces one setting with a selected option among several rather than a row of unrelated buttons. Tabs additionally owns panels and the tab/tabpanel relationship, which is the wrong contract when the choice only filters or reframes data already on the page, and a switch only covers two states. The keyboard follows the radio pattern rather than the button one: arrow keys (left/right and up/down) move and select in a single press and wrap around the ends, Home/End jump to the first and last usable segment, disabled segments are stepped over instead of trapping focus, and a roving tabindex keeps the whole group one tab stop with the selected segment as the entry point. Also: per-segment disabling as well as a whole-group disabled state, controlled or uncontrolled through a string value with onValueChange, a focus-visible ring, and shadcn tokens throughout so it follows the theme in light and dark.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/ui/segmented-control.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface SegmentedControlOption {\n  /** Stable value reported through onValueChange and compared against value. */\n  value: string\n  /** Visible label; falls back to the value when omitted. */\n  label?: React.ReactNode\n  /** Disable just this segment while leaving the rest usable. */\n  disabled?: boolean\n}\n\ninterface SegmentedControlProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"div\">,\n    \"onChange\" | \"defaultValue\"\n  > {\n  /** The 2–4 mutually exclusive choices, rendered left to right. */\n  options: SegmentedControlOption[]\n  /** Controlled selected value. Pair with onValueChange. */\n  value?: string\n  /** Initial selection when uncontrolled (defaults to the first option). */\n  defaultValue?: string\n  /** Fires with the new value whenever the selection changes. */\n  onValueChange?: (value: string) => void\n  /** Disable the whole control. */\n  disabled?: boolean\n  /** Accessible name for the group, e.g. \"View\" or \"Time range\". */\n  \"aria-label\"?: string\n}\n\nexport const SegmentedControl = React.forwardRef<\n  HTMLDivElement,\n  SegmentedControlProps\n>(function SegmentedControl(\n  {\n    className,\n    options,\n    value,\n    defaultValue,\n    onValueChange,\n    disabled,\n    \"aria-label\": ariaLabel,\n    ...props\n  },\n  ref\n) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState(\n    () => defaultValue ?? options[0]?.value\n  )\n  const selected = isControlled ? value : internal\n\n  const refs = React.useRef<(HTMLButtonElement | null)[]>([])\n\n  function select(next: string) {\n    if (!isControlled) setInternal(next)\n    if (next !== selected) onValueChange?.(next)\n    const idx = options.findIndex((o) => o.value === next)\n    refs.current[idx]?.focus()\n  }\n\n  // Next enabled segment in a direction, wrapping around the ends.\n  function step(start: number, dir: 1 | -1) {\n    const count = options.length\n    for (let i = 1; i <= count; i++) {\n      const idx = (((start + dir * i) % count) + count) % count\n      if (!options[idx]?.disabled) return idx\n    }\n    return start\n  }\n\n  // First enabled segment scanning from one end (Home/End).\n  function edge(dir: 1 | -1) {\n    const count = options.length\n    for (let i = 0; i < count; i++) {\n      const idx = dir === 1 ? i : count - 1 - i\n      if (!options[idx]?.disabled) return idx\n    }\n    return 0\n  }\n\n  function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {\n    const start = options.findIndex((o) => o.value === selected)\n    let idx: number\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        idx = step(start, 1)\n        break\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        idx = step(start, -1)\n        break\n      case \"Home\":\n        idx = edge(1)\n        break\n      case \"End\":\n        idx = edge(-1)\n        break\n      default:\n        return\n    }\n    e.preventDefault()\n    select(options[idx].value)\n  }\n\n  // Roving tabindex: the selected segment is tabbable, unless it's disabled, in\n  // which case fall back to the first enabled one so the group stays reachable.\n  const selectedIdx = options.findIndex((o) => o.value === selected)\n  const rovingIdx =\n    selectedIdx >= 0 && !options[selectedIdx]?.disabled\n      ? selectedIdx\n      : edge(1)\n\n  return (\n    <div\n      ref={ref}\n      role=\"radiogroup\"\n      aria-label={ariaLabel}\n      {...props}\n      onKeyDown={disabled ? undefined : handleKeyDown}\n      className={cn(\n        \"inline-flex h-9 items-center gap-1 rounded-md bg-muted p-1 text-muted-foreground\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className\n      )}\n    >\n      {options.map((option, i) => {\n        const isSelected = option.value === selected\n        const isDisabled = disabled || option.disabled\n        return (\n          <button\n            key={option.value}\n            ref={(el) => {\n              refs.current[i] = el\n            }}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={isSelected}\n            disabled={isDisabled}\n            tabIndex={!disabled && i === rovingIdx ? 0 : -1}\n            onClick={() => select(option.value)}\n            className={cn(\n              \"inline-flex h-7 items-center justify-center whitespace-nowrap rounded-sm px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n              isSelected\n                ? \"bg-background text-foreground shadow-sm\"\n                : \"hover:text-foreground\"\n            )}\n          >\n            {option.label ?? option.value}\n          </button>\n        )\n      })}\n    </div>\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}