{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "title": "Segmented Control",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A macOS segmented control: mutually exclusive options with a selected pill that springs between segments and hairline dividers between unselected ones. A Radix radio group underneath.",
  "dependencies": [
    "motion@^12",
    "radix-ui",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/direction.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/segmented-control.tsx",
      "content": "// Ballmac UI: Segmented Control. https://ui.ballmac.com/components/segmented-control\n\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { MotionConfig, motion } from \"motion/react\"\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\"\n\nimport { useDirection } from \"@/lib/ballmac/direction\"\nimport { cn } from \"@/lib/utils\"\n\nconst segmentedControlVariants = cva(\n  \"relative inline-flex w-fit max-w-full items-center rounded-[9px] bg-foreground/[0.07] p-0.5 shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.06)] dark:bg-white/[0.08] dark:shadow-[inset_0_0_0_0.5px_rgb(255_255_255/0.06)] data-[disabled]:opacity-50\",\n  {\n    variants: {\n      size: {\n        sm: \"h-7 text-xs\",\n        default: \"h-8 text-[13px]\",\n        lg: \"h-9 text-sm\",\n      },\n    },\n    defaultVariants: { size: \"default\" },\n  }\n)\n\ntype SegmentedControlContextValue = { value: string; layoutId: string; values: string[] }\nconst SegmentedControlContext = React.createContext<SegmentedControlContextValue | null>(null)\n\ntype SegmentedControlProps = Omit<React.ComponentProps<typeof RadioGroupPrimitive.Root>, \"value\" | \"defaultValue\" | \"onValueChange\" | \"orientation\"> &\n  VariantProps<typeof segmentedControlVariants> & {\n    /** Selected value (controlled). */\n    value?: string\n    /** Initially selected value when uncontrolled. Defaults to the first item. */\n    defaultValue?: string\n    /** Called with the newly selected value. */\n    onValueChange?: (value: string) => void\n    /** Stretch to the container width with equal segments. */\n    fullWidth?: boolean\n  }\n\n/**\n * A macOS segmented control: a row of mutually exclusive options with a selected pill that slides between them.\n * It is a radio group (Radix): Tab focuses the selected segment, arrow keys move and select.\n */\nfunction SegmentedControl({\n  value: valueProp,\n  defaultValue,\n  onValueChange,\n  size = \"default\",\n  fullWidth = false,\n  className,\n  children,\n  ...props\n}: SegmentedControlProps) {\n  const values = React.Children.toArray(children).flatMap((child) =>\n    React.isValidElement<{ value?: unknown }>(child) && typeof child.props.value === \"string\" ? [child.props.value] : []\n  )\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? values[0] ?? \"\")\n  const value = valueProp ?? uncontrolled\n  const layoutId = `segmented-pill-${React.useId()}`\n  const dir = useDirection(props.dir)\n\n  return (\n    <SegmentedControlContext.Provider value={{ value, layoutId, values }}>\n      <MotionConfig reducedMotion=\"user\">\n        <RadioGroupPrimitive.Root\n          data-slot=\"segmented-control\"\n          orientation=\"horizontal\"\n          dir={dir}\n          value={value}\n          onValueChange={(next) => {\n            if (valueProp === undefined) setUncontrolled(next)\n            onValueChange?.(next)\n          }}\n          className={cn(segmentedControlVariants({ size }), fullWidth && \"flex w-full [&>[data-slot=segmented-control-item]]:flex-1\", className)}\n          {...props}\n        >\n          {children}\n        </RadioGroupPrimitive.Root>\n      </MotionConfig>\n    </SegmentedControlContext.Provider>\n  )\n}\n\ntype SegmentedControlItemProps = React.ComponentProps<typeof RadioGroupPrimitive.Item>\n\n/** One segment. Icon-only segments need an aria-label. */\nfunction SegmentedControlItem({ value, className, children, ...props }: SegmentedControlItemProps) {\n  const context = React.useContext(SegmentedControlContext)\n  if (!context) throw new Error(\"SegmentedControlItem must be used inside <SegmentedControl>.\")\n  const selected = context.value === value\n  const index = context.values.indexOf(value)\n  // A hairline divider sits between two unselected neighbors, as on macOS.\n  const showDivider = index > 0 && !selected && context.values[index - 1] !== context.value\n\n  return (\n    <RadioGroupPrimitive.Item\n      value={value}\n      data-slot=\"segmented-control-item\"\n      className={cn(\n        \"group/segment relative flex h-full flex-auto cursor-default items-center justify-center gap-1.5 rounded-[7px] px-3 font-medium whitespace-nowrap text-foreground/70 outline-none transition-colors duration-150 select-none\",\n        \"hover:text-foreground data-[state=checked]:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50\",\n        \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"segmented-control-divider\"\n        className={cn(\n          \"pointer-events-none absolute top-1/2 -start-px h-[45%] w-px -translate-y-1/2 bg-foreground/15 transition-opacity duration-150\",\n          showDivider ? \"opacity-100\" : \"opacity-0\"\n        )}\n      />\n      {selected ? (\n        <motion.span\n          layoutId={context.layoutId}\n          aria-hidden=\"true\"\n          data-slot=\"segmented-control-indicator\"\n          className=\"absolute inset-0 rounded-[7px] bg-background shadow-[0_0_0_0.5px_rgb(0_0_0/0.08),0_1px_2px_0_rgb(0_0_0/0.14),0_2px_6px_-2px_rgb(0_0_0/0.1)] dark:bg-white/[0.2] dark:shadow-[0_0_0_0.5px_rgb(0_0_0/0.4),inset_0_0.5px_0_rgb(255_255_255/0.12),0_1px_2px_rgb(0_0_0/0.3)]\"\n          transition={{ type: \"spring\", stiffness: 500, damping: 38, mass: 0.8 }}\n        />\n      ) : null}\n      <span className=\"relative z-10 flex items-center gap-1.5\">{children}</span>\n    </RadioGroupPrimitive.Item>\n  )\n}\n\nexport { SegmentedControl, SegmentedControlItem, segmentedControlVariants, type SegmentedControlItemProps, type SegmentedControlProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/segmented-control.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "segmented control",
      "toggle group",
      "radio group",
      "tabs",
      "macos",
      "switcher",
      "form"
    ],
    "version": "1.0.0",
    "updated": "2026-09-29",
    "ai": {
      "summary": "<SegmentedControl aria-label value|defaultValue onValueChange size><SegmentedControlItem value>Label</SegmentedControlItem>…</SegmentedControl>. Exactly one segment is always selected. Icon-only items need aria-label; fullWidth stretches segments equally.",
      "whenToUse": [
        "Switching a view mode or range (Day / Week / Month, List / Grid)",
        "A short, always-visible choice in settings (Light / Dark / Auto)",
        "Filtering one list by a few exclusive categories"
      ],
      "whenNotToUse": [
        "Switching between panels of content with their own semantics (use tabs)",
        "More than about five options or long labels (use select)",
        "Options that can be combined (use checkboxes or a toggle group)"
      ],
      "composesWith": [
        "mac-window"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Focuses the selected segment (one tab stop)"
        },
        {
          "keys": "ArrowLeft / ArrowRight",
          "action": "Moves to and selects the previous or next segment, wrapping"
        },
        {
          "keys": "Space",
          "action": "Selects the focused segment"
        }
      ],
      "customization": [
        "size: sm | default | lg",
        "fullWidth: stretch to the container with equal segments",
        "value / defaultValue / onValueChange: controlled or uncontrolled",
        "Parts: [data-slot=segmented-control-indicator] (the sliding pill), [data-slot=segmented-control-divider]",
        "Under reduced motion the pill jumps instead of sliding"
      ]
    },
    "examples": [
      "segmented-control-demo"
    ],
    "url": "https://ui.ballmac.com/components/segmented-control"
  },
  "categories": [
    "macos"
  ],
  "type": "registry:ui"
}