{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-indicator",
  "title": "Indicator",
  "description": "The shared-layout highlight primitive: render <Indicator> inside whichever item is active and it glides there on the snappy spring, with corners kept round while it stretches. Used by Segmented, Tabs, Toggle Group and Radio Cards.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/indicator/indicator.tsx",
      "content": "\"use client\";\n\nimport {\n  LayoutGroup,\n  motion,\n  useMotionValue,\n  type HTMLMotionProps,\n  type Transition,\n} from \"motion/react\";\nimport { createContext, use, useId, useLayoutEffect, useMemo, useRef, type ReactNode } from \"react\";\nimport { instant, snappy, useSafeReducedMotion } from \"../../lib/motion\";\n\ntype IndicatorContextValue = { id: string; transition: Transition };\n\nconst IndicatorContext = createContext<IndicatorContextValue | null>(null);\n\nexport type IndicatorGroupProps = {\n  children: ReactNode;\n  /**\n   * The spring every indicator in the group travels on. Default `snappy` (indicators are\n   * feedback for a choice). Reduced motion always jumps, whatever you pass.\n   */\n  transition?: Transition;\n};\n\n/**\n * Scopes a set of indicators. Every `<Indicator>` with the same `name` inside one group is\n * the same visual element: render it inside whichever item is active and it travels there.\n * Groups never collide, so two segmented controls on a page move independently.\n */\nexport function IndicatorGroup({ children, transition = snappy }: IndicatorGroupProps) {\n  const id = useId();\n  const reduced = useSafeReducedMotion();\n  const value = useMemo(() => ({ id, transition: reduced ? instant : transition }), [id, reduced, transition]);\n  return (\n    <LayoutGroup id={id}>\n      <IndicatorContext value={value}>{children}</IndicatorContext>\n    </LayoutGroup>\n  );\n}\n\nexport type IndicatorProps = Omit<HTMLMotionProps<\"span\">, \"layoutId\" | \"layout\"> & {\n  /**\n   * Indicators with the same name are one element. Use two names for two layers that move\n   * independently, such as a selected thumb and a hover highlight. Default \"indicator\".\n   */\n  name?: string;\n  /**\n   * The corner radius, e.g. `\"var(--eased-radius-control-sm)\"`. Default: whatever your classes\n   * resolve to, re-read whenever the indicator resizes. Pass it when the radius can change\n   * without a resize (a theme that retunes `--eased-radius-*`).\n   */\n  radius?: string;\n};\n\nconst CORNERS = [\"borderTopLeftRadius\", \"borderTopRightRadius\", \"borderBottomRightRadius\", \"borderBottomLeftRadius\"] as const;\n\n/**\n * The shared-layout highlight. Render it, absolutely positioned, inside the active item\n * (the item needs `position: relative` and should draw its label above it); when another\n * item becomes active, render it there instead and it glides across.\n *\n * Interruptible: a new target mid-flight redirects from wherever the highlight is.\n * The corners stay round while it stretches between items of different widths: the\n * radius your classes resolve to (or `radius`) is handed to Motion's scale correction, and\n * re-read when the indicator resizes, so a responsive radius class stays correct.\n * Decorative: `aria-hidden` and `pointer-events: none`; the item carries the state.\n */\nexport function Indicator({ name = \"indicator\", radius, className = \"\", transition, style, ...props }: IndicatorProps) {\n  const group = use(IndicatorContext);\n  if (!group) throw new Error(\"<Indicator> must be rendered inside an <IndicatorGroup>.\");\n  const ref = useRef<HTMLSpanElement>(null);\n  const tl = useMotionValue(\"\");\n  const tr = useMotionValue(\"\");\n  const br = useMotionValue(\"\");\n  const bl = useMotionValue(\"\");\n\n  // Before the first frame of a layout animation: corners set in `style` are scale-corrected.\n  // To re-read the class radius, drop the inline corners for the read, then hand them back.\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const corners = [tl, tr, br, bl];\n    const read = () => {\n      if (radius !== undefined) return corners.forEach((corner) => corner.set(radius));\n      CORNERS.forEach((k) => (el.style[k] = \"\"));\n      const computed = getComputedStyle(el);\n      corners.forEach((corner, i) => {\n        const value = computed[CORNERS[i]];\n        el.style[CORNERS[i]] = value;\n        corner.set(value);\n      });\n    };\n    read();\n    const ro = new ResizeObserver(read);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [radius, tl, tr, br, bl]);\n\n  return (\n    <motion.span\n      ref={ref}\n      aria-hidden\n      layoutId={`${group.id}-${name}`}\n      // One highlight moving, not two cross-fading: the old one hands over at full opacity.\n      layoutCrossfade={false}\n      transition={group.transition === instant ? instant : (transition ?? group.transition)}\n      className={`pointer-events-none absolute ${className}`}\n      style={{ borderTopLeftRadius: tl, borderTopRightRadius: tr, borderBottomRightRadius: br, borderBottomLeftRadius: bl, ...style }}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/_primitives/indicator/indicator.tsx"
    }
  ],
  "type": "registry:ui"
}