{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "segmented-control",
  "title": "SegmentedControl",
  "description": "Exposure-sheet cell selector with mix-blend-mode: difference indicator and stepped 12fps motion.",
  "registryDependencies": [
    "https://frame12.dev/r/frame12-styles.json"
  ],
  "files": [
    {
      "path": "ui/segmented-control.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nexport interface SegmentedOption<T extends string | number = string | number> {\n  value: T;\n  label: string;\n  unit?: string;\n}\n\nexport interface SegmentedControlProps<T extends string | number = string | number> {\n  options: Array<SegmentedOption<T>>;\n  value: T;\n  onChange: (value: T) => void;\n  ariaLabel?: string;\n  idPrefix?: string;\n  className?: string;\n}\n\nconst useIsoLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;\n\nexport function SegmentedControl<T extends string | number = string | number>({\n  options,\n  value,\n  onChange,\n  ariaLabel = 'Options',\n  idPrefix,\n  className = '',\n}: SegmentedControlProps<T>) {\n  const autoId = React.useId().replace(/:/g, '');\n  const resolvedPrefix = idPrefix ?? `seg-${autoId}`;\n  const wrap = React.useRef<HTMLDivElement | null>(null);\n  const [ind, setInd] = React.useState<{ x: number; w: number } | null>(null);\n  // The indicator only animates once it has been painted. Values restored while the page starts\n  // up (a saved theme, filter, or tab applied right after hydration) snap into place instead of\n  // sliding across from the server-rendered default; later changes animate as usual.\n  const [settled, setSettled] = React.useState(false);\n  React.useEffect(() => {\n    if (!ind || settled) return;\n    let inner = 0;\n    const outer = requestAnimationFrame(() => {\n      inner = requestAnimationFrame(() => setSettled(true));\n    });\n    return () => {\n      cancelAnimationFrame(outer);\n      cancelAnimationFrame(inner);\n    };\n  }, [ind, settled]);\n  const [focusedValue, setFocusedValue] = React.useState<T>(value);\n\n  React.useEffect(() => {\n    if (typeof document !== 'undefined' && wrap.current?.contains(document.activeElement)) {\n      return;\n    }\n    setFocusedValue(value);\n  }, [value]);\n\n  const activeFocusValue = options.some((o) => o.value === focusedValue)\n    ? focusedValue\n    : (options.find((o) => o.value === value)?.value ?? options[0]?.value);\n\n  useIsoLayoutEffect(() => {\n    const el = wrap.current?.querySelector<HTMLElement>('[aria-pressed=\"true\"]');\n    if (!el) return;\n    const x = el.offsetLeft + el.clientLeft;\n    const w = el.clientWidth;\n    setInd((s) => (s && s.x === x && s.w === w ? s : { x, w }));\n  });\n\n  // Re-measure when fonts finish loading or the control resizes.\n  const [, bump] = React.useState(0);\n  React.useEffect(() => {\n    const el = wrap.current;\n    if (!el || typeof ResizeObserver === 'undefined') return;\n    const ro = new ResizeObserver(() => bump((n) => n + 1));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, []);\n\n  return (\n    <div\n      className={`f12-rng${className ? ` ${className}` : ''}`}\n      ref={wrap}\n      role=\"group\"\n      aria-label={ariaLabel}\n      onBlur={(e) => {\n        if (!wrap.current?.contains(e.relatedTarget as Node | null)) {\n          setFocusedValue(value);\n        }\n      }}\n    >\n      {ind && (\n        <span\n          className=\"f12-rng-ind\"\n          aria-hidden=\"true\"\n          style={{\n            transform: `translateX(${ind.x}px)`,\n            width: ind.w,\n            transition: settled ? undefined : 'none',\n          }}\n        />\n      )}\n      {options.map((opt, idx) => (\n        <button\n          key={String(opt.value)}\n          id={`${resolvedPrefix}-${opt.value}`}\n          type=\"button\"\n          className=\"f12-rng-btn\"\n          aria-pressed={value === opt.value}\n          aria-keyshortcuts=\"ArrowLeft ArrowRight ArrowUp ArrowDown Home End\"\n          tabIndex={opt.value === activeFocusValue ? 0 : -1}\n          onFocus={() => setFocusedValue(opt.value)}\n          onClick={() => onChange(opt.value)}\n          onKeyDown={(e) => {\n            if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || options.length === 0) return;\n            let nextIdx = -1;\n            if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n              nextIdx = (idx + 1) % options.length;\n            } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n              nextIdx = (idx - 1 + options.length) % options.length;\n            } else if (e.key === 'Home') {\n              nextIdx = 0;\n            } else if (e.key === 'End') {\n              nextIdx = options.length - 1;\n            }\n            if (nextIdx !== -1) {\n              e.preventDefault();\n              const nextOpt = options[nextIdx];\n              setFocusedValue(nextOpt.value);\n              const buttons = wrap.current?.querySelectorAll<HTMLButtonElement>('.f12-rng-btn');\n              buttons?.[nextIdx]?.focus();\n            }\n          }}\n        >\n          <span className=\"f12-rng-n\">{opt.label}</span>\n          {opt.unit && <span className=\"f12-rng-u\">{opt.unit}</span>}\n        </button>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/frame12/segmented-control.tsx"
    }
  ],
  "type": "registry:ui"
}