{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "type": "registry:ui",
  "title": "Segmented control",
  "description": "Switch between a small set of related views.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json",
    "https://irisui.dev/r/motion-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/components/segmented-control/segmented-control.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, type KeyboardEvent, type ReactNode } from \"react\";\nimport { LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"../lib/utils\";\nimport { motionTokens } from \"../lib/motion-tokens\";\n\nexport interface Segment {\n  value: string;\n  label: string;\n  /** Optional content after the label, such as a badge. */ accessory?: ReactNode;\n}\nexport interface SegmentedControlProps {\n  options: Segment[];\n  value: string;\n  onValueChange: (value: string) => void;\n  label?: string;\n  /** Called when the pointer or focus reaches an option, before it is chosen. Use it to start loading what that option shows. */\n  onOptionIntent?: (value: string) => void;\n  className?: string;\n}\n\nexport default function SegmentedControl({\n  options,\n  value,\n  onValueChange,\n  label,\n  onOptionIntent,\n  className,\n}: SegmentedControlProps) {\n  const id = useId();\n  const reduced = useReducedMotion();\n  const track = useRef<HTMLDivElement>(null);\n\n  // When the options are wider than the container, the track scrolls inside itself. Edges fade only on the side with more to see.\n  useLayoutEffect(() => {\n    const node = track.current;\n    if (!node) return;\n    const edges = () => {\n      const rest = node.scrollWidth - node.clientWidth - node.scrollLeft;\n      node.toggleAttribute(\"data-fade-start\", node.scrollLeft > 1);\n      node.toggleAttribute(\"data-fade-end\", rest > 1);\n    };\n    edges();\n    node.addEventListener(\"scroll\", edges, { passive: true });\n    const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(edges);\n    observer?.observe(node);\n    return () => {\n      node.removeEventListener(\"scroll\", edges);\n      observer?.disconnect();\n    };\n  }, [options.length]);\n\n  // The selected option is always scrolled fully into view, with a little room so it clears the fade.\n  const first = useRef(true);\n  useEffect(() => {\n    const node = track.current;\n    const button = node?.querySelector<HTMLElement>('[aria-pressed=\"true\"]');\n    if (!node || !button || node.scrollWidth <= node.clientWidth) {\n      first.current = false;\n      return;\n    }\n    const room = 20,\n      start = button.offsetLeft - room,\n      end = button.offsetLeft + button.offsetWidth + room - node.clientWidth;\n    const left = node.scrollLeft > start ? start : node.scrollLeft < end ? end : node.scrollLeft;\n    if (left !== node.scrollLeft)\n      node.scrollTo({ left: Math.max(0, left), behavior: first.current || reduced ? \"auto\" : \"smooth\" });\n    first.current = false;\n  }, [value, reduced]);\n\n  // Arrow keys, Home and End move the selection like a tab list; only the selected option is a tab stop.\n  const selectedIndex = Math.max(\n    0,\n    options.findIndex((option) => option.value === value),\n  );\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    const last = options.length - 1;\n    const target =\n      event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n        ? selectedIndex === last\n          ? 0\n          : selectedIndex + 1\n        : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n          ? selectedIndex === 0\n            ? last\n            : selectedIndex - 1\n          : event.key === \"Home\"\n            ? 0\n            : event.key === \"End\"\n              ? last\n              : -1;\n    if (target < 0 || !options[target]) return;\n    event.preventDefault();\n    onValueChange(options[target].value);\n    track.current\n      ?.querySelector<HTMLElement>(`[data-value=\"${CSS.escape(options[target].value)}\"]`)\n      ?.focus({ preventScroll: true });\n  };\n\n  // The frame can shrink below its content inside flex and grid parents; the track then scrolls inside it. The track owns the\n  // stacking context so the gliding highlight passes under every label, not over earlier ones.\n  return (\n    <div\n      className={cn(\"inline-flex max-w-full min-w-0 shrink rounded-button border border-line bg-hover\", className)}\n      role=\"group\"\n      aria-label={label}\n    >\n      <LayoutGroup id={id}>\n        <motion.div\n          ref={track}\n          layoutScroll\n          className=\"isolate flex min-w-0 gap-0.5 overflow-x-auto overscroll-x-contain rounded-[inherit] p-[3px] [--fade-end:0px] [--fade-start:0px] [mask-image:linear-gradient(to_right,transparent,#000_var(--fade-start),#000_calc(100%_-_var(--fade-end)),transparent)] [scrollbar-width:none] data-fade-end:[--fade-end:20px] data-fade-start:[--fade-start:20px] [&::-webkit-scrollbar]:hidden\"\n        >\n          {options.map((option, index) => (\n            <button\n              key={option.value}\n              id={`${id}-${option.value}`}\n              className=\"relative min-h-control-sm flex-none cursor-pointer rounded-button border-0 bg-transparent px-3 text-sm/[inherit] font-medium whitespace-nowrap text-ink-3 transition-[color] duration-fast ease-standard [-webkit-tap-highlight-color:transparent] hover:text-ink active:text-ink aria-pressed:text-ink motion-reduce:transition-none max-[380px]:px-2.5\"\n              type=\"button\"\n              data-value={option.value}\n              aria-pressed={value === option.value}\n              tabIndex={index === selectedIndex ? 0 : -1}\n              onClick={() => onValueChange(option.value)}\n              onKeyDown={onKeyDown}\n              onPointerEnter={onOptionIntent ? () => onOptionIntent(option.value) : undefined}\n              onFocus={onOptionIntent ? () => onOptionIntent(option.value) : undefined}\n            >\n              {value === option.value && (\n                <motion.span\n                  className=\"absolute inset-0 -z-1 rounded-[inherit] border border-line bg-surface shadow-resting\"\n                  layoutId=\"selection\"\n                  layoutDependency={value}\n                  transition={reduced ? { duration: 0 } : motionTokens.spring.morph}\n                  aria-hidden=\"true\"\n                />\n              )}\n              <span className=\"relative z-1 inline-flex items-center gap-1.5\">\n                {option.label}\n                {option.accessory}\n              </span>\n            </button>\n          ))}\n        </motion.div>\n      </LayoutGroup>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/segmented-control/segmented-control.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/segmented-control",
  "meta": {
    "kind": "component",
    "group": "Inputs",
    "export": "SegmentedControl",
    "tags": [
      "control",
      "choice"
    ],
    "docs": "https://irisui.dev/components/segmented-control"
  }
}
