{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "type": "registry:ui",
  "title": "Segmented control",
  "description": "Carries one shared highlight between options.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "navigation"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/segmented-control/",
  "meta": {
    "engines": [
      "motion"
    ],
    "runtimeCost": "light",
    "signature": "One shared highlight moves continuously between options",
    "sceneFamily": "product-mono",
    "motionRole": "ui",
    "primaryState": "Shared highlight settled on the current segment",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/segmented-control.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/segmented-control.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\n\nconst CELL = { type: \"spring\", stiffness: 520, damping: 34, mass: 0.45 } as const;\n\nconst SEG =\n  \"flex min-h-11 items-center justify-center px-3 text-center text-[13px] font-medium leading-[18px] tracking-[-0.01em] whitespace-nowrap\";\n\nexport type SegmentedOption = {\n  value: string;\n  label: string;\n  disabled?: boolean;\n};\n\nexport type SegmentedControlProps = {\n  options: SegmentedOption[];\n  label: string;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  className?: string;\n};\n\nexport function SegmentedControl({\n  options,\n  label,\n  value,\n  defaultValue,\n  onValueChange,\n  className = \"\",\n}: SegmentedControlProps) {\n  const count = Math.max(1, options.length);\n  const template = `repeat(${count}, minmax(0, 1fr))`;\n\n  const [internal, setInternal] = useState(\n    () => defaultValue ?? options[0]?.value ?? \"\",\n  );\n  const [hovered, setHovered] = useState(-1);\n\n  const controlled = value !== undefined;\n  const current = controlled ? value : internal;\n  const found = options.findIndex((o) => o.value === current);\n  const index = found < 0 ? 0 : found;\n\n  const buttons = useRef<(HTMLButtonElement | null)[]>([]);\n  const emit = useRef(onValueChange);\n  emit.current = onValueChange;\n\n  const reduced = useReducedMotion();\n  const pos = useMotionValue(index);\n  const thumbX = useTransform(pos, (v) => `${v * 100}%`);\n  const maskX = useTransform(pos, (v) => `${v * -100}%`);\n\n  useEffect(() => {\n    if (reduced) {\n      pos.set(index);\n      return;\n    }\n    const controls = animate(pos, index, CELL);\n    return () => controls.stop();\n  }, [index, reduced, pos]);\n\n  const select = useCallback(\n    (next: string) => {\n      if (!controlled) setInternal(next);\n      if (next !== current) emit.current?.(next);\n    },\n    [controlled, current],\n  );\n\n  const seek = useCallback(\n    (from: number, dir: number) => {\n      let i = from;\n      for (let k = 0; k < count; k++) {\n        i = (i + dir + count) % count;\n        if (!options[i]?.disabled) return i;\n      }\n      return from;\n    },\n    [count, options],\n  );\n\n  const go = useCallback(\n    (i: number) => {\n      const option = options[i];\n      if (!option || option.disabled) return;\n      buttons.current[i]?.focus();\n      select(option.value);\n    },\n    [options, select],\n  );\n\n  const onKeyDown = (e: React.KeyboardEvent, i: number) => {\n    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") {\n      e.preventDefault();\n      go(seek(i, 1));\n    } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") {\n      e.preventDefault();\n      go(seek(i, -1));\n    } else if (e.key === \"Home\") {\n      e.preventDefault();\n      go(seek(count - 1, 1));\n    } else if (e.key === \"End\") {\n      e.preventDefault();\n      go(seek(0, -1));\n    }\n  };\n\n  return (\n    <div\n      role=\"radiogroup\"\n      aria-label={label}\n      className={`relative inline-block select-none rounded-[9px] border border-stone-200 bg-stone-100/70 p-0.5 shadow-[inset_0_1px_2px_rgba(28,25,23,0.07)] dark:border-white/[0.16] dark:bg-[#1D1D1A] dark:shadow-[inset_0_1px_2px_rgba(0,0,0,0.45)] ${className}`}\n    >\n      <div\n        className=\"relative grid\"\n        style={{ gridTemplateColumns: template, touchAction: \"manipulation\" }}\n      >\n        {options.map((option, i) => (\n          <span\n            key={option.value}\n            aria-hidden\n            className={`${SEG} pointer-events-none ${\n              option.disabled\n                ? \"text-stone-300 dark:text-stone-600\"\n                : hovered === i && i !== index\n                  ? \"text-stone-700 dark:text-stone-200\"\n                  : \"text-stone-500 dark:text-stone-400\"\n            }`}\n          >\n            {option.label}\n          </span>\n        ))}\n\n        <motion.div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-y-0 left-0 overflow-hidden rounded-[6px] bg-stone-800 shadow-[0_1px_2px_rgba(28,25,23,0.28)] dark:bg-stone-100 dark:shadow-[0_1px_2px_rgba(0,0,0,0.5)]\"\n          style={{ width: `${100 / count}%`, x: thumbX }}\n          initial={false}\n        >\n          <motion.div\n            className=\"absolute inset-0\"\n            style={{ x: maskX }}\n            initial={false}\n          >\n            <div\n              className=\"absolute inset-y-0 left-0 grid\"\n              style={{ width: `${count * 100}%`, gridTemplateColumns: template }}\n            >\n              {options.map((option) => (\n                <span\n                  key={option.value}\n                  className={`${SEG} text-stone-50 dark:text-stone-900`}\n                >\n                  {option.label}\n                </span>\n              ))}\n            </div>\n          </motion.div>\n        </motion.div>\n        <div\n          className=\"absolute inset-0 grid\"\n          style={{ gridTemplateColumns: template }}\n          onPointerLeave={() => setHovered(-1)}\n        >\n          {options.map((option, i) => (\n            <button\n              key={option.value}\n              ref={(node) => {\n                buttons.current[i] = node;\n              }}\n              type=\"button\"\n              role=\"radio\"\n              aria-checked={i === index}\n              aria-disabled={option.disabled || undefined}\n              tabIndex={i === index ? 0 : -1}\n              onClick={() => !option.disabled && select(option.value)}\n              onKeyDown={(e) => onKeyDown(e, i)}\n              onPointerEnter={() => !option.disabled && setHovered(i)}\n              className=\"cursor-default rounded-[6px] outline-none focus-visible:bg-[#4568FF]/[0.06] focus-visible:shadow-[inset_0_0_0_1px_#4568FF] dark:focus-visible:bg-[#93B0FF]/[0.08] dark:focus-visible:shadow-[inset_0_0_0_1px_#93B0FF]\"\n            >\n              <span className=\"sr-only\">{option.label}</span>\n            </button>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}
