{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"lib-use-indicator","type":"registry:lib","title":"use-indicator helpers","description":"Shared by the apps (the website and the dashboards): sliding highlights, underlines and markers.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/dashral/lib/use-indicator.ts","type":"registry:file","target":"components/dashral/lib/use-indicator.ts","content":"\"use client\"\n\nimport { useLayoutEffect, useRef, useState } from \"react\"\n\nexport type IndicatorRect = { x: number; y: number; width: number; height: number }\n\n// Shared by the apps (the website and the dashboards): sliding highlights, underlines and markers.\n\n/**\n * Where the active element (`selector`, e.g. \"[aria-current=page]\") sits inside the container, so an indicator can\n * slide to it. Re-measures when the container resizes or anything inside it changes (a new page, a toggled option).\n * Null when nothing matches.\n */\nexport function useIndicator<T extends HTMLElement>(selector: string) {\n  const ref = useRef<T>(null)\n  const [rect, setRect] = useState<IndicatorRect | null>(null)\n\n  useLayoutEffect(() => {\n    const container = ref.current\n    if (!container) return\n    const measure = () => {\n      // The first match that shows: not hidden (display: none) and not in a closed section (inert).\n      const active = [...container.querySelectorAll<HTMLElement>(selector)].find((el) => el.offsetParent !== null && !el.closest(\"[inert]\"))\n      if (!active) return setRect(null)\n      const box = container.getBoundingClientRect()\n      const item = active.getBoundingClientRect()\n      // Plus the container's scroll, for indicators inside something that scrolls (a long menu), and minus its border:\n      // the indicator is placed from inside the border (top-0 left-0), the boxes are measured from outside it.\n      const next = {\n        x: item.left - box.left + container.scrollLeft - container.clientLeft,\n        y: item.top - box.top + container.scrollTop - container.clientTop,\n        width: item.width,\n        height: item.height,\n      }\n      setRect((current) =>\n        current && current.x === next.x && current.y === next.y && current.width === next.width && current.height === next.height ? current : next\n      )\n    }\n    measure()\n    const resize = new ResizeObserver(measure)\n    resize.observe(container)\n    const changes = new MutationObserver(measure)\n    changes.observe(container, { subtree: true, childList: true, attributes: true, attributeFilter: [\"aria-current\", \"aria-pressed\", \"aria-checked\", \"data-state\", \"data-nav-current\", \"hidden\", \"inert\"] })\n    return () => {\n      resize.disconnect()\n      changes.disconnect()\n    }\n  }, [selector])\n\n  return [ref, rect] as const\n}\n\n/**\n * Classes for the sliding indicator itself: it glides to its new place (--duration-considered, --ease-standard: off\n * quickly, then a long, soft stop, no bounce), or just moves with reduced motion. Every sliding highlight uses it.\n */\nexport const indicatorMotion =\n  \"pointer-events-none absolute top-0 left-0 transition-[translate,width,height] duration-(--duration-considered) ease-standard motion-reduce:transition-none\"\n"}]}