{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-sliding-indicator",
  "type": "registry:hook",
  "title": "useSlidingIndicator",
  "description": "Measures the active item in a tab-like container so one element can animate between items instead of the highlight jumping.",
  "categories": [
    "hooks"
  ],
  "files": [
    {
      "path": "hooks/use-sliding-indicator.ts",
      "type": "registry:hook",
      "target": "hooks/use-sliding-indicator.ts",
      "content": "import * as React from 'react'\n\n/** Where a sliding indicator should sit, along one axis, in container pixels. */\nexport interface SlidingIndicator {\n  /** Distance from the container's start edge (left for `x`, top for `y`). */\n  offset: number\n  /** Length along the axis (width for `x`, height for `y`). */\n  size: number\n  /**\n   * The container's own length along the axis, in the same coordinate space as\n   * `offset`. Lets a caller position by BOTH edges (`left` + `right`) instead\n   * of `left` + `width`, which is what allows each edge its own easing -- see\n   * `SegmentedControl`'s pill and `.wj-toggle-knob`.\n   */\n  extent: number\n}\n\nexport interface SlidingIndicatorOptions {\n  /** Axis the indicator travels along. */\n  axis?: 'x' | 'y'\n  /** Attribute carrying each item's id on its element. */\n  attribute?: string\n  /**\n   * Re-measure when this changes. It has to capture item ORDER, not just\n   * count: a reorder that keeps the same items and the same active one moves\n   * the active element without changing `activeId` or the container's size, so\n   * nothing else here would fire. Pass the ids joined in order.\n   */\n  changeKey?: unknown\n}\n\n/**\n * Measures the active item inside a container so a single element can animate\n * between items instead of the highlight jumping.\n *\n * Returns null until a measurement lands, so a caller renders no indicator on\n * the first paint rather than one at zero.\n *\n * Shared by `TabBar` (underline), `SegmentedControl` (fill pill) and\n * `SidebarTabs` (highlight) -- the three had no reason to travel at different\n * speeds or fail differently on the edge case below.\n *\n * Offsets are physical (`offsetLeft`/`offsetTop`), so a caller positions with\n * physical `left`/`top` and both directions agree without an RTL branch.\n */\nexport function useSlidingIndicator(\n  containerRef: React.RefObject<HTMLElement | null>,\n  activeId: string | null | undefined,\n  { axis = 'x', attribute = 'data-tab-id', changeKey }: SlidingIndicatorOptions = {},\n): SlidingIndicator | null {\n  const [indicator, setIndicator] = React.useState<SlidingIndicator | null>(null)\n\n  const measure = React.useCallback(() => {\n    const container = containerRef.current\n    if (!container || !activeId) {\n      setIndicator(null)\n      return\n    }\n    const el = container.querySelector<HTMLElement>(`[${attribute}=\"${activeId}\"]`)\n    // A hidden container (display:none at another breakpoint -- the sidebar\n    // layout taking over from the tab bar) measures 0. Keep the last good\n    // numbers instead of collapsing, so the indicator comes back intact; the\n    // ResizeObserver re-measures on the 0 -> visible transition.\n    const size = axis === 'x' ? el?.offsetWidth : el?.offsetHeight\n    if (!el || !size) return\n    setIndicator({\n      offset: axis === 'x' ? el.offsetLeft : el.offsetTop,\n      size,\n      // clientWidth/Height is the padding box, the same box an absolutely\n      // positioned child's `left`/`right` resolve against.\n      extent: axis === 'x' ? container.clientWidth : container.clientHeight,\n    })\n  }, [containerRef, activeId, axis, attribute])\n\n  React.useEffect(() => {\n    measure()\n  }, [measure, changeKey])\n\n  React.useEffect(() => {\n    const el = containerRef.current\n    if (!el || typeof ResizeObserver === 'undefined') return\n    const ro = new ResizeObserver(() => measure())\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [containerRef, measure])\n\n  return indicator\n}\n"
    }
  ],
  "docs": "Shared by TabBar (underline), SegmentedControl (fill pill) and SidebarTabs (highlight), so the three cannot travel at different speeds. Pass axis 'y' for a vertical list. Returns null until a measurement lands, so render no indicator on the first paint rather than one at zero; a hidden container measures 0 and keeps its last good numbers instead of collapsing.",
  "meta": {
    "group": "hooks",
    "related": [
      "tab-bar",
      "segmented-control",
      "sidebar-tabs"
    ],
    "exports": [
      "useSlidingIndicator"
    ],
    "siteSlug": "use-sliding-indicator"
  }
}
