{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-spy",
  "title": "ScrollSpy",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/scroll-spy/scroll-spy.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type ScrollSpyTurn = 'straight' | 'sharp' | 'rounded'\nexport type ScrollSpyVariant =\n  | 'default'\n  | 'line'\n  | 'angle'\n  | 'sharp'\n  | 'rounded'\n  | 'stepper'\n  | 'scrollspy'\n  | 'tabs'\n  | 'pills'\nexport type ScrollSpyIndicatorMode = 'segment' | 'fill' | 'progress' | 'pill' | 'dot' | 'line'\nexport type ScrollSpyPosition = 'right' | 'left' | 'top' | 'bottom'\nexport type ScrollSpyRailPosition = 'left' | 'right'\n\nexport interface ScrollSpyItem {\n  href: string\n  title: string\n  depth?: number\n  children?: ScrollSpyItem[]\n}\n\nexport interface RegisteredItem {\n  value: string\n  depth: number\n  el: HTMLElement | null\n  title?: string\n  parentValue?: string | null\n}\n\nfunction flattenItems(rawItems: ScrollSpyItem[]): RegisteredItem[] {\n  const result: RegisteredItem[] = []\n  function walk(list: ScrollSpyItem[], depth = 1, parentVal: string | null = null) {\n    for (const it of list) {\n      result.push({\n        value: it.href,\n        depth: it.depth ?? depth,\n        el: null,\n        title: it.title,\n        parentValue: parentVal,\n      })\n      if (it.children && it.children.length > 0) {\n        walk(it.children, depth + 1, it.href)\n      }\n    }\n  }\n  walk(rawItems)\n  return result\n}\n\nexport type ScrollSpyLineWidth = 'thin' | 'default' | 'thick' | number\nexport type ScrollSpyColor = 'primary' | 'foreground' | 'destructive' | 'secondary' | (string & {})\n\nexport interface HandleColorResolved {\n  bgClass: string\n  strokeClass: string\n  borderClass: string\n  customColor?: string\n}\n\nexport function resolveScrollSpyColor(color: ScrollSpyColor = 'primary'): HandleColorResolved {\n  if (color === 'primary')\n    return {\n      bgClass: 'bg-primary',\n      strokeClass: 'stroke-primary',\n      borderClass: 'border-primary',\n      customColor: undefined,\n    }\n  if (color === 'foreground')\n    return {\n      bgClass: 'bg-foreground',\n      strokeClass: 'stroke-foreground',\n      borderClass: 'border-foreground',\n      customColor: undefined,\n    }\n  if (color === 'destructive')\n    return {\n      bgClass: 'bg-destructive',\n      strokeClass: 'stroke-destructive',\n      borderClass: 'border-destructive',\n      customColor: undefined,\n    }\n  if (color === 'secondary')\n    return {\n      bgClass: 'bg-secondary',\n      strokeClass: 'stroke-secondary',\n      borderClass: 'border-secondary',\n      customColor: undefined,\n    }\n  if (color.startsWith('bg-')) {\n    const raw = color.replace(/^bg-/, '')\n    return { bgClass: color, strokeClass: `stroke-${raw}`, borderClass: `border-${raw}`, customColor: undefined }\n  }\n  if (\n    color.startsWith('#') ||\n    color.startsWith('oklch') ||\n    color.startsWith('rgb') ||\n    color.startsWith('hsl') ||\n    color.startsWith('var(')\n  ) {\n    return { bgClass: '', strokeClass: '', borderClass: '', customColor: color }\n  }\n  return {\n    bgClass: `bg-${color}`,\n    strokeClass: `stroke-${color}`,\n    borderClass: `border-${color}`,\n    customColor: undefined,\n  }\n}\n\ninterface ScrollSpyContextValue {\n  activeValue: string\n  setActiveValue: (value: string) => void\n  scrollProgress: number\n  registerItem: (item: RegisteredItem) => void\n  unregisterItem: (value: string) => void\n  getItems: () => RegisteredItem[]\n  variant: ScrollSpyVariant\n  turn: ScrollSpyTurn\n  indicator: ScrollSpyIndicatorMode\n  keepScrolled: boolean\n  highlightParent: boolean\n  lineWidth: ScrollSpyLineWidth\n  resolvedLineWidth: number\n  color: ScrollSpyColor\n  position: ScrollSpyPosition\n  railPosition: ScrollSpyRailPosition\n  scrollContainerRef: React.MutableRefObject<HTMLElement | Window | null>\n  offsetTopRef: React.MutableRefObject<number>\n  getListEl: () => HTMLElement | null\n  setListEl: (el: HTMLElement | null) => void\n  subscribe: (listener: () => void) => () => void\n  scrollToHref: (href: string) => void\n  goToPrev: () => void\n  goToNext: () => void\n  isItemActive: (value: string) => boolean\n  isItemParentActive: (value: string) => boolean\n  isItemScrolled: (value: string) => boolean\n}\n\nconst ScrollSpyContext = React.createContext<ScrollSpyContextValue | null>(null)\nconst ScrollSpyItemDepthContext = React.createContext<number>(0)\n\nexport interface ScrollSpyProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'onProgress'> {\n  title?: string\n  items?: ScrollSpyItem[]\n  value?: string\n  defaultValue?: string\n  offsetTop?: number\n  bounds?: number\n  scrollContainer?: HTMLElement | string | null\n  affix?: boolean\n  variant?: ScrollSpyVariant\n  turn?: ScrollSpyTurn\n  indicator?: ScrollSpyIndicatorMode\n  keepScrolled?: boolean\n  highlightParent?: boolean\n  lineWidth?: ScrollSpyLineWidth\n  color?: ScrollSpyColor\n  position?: ScrollSpyPosition\n  railPosition?: ScrollSpyRailPosition\n  onChange?: (value: string) => void\n  onProgress?: (progress: number) => void\n}\n\ninterface Marker {\n  value: string\n  depth: number\n  x: number\n  y: number\n  top: number\n  bottom: number\n}\n\nfunction depthX(relDepth: number, isRightRail: boolean, listWidth: number): number {\n  if (!isRightRail) {\n    if (relDepth <= 1) return 1\n    if (relDepth === 2) return 13\n    if (relDepth === 3) return 21\n    return 21 + (relDepth - 3) * 8\n  }\n  const base = listWidth - 1\n  if (relDepth <= 1) return base\n  if (relDepth === 2) return base - 12\n  if (relDepth === 3) return base - 20\n  return base - 20 - (relDepth - 3) * 8\n}\n\nfunction buildCircuitPath(markers: Marker[], endIndex: number, rounded: boolean, edge: 'top' | 'bottom'): string {\n  if (markers.length === 0 || endIndex < 0) return ''\n\n  const end = Math.min(endIndex, markers.length - 1)\n  const first = markers[0]!\n  const parts: string[] = [`M ${first.x} ${first.top}`]\n\n  for (let i = 0; i <= end; i++) {\n    const curr = markers[i]!\n\n    if (i === end) {\n      const targetY = edge === 'top' ? curr.top : curr.bottom\n      parts.push(`L ${curr.x} ${targetY}`)\n      break\n    }\n\n    const next = markers[i + 1]\n    if (!next) {\n      parts.push(`L ${curr.x} ${curr.bottom}`)\n      break\n    }\n\n    // Always draw down the full height of curr at curr.x first\n    parts.push(`L ${curr.x} ${curr.bottom}`)\n\n    if (curr.x === next.x) {\n      parts.push(`L ${curr.x} ${next.top}`)\n      continue\n    }\n\n    // Smooth monotonic depth transition strictly bounded within [curr.bottom, next.top]\n    const gap = Math.max(0, next.top - curr.bottom)\n    const absDx = Math.abs(next.x - curr.x)\n    const transitionH = Math.min(gap, absDx)\n\n    if (transitionH <= 1) {\n      parts.push(`L ${next.x} ${next.top}`)\n      continue\n    }\n\n    const y1 = curr.bottom + (gap - transitionH) / 2\n    let y2 = y1 + transitionH\n    if (next.top - y2 <= 0.5) {\n      y2 = next.top\n    }\n\n    // 1. Straight rail down to y1 at curr.x\n    if (y1 - curr.bottom > 0.5) {\n      parts.push(`L ${curr.x} ${y1}`)\n    }\n\n    // 2. Transition from (curr.x, y1) to (next.x, y2)\n    if (rounded) {\n      const midY = (y1 + y2) / 2\n      parts.push(`C ${curr.x} ${midY}, ${next.x} ${midY}, ${next.x} ${y2}`)\n    } else {\n      parts.push(`L ${next.x} ${y2}`)\n    }\n\n    // 3. Connect to next.top if next.top > y2\n    if (next.top - y2 > 0.5) {\n      parts.push(`L ${next.x} ${next.top}`)\n    }\n  }\n\n  return parts.join(' ')\n}\n\nexport const ScrollSpyRoot = React.forwardRef<HTMLElement, ScrollSpyProps>(\n  (\n    {\n      className,\n      title,\n      items = [],\n      value,\n      defaultValue,\n      offsetTop = 0,\n      bounds = 5,\n      scrollContainer = null,\n      affix = false,\n      variant,\n      turn,\n      indicator = 'line',\n      keepScrolled = false,\n      highlightParent = true,\n      lineWidth = 'default',\n      color = 'primary',\n      position = 'right',\n      railPosition,\n      onChange,\n      onProgress,\n      style,\n      children,\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const resolvedPosition: ScrollSpyPosition = position ?? 'right'\n    const resolvedRailPosition: ScrollSpyRailPosition = railPosition ?? (position === 'left' ? 'right' : 'left')\n\n    const resolvedLineWidth = React.useMemo(() => {\n      if (typeof lineWidth === 'number') return Math.max(1, lineWidth)\n      if (lineWidth === 'thin') return 1.5\n      if (lineWidth === 'thick') return 3.5\n      return 2.5\n    }, [lineWidth])\n\n    const resolvedTurn: ScrollSpyTurn =\n      turn ?? (variant === 'angle' || variant === 'rounded' ? 'rounded' : variant === 'sharp' ? 'sharp' : 'straight')\n    const resolvedVariant: ScrollSpyVariant =\n      variant && variant !== 'default'\n        ? variant\n        : turn === 'sharp'\n          ? 'angle'\n          : turn === 'rounded'\n            ? 'rounded'\n            : position === 'top' || position === 'bottom'\n              ? 'stepper'\n              : 'line'\n    const resolvedIndicator: ScrollSpyIndicatorMode = indicator ?? 'line'\n\n    const [internalActive, setInternalActive] = React.useState<string>(defaultValue || (items[0]?.href ?? ''))\n    const activeValue = value !== undefined ? value : internalActive\n    const activeValueRef = React.useRef(activeValue)\n    activeValueRef.current = activeValue\n\n    const registeredItemsMap = React.useRef<Map<string, RegisteredItem>>(\n      new Map(items && items.length > 0 ? flattenItems(items).map((it) => [it.value, it]) : []),\n    )\n    const listenersRef = React.useRef<Set<() => void>>(new Set())\n\n    const notifyListeners = React.useCallback(() => {\n      for (const listener of listenersRef.current) {\n        listener()\n      }\n    }, [])\n\n    React.useEffect(() => {\n      if (items && items.length > 0) {\n        const flattened = flattenItems(items)\n        for (const item of flattened) {\n          const existing = registeredItemsMap.current.get(item.value)\n          registeredItemsMap.current.set(item.value, {\n            ...item,\n            el: existing?.el ?? null,\n            title: item.title ?? existing?.title,\n          })\n        }\n        notifyListeners()\n        if (!activeValueRef.current && flattened.length > 0) {\n          activeValueRef.current = flattened[0]!.value\n          setInternalActive(flattened[0]!.value)\n        }\n      }\n    }, [items, notifyListeners])\n\n    const subscribe = React.useCallback((listener: () => void) => {\n      listenersRef.current.add(listener)\n      return () => {\n        listenersRef.current.delete(listener)\n      }\n    }, [])\n\n    const registerItem = React.useCallback(\n      (item: RegisteredItem) => {\n        const existing = registeredItemsMap.current.get(item.value)\n        registeredItemsMap.current.set(item.value, {\n          ...existing,\n          ...item,\n          title: item.title ?? existing?.title,\n        })\n        notifyListeners()\n        if (!activeValueRef.current) {\n          activeValueRef.current = item.value\n          setInternalActive(item.value)\n        }\n      },\n      [notifyListeners],\n    )\n\n    const unregisterItem = React.useCallback(\n      (val: string) => {\n        registeredItemsMap.current.delete(val)\n        notifyListeners()\n      },\n      [notifyListeners],\n    )\n\n    const getItems = React.useCallback(() => Array.from(registeredItemsMap.current.values()), [])\n\n    const [readingProgress, setReadingProgress] = React.useState<number>(0)\n    const readingProgressRef = React.useRef(0)\n    readingProgressRef.current = readingProgress\n\n    const listElRef = React.useRef<HTMLElement | null>(null)\n    const scrollContainerRef = React.useRef<HTMLElement | Window | null>(null)\n    const offsetTopRef = React.useRef(offsetTop)\n    offsetTopRef.current = offsetTop\n    const boundsRef = React.useRef(bounds)\n    boundsRef.current = bounds\n    const onChangeRef = React.useRef(onChange)\n    onChangeRef.current = onChange\n    const onProgressRef = React.useRef(onProgress)\n    onProgressRef.current = onProgress\n\n    const resolveContainer = React.useCallback((): HTMLElement | Window | null => {\n      if (typeof window === 'undefined') return null\n      if (!scrollContainer) return window\n      if (typeof scrollContainer === 'string') {\n        return (document.querySelector(scrollContainer) as HTMLElement) ?? window\n      }\n      return scrollContainer\n    }, [scrollContainer])\n\n    const setActiveValue = React.useCallback((val: string) => {\n      setInternalActive(val)\n      onChangeRef.current?.(val)\n    }, [])\n\n    const scrollToHref = React.useCallback(\n      (href: string) => {\n        setActiveValue(href)\n        const container = scrollContainerRef.current\n        const offset = offsetTopRef.current\n        const smooth = !window.matchMedia('(prefers-reduced-motion: reduce)').matches\n        const behavior: ScrollBehavior = smooth ? 'smooth' : 'auto'\n\n        if (container && container !== window) {\n          const cEl = container as HTMLElement\n          const target = cEl.querySelector(href) as HTMLElement | null\n          if (target) {\n            const cRect = cEl.getBoundingClientRect()\n            const tRect = target.getBoundingClientRect()\n            const top = cEl.scrollTop + (tRect.top - cRect.top) - offset\n            cEl.scrollTo({ top, behavior })\n          }\n        } else {\n          const target = document.querySelector(href) as HTMLElement | null\n          if (target) {\n            const top = window.scrollY + target.getBoundingClientRect().top - offset\n            window.scrollTo({ top, behavior })\n          }\n        }\n\n        if (typeof history !== 'undefined') {\n          history.replaceState(null, '', href)\n        }\n      },\n      [setActiveValue],\n    )\n\n    const goToPrev = React.useCallback(() => {\n      const list = Array.from(registeredItemsMap.current.values())\n      const active = activeValueRef.current\n      const idx = list.findIndex((i) => i.value === active || i.value.replace(/^#/, '') === active.replace(/^#/, ''))\n      if (idx > 0 && list[idx - 1]) {\n        scrollToHref(list[idx - 1]!.value)\n      }\n    }, [scrollToHref])\n\n    const goToNext = React.useCallback(() => {\n      const list = Array.from(registeredItemsMap.current.values())\n      const active = activeValueRef.current\n      const idx = list.findIndex((i) => i.value === active || i.value.replace(/^#/, '') === active.replace(/^#/, ''))\n      if (idx >= 0 && idx < list.length - 1 && list[idx + 1]) {\n        scrollToHref(list[idx + 1]!.value)\n      }\n    }, [scrollToHref])\n\n    const getListEl = React.useCallback(() => listElRef.current, [])\n    const setListEl = React.useCallback((el: HTMLElement | null) => {\n      listElRef.current = el\n    }, [])\n\n    // Scroll spy and progress tracking\n    const recomputeActive = React.useCallback(() => {\n      const container = scrollContainerRef.current\n      const currentItems = Array.from(registeredItemsMap.current.values())\n      if (!container || currentItems.length === 0) return\n\n      const isWin = container === window\n      let currentScroll = 0\n      let maxScroll = 0\n\n      if (isWin) {\n        currentScroll = window.scrollY\n        maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight)\n      } else {\n        const cEl = container as HTMLElement\n        currentScroll = cEl.scrollTop\n        maxScroll = Math.max(1, cEl.scrollHeight - cEl.clientHeight)\n      }\n\n      const prog = Math.min(1, Math.max(0, currentScroll / maxScroll))\n      setReadingProgress(prog)\n      onProgressRef.current?.(prog)\n\n      const containerTop = isWin ? 0 : (container as HTMLElement).getBoundingClientRect().top\n      const triggerThreshold = containerTop + offsetTopRef.current + boundsRef.current + 40\n\n      let currentTarget = ''\n\n      for (const item of currentItems) {\n        const selector = item.value.startsWith('#') ? item.value : `#${item.value}`\n        const target = isWin ? document.querySelector(selector) : (container as HTMLElement).querySelector(selector)\n\n        if (!target) continue\n        const targetRect = target.getBoundingClientRect()\n        if (targetRect.top <= triggerThreshold) {\n          currentTarget = item.value\n        } else if (currentTarget) {\n          break\n        }\n      }\n\n      const atBottom = isWin\n        ? document.documentElement.scrollHeight > window.innerHeight &&\n          window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 6\n        : (container as HTMLElement).scrollHeight > (container as HTMLElement).clientHeight &&\n          (container as HTMLElement).scrollTop + (container as HTMLElement).clientHeight >=\n            (container as HTMLElement).scrollHeight - 6\n\n      if (atBottom && currentItems.length > 0) {\n        currentTarget = currentItems[currentItems.length - 1]!.value\n      }\n\n      if (!currentTarget && currentItems.length > 0) {\n        currentTarget = currentItems[0]!.value\n      }\n\n      if (currentTarget && currentTarget !== activeValueRef.current) {\n        setActiveValue(currentTarget)\n      }\n    }, [setActiveValue])\n\n    React.useEffect(() => {\n      scrollContainerRef.current = resolveContainer()\n      const c = scrollContainerRef.current\n      if (!c) return\n\n      let raf = 0\n      const onScroll = () => {\n        cancelAnimationFrame(raf)\n        raf = requestAnimationFrame(recomputeActive)\n      }\n\n      c.addEventListener('scroll', onScroll, { passive: true })\n      recomputeActive()\n      const t = setTimeout(recomputeActive, 100)\n\n      return () => {\n        cancelAnimationFrame(raf)\n        clearTimeout(t)\n        c.removeEventListener('scroll', onScroll)\n      }\n    }, [resolveContainer, recomputeActive])\n\n    const isItemActive = React.useCallback(\n      (val: string) => {\n        if (!val || !activeValue) return false\n        const cleanVal = val.replace(/^#/, '')\n        const cleanActive = activeValue.replace(/^#/, '')\n        return cleanVal === cleanActive\n      },\n      [activeValue],\n    )\n\n    const isItemParentActive = React.useCallback(\n      (val: string) => {\n        if (!highlightParent || !val || !activeValue) return false\n        const cleanVal = val.replace(/^#/, '')\n        const cleanActive = activeValue.replace(/^#/, '')\n        if (cleanVal === cleanActive) return false\n\n        const itemsList = getItems()\n        const activeItem = itemsList.find((i) => i.value === activeValue || i.value.replace(/^#/, '') === cleanActive)\n        let parent = activeItem?.parentValue\n        while (parent) {\n          if (parent === val || parent.replace(/^#/, '') === cleanVal) return true\n          const pItem = itemsList.find(\n            (i) => i.value === parent || i.value.replace(/^#/, '') === parent!.replace(/^#/, ''),\n          )\n          parent = pItem?.parentValue\n        }\n        return false\n      },\n      [highlightParent, activeValue, getItems],\n    )\n\n    const isItemScrolled = React.useCallback(\n      (val: string) => {\n        if (!keepScrolled) return false\n        const itemsList = getItems()\n        const cleanActive = activeValue.replace(/^#/, '')\n        const activeIdx = itemsList.findIndex(\n          (i) => i.value === activeValue || i.value.replace(/^#/, '') === cleanActive,\n        )\n        if (activeIdx < 0) return false\n        const cleanVal = val.replace(/^#/, '')\n        const idx = itemsList.findIndex((i) => i.value === val || i.value.replace(/^#/, '') === cleanVal)\n        return idx >= 0 && idx <= activeIdx\n      },\n      [keepScrolled, activeValue, getItems],\n    )\n\n    const ctxValue = React.useMemo<ScrollSpyContextValue>(\n      () => ({\n        activeValue,\n        setActiveValue,\n        scrollProgress: readingProgress,\n        registerItem,\n        unregisterItem,\n        getItems,\n        variant: resolvedVariant,\n        turn: resolvedTurn,\n        indicator: resolvedIndicator,\n        keepScrolled,\n        highlightParent,\n        lineWidth,\n        resolvedLineWidth,\n        color,\n        position: resolvedPosition,\n        railPosition: resolvedRailPosition,\n        scrollContainerRef,\n        offsetTopRef,\n        getListEl,\n        setListEl,\n        subscribe,\n        scrollToHref,\n        goToPrev,\n        goToNext,\n        isItemActive,\n        isItemParentActive,\n        isItemScrolled,\n      }),\n      [\n        activeValue,\n        setActiveValue,\n        readingProgress,\n        registerItem,\n        unregisterItem,\n        getItems,\n        resolvedVariant,\n        resolvedTurn,\n        resolvedIndicator,\n        keepScrolled,\n        highlightParent,\n        lineWidth,\n        resolvedLineWidth,\n        color,\n        resolvedPosition,\n        resolvedRailPosition,\n        getListEl,\n        setListEl,\n        subscribe,\n        scrollToHref,\n        goToPrev,\n        goToNext,\n        isItemActive,\n        isItemParentActive,\n        isItemScrolled,\n      ],\n    )\n\n    const isTopOrBottom = resolvedPosition === 'top' || resolvedPosition === 'bottom'\n\n    return (\n      <ScrollSpyContext.Provider value={ctxValue}>\n        <ScrollSpyItemDepthContext.Provider value={0}>\n          <nav\n            ref={forwardedRef}\n            data-uipkge=\"\"\n            data-slot=\"scroll-spy\"\n            data-position={resolvedPosition}\n            data-rail-position={resolvedRailPosition}\n            data-variant={resolvedVariant}\n            data-turn={resolvedTurn}\n            data-indicator={resolvedIndicator}\n            data-keep-scrolled={keepScrolled ? 'true' : undefined}\n            data-highlight-parent={highlightParent ? 'true' : undefined}\n            data-line-width={lineWidth}\n            data-color={color}\n            aria-label=\"Scroll spy navigation\"\n            className={cn(\n              'relative flex text-sm',\n              isTopOrBottom ? 'w-full flex-col' : 'flex-col',\n              affix && (resolvedPosition === 'bottom' ? 'sticky bottom-4' : 'sticky'),\n              className,\n            )}\n            style={affix && resolvedPosition !== 'bottom' ? { top: `${offsetTop}px`, ...style } : style}\n            {...props}\n          >\n            {items && items.length > 0 ? (\n              <>\n                {resolvedPosition === 'top' && <ScrollSpyStepper />}\n                {resolvedPosition !== 'top' && resolvedPosition !== 'bottom' && (\n                  <>\n                    {title && <ScrollSpyTitle>{title}</ScrollSpyTitle>}\n                    <ScrollSpyList>\n                      <ScrollSpyIndicator />\n                      {items.map((item, itemIdx) => {\n                        const prevHadChildren = Boolean(itemIdx > 0 && items[itemIdx - 1]?.children?.length)\n                        return (\n                          <React.Fragment key={item.href}>\n                            <ScrollSpyItem\n                              value={item.href}\n                              depth={item.depth ?? 1}\n                              className={prevHadChildren ? 'mt-2' : undefined}\n                            >\n                              <ScrollSpyLink href={item.href} title={item.title} />\n                            </ScrollSpyItem>\n                            {(item.children ?? []).map((child, childIdx) => {\n                              const prevChildHadChildren = Boolean(\n                                childIdx > 0 && item.children?.[childIdx - 1]?.children?.length,\n                              )\n                              return (\n                                <React.Fragment key={child.href}>\n                                  <ScrollSpyItem\n                                    value={child.href}\n                                    depth={child.depth ?? 2}\n                                    className={childIdx === 0 || prevChildHadChildren ? 'mt-2' : undefined}\n                                  >\n                                    <ScrollSpyLink href={child.href} title={child.title} />\n                                  </ScrollSpyItem>\n                                  {(child.children ?? []).map((grandchild, gIdx) => (\n                                    <ScrollSpyItem\n                                      key={grandchild.href}\n                                      value={grandchild.href}\n                                      depth={grandchild.depth ?? 3}\n                                      className={gIdx === 0 ? 'mt-2' : undefined}\n                                    >\n                                      <ScrollSpyLink href={grandchild.href} title={grandchild.title} />\n                                    </ScrollSpyItem>\n                                  ))}\n                                </React.Fragment>\n                              )\n                            })}\n                          </React.Fragment>\n                        )\n                      })}\n                    </ScrollSpyList>\n                  </>\n                )}\n                {resolvedPosition === 'bottom' && <ScrollSpyStepper />}\n              </>\n            ) : (\n              children\n            )}\n          </nav>\n        </ScrollSpyItemDepthContext.Provider>\n      </ScrollSpyContext.Provider>\n    )\n  },\n)\nScrollSpyRoot.displayName = 'ScrollSpyRoot'\n\nexport interface ScrollSpyTitleProps extends React.HTMLAttributes<HTMLParagraphElement> {}\n\nexport const ScrollSpyTitle = React.forwardRef<HTMLParagraphElement, ScrollSpyTitleProps>(\n  ({ className, children, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    const isRightRail = ctx?.position === 'left' && ctx?.railPosition === 'right'\n\n    return (\n      <p\n        ref={ref}\n        data-slot=\"scroll-spy-title\"\n        className={cn(\n          'text-foreground mb-3 text-sm font-semibold tracking-tight',\n          isRightRail && 'pr-3 text-right',\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </p>\n    )\n  },\n)\nScrollSpyTitle.displayName = 'ScrollSpyTitle'\n\nexport interface ScrollSpyListProps extends React.HTMLAttributes<HTMLUListElement> {}\n\nexport const ScrollSpyList = React.forwardRef<HTMLUListElement, ScrollSpyListProps>(\n  ({ className, children, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    const localRef = React.useRef<HTMLUListElement | null>(null)\n\n    React.useEffect(() => {\n      if (localRef.current && ctx) {\n        ctx.setListEl(localRef.current)\n      }\n    }, [ctx])\n\n    const isStraight = ctx?.turn === 'straight'\n    const isRightRail = ctx?.position === 'left' && ctx?.railPosition === 'right'\n\n    return (\n      <ul\n        ref={(node) => {\n          localRef.current = node\n          if (typeof ref === 'function') ref(node)\n          else if (ref) ref.current = node\n        }}\n        data-slot=\"scroll-spy-list\"\n        style={{\n          borderLeftWidth: isStraight && !isRightRail ? `${ctx?.resolvedLineWidth ?? 2.5}px` : undefined,\n          borderRightWidth: isStraight && isRightRail ? `${ctx?.resolvedLineWidth ?? 2.5}px` : undefined,\n          ...props.style,\n        }}\n        className={cn(\n          'relative flex flex-col space-y-1 text-sm',\n          isStraight && (isRightRail ? 'border-border border-r' : 'border-border border-l'),\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </ul>\n    )\n  },\n)\nScrollSpyList.displayName = 'ScrollSpyList'\n\nexport interface ScrollSpyIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {\n  color?: ScrollSpyColor\n}\n\nexport const ScrollSpyIndicator = React.forwardRef<HTMLDivElement, ScrollSpyIndicatorProps>(\n  ({ className, color, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    if (!ctx) throw new Error('ScrollSpyIndicator must be used inside <ScrollSpy>.')\n\n    const resolvedColor = color ?? ctx.color ?? 'primary'\n    const handleColor = resolveScrollSpyColor(resolvedColor)\n\n    const measurePathRef = React.useRef<SVGPathElement | null>(null)\n    const [trackPath, setTrackPath] = React.useState('')\n    const [pathLength, setPathLength] = React.useState(0)\n    const [activeStart, setActiveStart] = React.useState(0)\n    const [activeEnd, setActiveEnd] = React.useState(0)\n    const [activeMarker, setActiveMarker] = React.useState<Marker | null>(null)\n    const [straightHighlight, setStraightHighlight] = React.useState({ top: 0, height: 0, visible: false })\n    const [canAnimate, setCanAnimate] = React.useState(false)\n\n    const enableAnimation = React.useCallback(() => {\n      if (!canAnimate) {\n        if (typeof requestAnimationFrame !== 'undefined') {\n          requestAnimationFrame(() => {\n            setCanAnimate(true)\n          })\n        } else {\n          setCanAnimate(true)\n        }\n      }\n    }, [canAnimate])\n\n    const measureLength = React.useCallback((pathD: string): number => {\n      const el = measurePathRef.current\n      if (!el || !pathD) return 0\n      el.setAttribute('d', pathD)\n      if (typeof el.getTotalLength === 'function') {\n        try {\n          return el.getTotalLength()\n        } catch {\n          // ignore\n        }\n      }\n      return 100\n    }, [])\n\n    const updateGeometry = React.useCallback(() => {\n      const list = ctx.getListEl()\n      if (!list) return\n      const listRect = list.getBoundingClientRect()\n      const validItems = ctx.getItems().filter((i) => i.el && i.el.isConnected)\n\n      if (validItems.length === 0) {\n        setTrackPath('')\n        setPathLength(0)\n        setActiveStart(0)\n        setActiveEnd(0)\n        setActiveMarker(null)\n        setStraightHighlight({ top: 0, height: 0, visible: false })\n        return\n      }\n\n      const isRightRail = ctx.position === 'left' && ctx.railPosition === 'right'\n      const listWidth = listRect.width || 180\n\n      const minDepth = Math.min(...validItems.map((i) => i.depth))\n      const markers: Marker[] = []\n\n      for (const item of validItems) {\n        if (!item.el) continue\n        const rect = item.el.getBoundingClientRect()\n        const relDepth = Math.max(1, item.depth - minDepth + 1)\n        const top = rect.top - listRect.top\n        const bottom = rect.bottom - listRect.top\n        markers.push({\n          value: item.value,\n          depth: item.depth,\n          x: depthX(relDepth, isRightRail, listWidth),\n          y: top + rect.height / 2,\n          top,\n          bottom,\n        })\n      }\n\n      markers.sort((a, b) => a.y - b.y)\n      if (markers.length === 0) return\n\n      const first = markers[0]!\n      const last = markers[markers.length - 1]!\n      const activeIdx = markers.findIndex(\n        (m) => m.value === ctx.activeValue || m.value.replace(/^#/, '') === ctx.activeValue.replace(/^#/, ''),\n      )\n\n      if (ctx.turn === 'straight') {\n        const railX = isRightRail ? first.x : 1\n        setTrackPath(`M ${railX} ${first.top} L ${railX} ${last.bottom}`)\n        setPathLength(0)\n        setActiveStart(0)\n        setActiveEnd(0)\n\n        if (ctx.indicator === 'progress') {\n          const totalHeight = list.clientHeight || last.bottom - first.top\n          setStraightHighlight({\n            top: first.top,\n            height: Math.max(0, totalHeight * ctx.scrollProgress),\n            visible: true,\n          })\n          setActiveMarker(null)\n          enableAnimation()\n          return\n        }\n\n        if (activeIdx < 0) {\n          setStraightHighlight({ top: 0, height: 0, visible: false })\n          setActiveMarker(null)\n          return\n        }\n\n        const active = markers[activeIdx]!\n        setActiveMarker(active)\n\n        const isFillMode = ctx.indicator === 'fill' || ctx.keepScrolled\n\n        if (isFillMode) {\n          setStraightHighlight({\n            top: first.top,\n            height: Math.max(active.bottom - first.top, 14),\n            visible: true,\n          })\n        } else {\n          setStraightHighlight({\n            top: active.top,\n            height: Math.max(active.bottom - active.top, 14),\n            visible: true,\n          })\n        }\n        enableAnimation()\n        return\n      }\n\n      // Circuit Mode: sharp 45° angle or rounded curves\n      const rounded = ctx.turn === 'rounded'\n      const fullPath = buildCircuitPath(markers, markers.length - 1, rounded, 'bottom')\n      setTrackPath(fullPath)\n      setStraightHighlight({ top: 0, height: 0, visible: false })\n\n      const total = measureLength(fullPath)\n      setPathLength(total)\n\n      if (total === 0) {\n        setActiveStart(0)\n        setActiveEnd(0)\n        setActiveMarker(null)\n        return\n      }\n\n      if (ctx.indicator === 'progress') {\n        setActiveStart(0)\n        setActiveEnd(total * ctx.scrollProgress)\n        setActiveMarker(null)\n        enableAnimation()\n        return\n      }\n\n      if (activeIdx < 0) {\n        setActiveStart(0)\n        setActiveEnd(0)\n        setActiveMarker(null)\n        return\n      }\n\n      const active = markers[activeIdx]!\n      setActiveMarker(active)\n\n      const isFillMode = ctx.indicator === 'fill' || ctx.keepScrolled\n      const startLen = isFillMode ? 0 : measureLength(buildCircuitPath(markers, activeIdx, rounded, 'top'))\n      const endLen = measureLength(buildCircuitPath(markers, activeIdx, rounded, 'bottom'))\n\n      setActiveStart(startLen)\n      setActiveEnd(endLen)\n      enableAnimation()\n    }, [ctx, measureLength, enableAnimation])\n\n    React.useEffect(() => {\n      let raf = 0\n      const schedule = () => {\n        cancelAnimationFrame(raf)\n        raf = requestAnimationFrame(updateGeometry)\n      }\n      schedule()\n      const unsubscribe = ctx.subscribe(schedule)\n      window.addEventListener('resize', schedule)\n      return () => {\n        cancelAnimationFrame(raf)\n        unsubscribe()\n        window.removeEventListener('resize', schedule)\n      }\n    }, [\n      updateGeometry,\n      ctx,\n      ctx.activeValue,\n      ctx.turn,\n      ctx.indicator,\n      ctx.keepScrolled,\n      ctx.resolvedLineWidth,\n      ctx.position,\n      ctx.railPosition,\n      ctx.scrollProgress,\n    ])\n\n    const isLeftWithRightRail = ctx.position === 'left' && ctx.railPosition === 'right'\n\n    return (\n      <div\n        ref={ref}\n        data-slot=\"scroll-spy-indicator\"\n        className={cn('pointer-events-none absolute inset-0 overflow-visible', className)}\n        aria-hidden=\"true\"\n        {...props}\n      >\n        {ctx.turn !== 'straight' && (\n          <svg className=\"absolute inset-0 size-full overflow-visible\" fill=\"none\">\n            <path ref={measurePathRef} className=\"invisible\" fill=\"none\" />\n            <path\n              d={trackPath}\n              className=\"stroke-border\"\n              strokeWidth={ctx.resolvedLineWidth}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            {trackPath && activeEnd > 0 && (\n              <path\n                d={trackPath}\n                className={handleColor.strokeClass}\n                strokeWidth={ctx.resolvedLineWidth}\n                strokeLinecap=\"butt\"\n                strokeLinejoin=\"round\"\n                style={{\n                  stroke: handleColor.customColor,\n                  strokeDasharray: `${Math.max(activeEnd - activeStart, 0)} ${Math.max(pathLength, 1)}`,\n                  strokeDashoffset: -activeStart,\n                  transition:\n                    canAnimate && ctx.indicator !== 'progress'\n                      ? 'stroke-dashoffset 260ms cubic-bezier(0.16, 1, 0.3, 1), stroke-dasharray 260ms cubic-bezier(0.16, 1, 0.3, 1)'\n                      : 'none',\n                }}\n              />\n            )}\n          </svg>\n        )}\n\n        {ctx.turn === 'straight' && straightHighlight.visible && (ctx.indicator !== 'segment' || ctx.keepScrolled) && (\n          <div\n            className={cn('absolute z-10 rounded-none', handleColor.bgClass)}\n            style={{\n              top: `${straightHighlight.top}px`,\n              height: `${straightHighlight.height}px`,\n              width: `${ctx.resolvedLineWidth}px`,\n              backgroundColor: handleColor.customColor,\n              left: isLeftWithRightRail ? undefined : `-${ctx.resolvedLineWidth}px`,\n              right: isLeftWithRightRail ? `-${ctx.resolvedLineWidth}px` : undefined,\n              transition:\n                canAnimate && ctx.indicator !== 'progress'\n                  ? 'top 260ms cubic-bezier(0.16, 1, 0.3, 1), height 260ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease-out'\n                  : 'none',\n            }}\n          />\n        )}\n      </div>\n    )\n  },\n)\nScrollSpyIndicator.displayName = 'ScrollSpyIndicator'\n\nexport interface ScrollSpyItemProps extends React.HTMLAttributes<HTMLLIElement> {\n  value: string\n  title?: string\n  depth?: number\n}\n\nexport const ScrollSpyItem = React.forwardRef<HTMLLIElement, ScrollSpyItemProps>(\n  ({ value, title, depth, className, children, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    const parentDepth = React.useContext(ScrollSpyItemDepthContext)\n    const computedDepth = depth ?? parentDepth + 1\n\n    const localRef = React.useRef<HTMLLIElement | null>(null)\n\n    React.useEffect(() => {\n      if (!ctx) return\n      ctx.registerItem({\n        value,\n        title,\n        depth: computedDepth,\n        el: localRef.current,\n      })\n      return () => {\n        ctx.unregisterItem(value)\n      }\n    }, [value, title, computedDepth])\n\n    return (\n      <ScrollSpyItemDepthContext.Provider value={computedDepth}>\n        <li\n          ref={(node) => {\n            localRef.current = node\n            if (typeof ref === 'function') ref(node)\n            else if (ref) ref.current = node\n          }}\n          data-slot=\"scroll-spy-item\"\n          data-depth={computedDepth}\n          className={cn('relative flex flex-col', className)}\n          {...props}\n        >\n          {children}\n        </li>\n      </ScrollSpyItemDepthContext.Provider>\n    )\n  },\n)\nScrollSpyItem.displayName = 'ScrollSpyItem'\n\nexport interface ScrollSpyLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n  href: string\n  title?: string\n  depth?: number\n}\n\nexport const ScrollSpyLink = React.forwardRef<HTMLAnchorElement, ScrollSpyLinkProps>(\n  ({ href, title, depth, className, children, style, onClick: userOnClick, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    const itemDepth = React.useContext(ScrollSpyItemDepthContext)\n    const selfDepth = depth ?? itemDepth ?? 1\n\n    const isActive = ctx ? ctx.isItemActive(href) : false\n    const isParentActive = ctx ? ctx.isItemParentActive(href) : false\n    const isScrolled = ctx ? ctx.isItemScrolled(href) : false\n\n    const isCircuit = ctx?.turn !== 'straight'\n    const isLeftWithRightRail = ctx?.position === 'left' && ctx?.railPosition === 'right'\n\n    const hasIndicatorBar = ctx?.turn === 'straight' && (ctx?.indicator !== 'segment' || ctx?.keepScrolled)\n    const handleColor = resolveScrollSpyColor(ctx?.color ?? 'primary')\n\n    const borderActiveClass = hasIndicatorBar\n      ? isActive || isParentActive\n        ? 'text-foreground font-medium'\n        : isScrolled\n          ? 'text-foreground/85'\n          : 'text-muted-foreground hover:text-foreground'\n      : isActive\n        ? cn(handleColor.borderClass, 'text-foreground font-medium')\n        : isParentActive\n          ? 'border-border/50 text-foreground font-medium'\n          : isScrolled\n            ? 'border-border/70 text-foreground/85'\n            : 'text-muted-foreground hover:border-foreground/40 hover:text-foreground'\n\n    const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n      userOnClick?.(e)\n      if (e.defaultPrevented) return\n      e.preventDefault()\n      if (!ctx) return\n      ctx.scrollToHref(href)\n    }\n\n    const linkBorderActiveStyle =\n      !isCircuit && !hasIndicatorBar && (isActive || isParentActive || isScrolled)\n        ? {\n            borderLeftWidth: isLeftWithRightRail ? undefined : `${ctx?.resolvedLineWidth ?? 2.5}px`,\n            borderRightWidth: isLeftWithRightRail ? `${ctx?.resolvedLineWidth ?? 2.5}px` : undefined,\n            marginLeft: isLeftWithRightRail ? undefined : `-${ctx?.resolvedLineWidth ?? 2.5}px`,\n            marginRight: isLeftWithRightRail ? `-${ctx?.resolvedLineWidth ?? 2.5}px` : undefined,\n            borderColor: isActive ? handleColor.customColor : undefined,\n          }\n        : undefined\n\n    return (\n      <a\n        ref={ref}\n        href={href}\n        data-slot=\"scroll-spy-link\"\n        aria-current={isActive ? 'location' : undefined}\n        data-active={isActive ? 'true' : 'false'}\n        data-parent-active={isParentActive ? 'true' : 'false'}\n        data-scrolled={isScrolled ? 'true' : 'false'}\n        data-depth={selfDepth}\n        style={{ ...linkBorderActiveStyle, ...style }}\n        className={cn(\n          'group block rounded-none leading-snug no-underline transition-[color,border-color,background-color,opacity,border-width,margin] duration-200 ease-out',\n          'focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',\n          isCircuit\n            ? [\n                'py-1',\n                isLeftWithRightRail\n                  ? [\n                      'text-right',\n                      selfDepth <= 1 && 'pr-4 pl-2 text-sm',\n                      selfDepth === 2 && 'pr-7 pl-2 text-xs',\n                      selfDepth === 3 && 'pr-10 pl-2 text-xs',\n                      selfDepth >= 4 && 'pr-12 pl-2 text-xs',\n                    ]\n                  : [\n                      selfDepth <= 1 && 'pr-2 pl-4 text-sm',\n                      selfDepth === 2 && 'pr-2 pl-7 text-xs',\n                      selfDepth === 3 && 'pr-2 pl-10 text-xs',\n                      selfDepth >= 4 && 'pr-2 pl-12 text-xs',\n                    ],\n                isActive || isParentActive\n                  ? 'text-foreground font-medium'\n                  : isScrolled\n                    ? 'text-foreground/85'\n                    : 'text-muted-foreground hover:text-foreground',\n              ]\n            : isLeftWithRightRail\n              ? [\n                  '-mr-px border-r border-transparent py-0.5 pr-3 pl-2 text-right',\n                  selfDepth <= 1 && 'text-sm',\n                  selfDepth === 2 && 'pr-6 text-xs',\n                  selfDepth === 3 && 'pr-9 text-xs',\n                  selfDepth >= 4 && 'pr-11 text-xs',\n                  borderActiveClass,\n                ]\n              : [\n                  '-ml-px border-l border-transparent py-0.5 pr-2 pl-3',\n                  selfDepth <= 1 && 'text-sm',\n                  selfDepth === 2 && 'pl-6 text-xs',\n                  selfDepth === 3 && 'pl-9 text-xs',\n                  selfDepth >= 4 && 'pl-11 text-xs',\n                  borderActiveClass,\n                ],\n          className,\n        )}\n        onClick={handleClick}\n        {...props}\n      >\n        {children ?? title}\n      </a>\n    )\n  },\n)\nScrollSpyLink.displayName = 'ScrollSpyLink'\n\nexport interface ScrollSpyStepperProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nexport const ScrollSpyStepper = React.forwardRef<HTMLDivElement, ScrollSpyStepperProps>(\n  ({ className, ...props }, ref) => {\n    const ctx = React.useContext(ScrollSpyContext)\n    if (!ctx) throw new Error('ScrollSpyStepper must be used inside <ScrollSpy>.')\n\n    const items = ctx.getItems()\n    const activeValue = ctx.activeValue\n    const activeIndex = Math.max(\n      0,\n      items.findIndex((i) => i.value === activeValue || i.value.replace(/^#/, '') === activeValue.replace(/^#/, '')),\n    )\n    const activeItem = items[activeIndex]\n    const activeTitle = activeItem\n      ? activeItem.title ||\n        activeItem.value\n          .replace(/^#/, '')\n          .replace(/[-_]/g, ' ')\n          .replace(/\\b\\w/g, (c) => c.toUpperCase())\n      : ''\n\n    if (ctx.position === 'top') {\n      const isScrollSpy =\n        ctx.variant === 'scrollspy' ||\n        ctx.variant === 'tabs' ||\n        ctx.variant === 'pills' ||\n        ctx.indicator === 'pill' ||\n        ctx.indicator === 'dot'\n\n      if (isScrollSpy) {\n        return (\n          <div\n            ref={ref}\n            data-slot=\"scroll-spy-top\"\n            className={cn(\n              'border-border/70 bg-card/85 relative sticky top-0 z-20 flex w-full scrollbar-none items-center gap-1 overflow-x-auto rounded-xl border p-1.5 shadow-xs backdrop-blur-md',\n              className,\n            )}\n            {...props}\n          >\n            {items.map((item, idx) => {\n              const isCurrent = idx === activeIndex\n              const label =\n                item.title ||\n                item.value\n                  .replace(/^#/, '')\n                  .replace(/[-_]/g, ' ')\n                  .replace(/\\b\\w/g, (c) => c.toUpperCase())\n\n              return (\n                <button\n                  key={item.value}\n                  type=\"button\"\n                  data-active={isCurrent ? 'true' : 'false'}\n                  className={cn(\n                    'group relative flex shrink-0 items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium transition-all duration-150 outline-none select-none',\n                    'focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-[0.98]',\n                    isCurrent\n                      ? 'bg-primary/10 text-primary font-semibold shadow-2xs'\n                      : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',\n                  )}\n                  onClick={() => ctx.scrollToHref(item.value)}\n                >\n                  {isCurrent && (\n                    <span className=\"bg-primary animate-in fade-in zoom-in-75 size-1.5 shrink-0 rounded-full duration-150\" />\n                  )}\n                  <span className=\"truncate\">{label}</span>\n                </button>\n              )\n            })}\n\n            <div className=\"bg-border/20 absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-xl\">\n              <div\n                className=\"bg-primary h-full transition-[width] duration-150 ease-out\"\n                style={{ width: `${Math.round(ctx.scrollProgress * 100)}%` }}\n              />\n            </div>\n          </div>\n        )\n      }\n\n      return (\n        <div\n          ref={ref}\n          data-slot=\"scroll-spy-stepper-top\"\n          className={cn(\n            'border-border/70 bg-card/85 sticky top-0 z-20 flex w-full scrollbar-none items-center gap-1.5 overflow-x-auto rounded-xl border p-2 shadow-xs backdrop-blur-md',\n            className,\n          )}\n          {...props}\n        >\n          {items.map((item, idx) => {\n            const isCurrent = idx === activeIndex\n            const isCompleted = idx < activeIndex\n            const label =\n              item.title ||\n              item.value\n                .replace(/^#/, '')\n                .replace(/[-_]/g, ' ')\n                .replace(/\\b\\w/g, (c) => c.toUpperCase())\n\n            return (\n              <React.Fragment key={item.value}>\n                <button\n                  type=\"button\"\n                  data-active={isCurrent ? 'true' : 'false'}\n                  className={cn(\n                    'group flex shrink-0 items-center gap-2 rounded-md px-2.5 py-1.5 text-xs font-medium transition-[color,background-color,transform] duration-150 outline-none select-none',\n                    'focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-1 active:scale-[0.98]',\n                    isCurrent\n                      ? 'bg-primary/10 text-foreground font-medium shadow-2xs'\n                      : isCompleted\n                        ? 'text-foreground/80 hover:bg-muted/50'\n                        : 'text-muted-foreground hover:bg-muted/30 hover:text-foreground',\n                  )}\n                  onClick={() => ctx.scrollToHref(item.value)}\n                >\n                  <span\n                    className={cn(\n                      'flex size-5 shrink-0 items-center justify-center rounded-full font-mono text-[10px] transition-all duration-200',\n                      isCurrent\n                        ? 'bg-primary text-primary-foreground scale-105 font-semibold shadow-2xs'\n                        : isCompleted\n                          ? 'bg-primary/15 text-primary font-medium'\n                          : 'bg-muted text-muted-foreground/70',\n                    )}\n                  >\n                    {isCompleted ? (\n                      <svg className=\"size-3 stroke-[2.5]\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                        <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M4.5 12.75l6 6 9-13.5\" />\n                      </svg>\n                    ) : (\n                      idx + 1\n                    )}\n                  </span>\n                  <span className=\"max-w-[120px] truncate\">{label}</span>\n                </button>\n\n                {idx < items.length - 1 && (\n                  <div\n                    className={cn(\n                      'bg-border/70 h-0.5 max-w-10 min-w-4 flex-1 rounded-full transition-colors duration-200',\n                      isCompleted && 'bg-primary',\n                    )}\n                  />\n                )}\n              </React.Fragment>\n            )\n          })}\n        </div>\n      )\n    }\n\n    if (ctx.position === 'bottom') {\n      return (\n        <div\n          ref={ref}\n          data-slot=\"scroll-spy-stepper-bottom\"\n          className={cn(\n            'border-border/80 bg-background/95 sticky bottom-3 z-30 mx-auto flex items-center gap-2 rounded-full border px-3 py-1.5 shadow-md backdrop-blur-md select-none',\n            className,\n          )}\n          {...props}\n        >\n          <button\n            type=\"button\"\n            aria-label=\"Previous section\"\n            disabled={activeIndex <= 0}\n            className=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground flex size-7 items-center justify-center rounded-full transition-all active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n            onClick={ctx.goToPrev}\n          >\n            <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"2\">\n              <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M15.75 19.5L8.25 12l7.5-7.5\" />\n            </svg>\n          </button>\n\n          <div className=\"flex items-center gap-1.5 px-1\">\n            {items.map((item, idx) => (\n              <button\n                key={item.value}\n                type=\"button\"\n                aria-label={`Jump to section ${idx + 1}`}\n                className={cn(\n                  'h-1.5 cursor-pointer rounded-full transition-all duration-200',\n                  idx === activeIndex\n                    ? 'bg-primary w-5'\n                    : idx < activeIndex\n                      ? 'bg-primary/40 hover:bg-primary/60 w-2'\n                      : 'bg-muted-foreground/30 hover:bg-muted-foreground/50 w-2',\n                )}\n                onClick={() => ctx.scrollToHref(item.value)}\n              />\n            ))}\n          </div>\n\n          <div className=\"border-border/60 flex items-center gap-2 border-l pl-2\">\n            <span className=\"text-foreground max-w-[130px] truncate text-xs font-medium tracking-tight\">\n              {activeTitle}\n            </span>\n            <span className=\"bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 font-mono text-[10px] tabular-nums\">\n              {Math.round(ctx.scrollProgress * 100)}%\n            </span>\n          </div>\n\n          <button\n            type=\"button\"\n            aria-label=\"Next section\"\n            disabled={activeIndex >= items.length - 1}\n            className=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground flex size-7 items-center justify-center rounded-full transition-all active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n            onClick={ctx.goToNext}\n          >\n            <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" strokeWidth=\"2\">\n              <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M8.25 4.5l7.5 7.5-7.5 7.5\" />\n            </svg>\n          </button>\n        </div>\n      )\n    }\n\n    return null\n  },\n)\nScrollSpyStepper.displayName = 'ScrollSpyStepper'\n\n// Compound ScrollSpy with attached static subcomponents\nexport const ScrollSpy = Object.assign(ScrollSpyRoot, {\n  Root: ScrollSpyRoot,\n  Title: ScrollSpyTitle,\n  List: ScrollSpyList,\n  Indicator: ScrollSpyIndicator,\n  Item: ScrollSpyItem,\n  Link: ScrollSpyLink,\n  Stepper: ScrollSpyStepper,\n})\n\nexport default ScrollSpy\n",
      "type": "registry:ui",
      "target": "~/components/ui/scroll-spy/scroll-spy.tsx"
    },
    {
      "path": "packages/registry-react/components/scroll-spy/index.ts",
      "content": "export {\n  ScrollSpy,\n  ScrollSpyRoot,\n  ScrollSpyTitle,\n  ScrollSpyList,\n  ScrollSpyIndicator,\n  ScrollSpyItem,\n  ScrollSpyLink,\n  ScrollSpyStepper,\n  type ScrollSpyItem as ScrollSpyItemType,\n  type ScrollSpyProps,\n  type ScrollSpyTitleProps,\n  type ScrollSpyListProps,\n  type ScrollSpyIndicatorProps,\n  type ScrollSpyItemProps,\n  type ScrollSpyLinkProps,\n  type ScrollSpyStepperProps,\n  type ScrollSpyVariant,\n  type ScrollSpyTurn,\n  type ScrollSpyIndicatorMode,\n  type ScrollSpyLineWidth,\n  type ScrollSpyColor,\n  type ScrollSpyPosition,\n  type ScrollSpyRailPosition,\n} from './scroll-spy'\n",
      "type": "registry:ui",
      "target": "~/components/ui/scroll-spy/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "In-page navigation list with scroll-spy. Renders a vertical list of links; the active item highlights as the user scrolls through anchored sections.",
  "categories": [
    "navigation"
  ]
}