{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "scroll-spy",
  "title": "ScrollSpy",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpy.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type {\n    ScrollSpyColor,\n    ScrollSpyIndicatorMode,\n    ScrollSpyLineWidth,\n    ScrollSpyPosition,\n    ScrollSpyRailPosition,\n    ScrollSpyTurn,\n    ScrollSpyVariant,\n  } from './context'\n\n  export interface ScrollSpyItemData {\n    href: string\n    title: string\n    depth?: number\n    children?: ScrollSpyItemData[]\n  }\n\n  export interface ScrollSpyProps extends HTMLAttributes<HTMLElement> {\n    modelValue?: string\n    items?: ScrollSpyItemData[]\n    title?: 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?: (value: number) => void\n    children?: Snippet\n    ref?: HTMLElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { setContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { SCROLL_SPY_CONTEXT_KEY, type RegisteredItem, type ScrollSpyContext } from './context'\n  import ScrollSpyTitle from './ScrollSpyTitle.svelte'\n  import ScrollSpyList from './ScrollSpyList.svelte'\n  import ScrollSpyIndicator from './ScrollSpyIndicator.svelte'\n  import ScrollSpyItemComp from './ScrollSpyItem.svelte'\n  import ScrollSpyLink from './ScrollSpyLink.svelte'\n  import ScrollSpyStepper from './ScrollSpyStepper.svelte'\n\n  let {\n    class: className,\n    modelValue = $bindable(''),\n    items = [],\n    title,\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    children,\n    ref = $bindable(null),\n    ...restProps\n  }: ScrollSpyProps = $props()\n\n  const resolvedPosition = $derived(position ?? 'right')\n  const resolvedRailPosition = $derived.by(() => {\n    if (railPosition) return railPosition\n    return position === 'left' ? 'right' : 'left'\n  })\n\n  const resolvedTurn = $derived.by(() => {\n    if (turn) return turn\n    if (variant === 'angle' || variant === 'rounded') return 'rounded'\n    if (variant === 'sharp') return 'sharp'\n    if (variant === 'line' || variant === 'default') return 'straight'\n    return 'straight'\n  })\n\n  const resolvedVariant = $derived.by(() => {\n    if (variant) return variant\n    if (turn === 'sharp') return 'angle'\n    if (turn === 'rounded') return 'rounded'\n    if (position === 'top' || position === 'bottom') return 'stepper'\n    return 'line'\n  })\n\n  const resolvedIndicator = $derived(indicator ?? 'line')\n\n  let internalActive = $state(modelValue || (items[0]?.href ?? ''))\n  const activeValue = $derived(modelValue || internalActive)\n\n  function setActiveValue(val: string) {\n    if (internalActive !== val) {\n      internalActive = val\n      modelValue = val\n      onChange?.(val)\n    }\n  }\n\n  function flattenItems(rawItems: ScrollSpyItemData[]): RegisteredItem[] {\n    const result: RegisteredItem[] = []\n    function walk(list: ScrollSpyItemData[], 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\n  let registeredItems = $state<RegisteredItem[]>(items && items.length > 0 ? flattenItems(items) : [])\n  let listEl = $state<HTMLElement | null>(null)\n  let resolvedContainer = $state<HTMLElement | Window | null>(null)\n  let readingProgress = $state(0)\n\n  function resolveContainer(): HTMLElement | Window | null {\n    if (typeof window === 'undefined') return null\n    const sc = scrollContainer\n    if (!sc) return window\n    if (typeof sc === 'function') {\n      return ((sc as () => HTMLElement | Window | null)() ?? window) as HTMLElement | Window\n    }\n    if (typeof sc === 'string') {\n      return (document.querySelector(sc) as HTMLElement) ?? window\n    }\n    return sc as HTMLElement | Window\n  }\n\n  function registerItem(item: RegisteredItem) {\n    const existingIdx = registeredItems.findIndex((i) => i.value === item.value)\n    if (existingIdx >= 0) {\n      registeredItems[existingIdx] = {\n        ...registeredItems[existingIdx]!,\n        ...item,\n        title: item.title ?? registeredItems[existingIdx]!.title,\n      }\n    } else {\n      registeredItems.push(item)\n    }\n    if (!activeValue && registeredItems.length > 0) {\n      setActiveValue(registeredItems[0]!.value)\n    }\n  }\n\n  // Re-flatten when the items prop changes, preserving measured elements.\n  $effect(() => {\n    const newItems = items\n    if (newItems && newItems.length > 0) {\n      const flattened = flattenItems(newItems)\n      const existingMap = new Map(registeredItems.map((i) => [i.value, i]))\n      registeredItems = flattened.map((item) => {\n        const existing = existingMap.get(item.value)\n        return {\n          ...item,\n          el: existing?.el ?? null,\n          title: item.title ?? existing?.title,\n        }\n      })\n      if (!activeValue && registeredItems.length > 0) {\n        setActiveValue(registeredItems[0]!.value)\n      }\n    }\n  })\n\n  function unregisterItem(value: string) {\n    registeredItems = registeredItems.filter((i) => i.value !== value)\n  }\n\n  function scrollToHref(href: string) {\n    setActiveValue(href)\n    const container = resolvedContainer || resolveContainer()\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) - offsetTop\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 - offsetTop\n        window.scrollTo({ top, behavior })\n      }\n    }\n\n    if (typeof history !== 'undefined') {\n      history.replaceState(null, '', href)\n    }\n  }\n\n  function goToPrev() {\n    const list = registeredItems\n    const active = activeValue\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  }\n\n  function goToNext() {\n    const list = registeredItems\n    const active = activeValue\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  }\n\n  const activeIndex = $derived.by(() => {\n    const current = activeValue\n    if (!current) return -1\n    const clean = current.replace(/^#/, '')\n    return registeredItems.findIndex((i) => i.value === current || i.value.replace(/^#/, '') === clean)\n  })\n\n  function isItemActive(value: string): boolean {\n    if (!value || !activeValue) return false\n    const cleanVal = value.replace(/^#/, '')\n    const cleanActive = activeValue.replace(/^#/, '')\n    return cleanVal === cleanActive\n  }\n\n  function isItemParentActive(value: string): boolean {\n    if (!highlightParent || !value || !activeValue) return false\n    const cleanVal = value.replace(/^#/, '')\n    const cleanActive = activeValue.replace(/^#/, '')\n    if (cleanVal === cleanActive) return false\n\n    const activeItem = registeredItems.find(\n      (i) => i.value === activeValue || i.value.replace(/^#/, '') === cleanActive,\n    )\n    let parent = activeItem?.parentValue\n    while (parent) {\n      if (parent === value || parent.replace(/^#/, '') === cleanVal) return true\n      const pItem = registeredItems.find((i) => i.value === parent || i.value.replace(/^#/, '') === parent!.replace(/^#/, ''))\n      parent = pItem?.parentValue\n    }\n    return false\n  }\n\n  function isItemScrolled(value: string): boolean {\n    if (!keepScrolled || activeIndex < 0) return false\n    const cleanVal = value.replace(/^#/, '')\n    const idx = registeredItems.findIndex((i) => i.value === value || i.value.replace(/^#/, '') === cleanVal)\n    return idx >= 0 && idx <= activeIndex\n  }\n\n  const resolvedLineWidth = $derived.by(() => {\n    const lw = lineWidth\n    if (typeof lw === 'number') return Math.max(1, lw)\n    if (lw === 'thin') return 1.5\n    if (lw === 'thick') return 3.5\n    return 2.5\n  })\n\n  const ctx: ScrollSpyContext = {\n    get activeValue() {\n      return activeValue\n    },\n    setActiveValue,\n    get scrollProgress() {\n      return readingProgress\n    },\n    registerItem,\n    unregisterItem,\n    get variant() {\n      return resolvedVariant\n    },\n    get turn() {\n      return resolvedTurn\n    },\n    get indicator() {\n      return resolvedIndicator\n    },\n    get keepScrolled() {\n      return keepScrolled\n    },\n    get highlightParent() {\n      return highlightParent\n    },\n    get lineWidth() {\n      return lineWidth\n    },\n    get resolvedLineWidth() {\n      return resolvedLineWidth\n    },\n    get color() {\n      return color\n    },\n    get position() {\n      return resolvedPosition\n    },\n    get railPosition() {\n      return resolvedRailPosition\n    },\n    get scrollContainer() {\n      return resolvedContainer\n    },\n    get offsetTop() {\n      return offsetTop\n    },\n    get items() {\n      return registeredItems\n    },\n    getListEl: () => listEl,\n    setListEl: (el) => {\n      listEl = el\n    },\n    scrollToHref,\n    goToPrev,\n    goToNext,\n    isItemActive,\n    isItemParentActive,\n    isItemScrolled,\n  }\n  setContext(SCROLL_SPY_CONTEXT_KEY, ctx)\n\n  // Scroll Spy tracking for both local container and window, plus reading progress\n  function recomputeActive() {\n    const container = resolvedContainer\n    if (!container || registeredItems.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    readingProgress = prog\n    onProgress?.(prog)\n\n    const containerTop = isWin ? 0 : (container as HTMLElement).getBoundingClientRect().top\n    const triggerThreshold = containerTop + offsetTop + bounds + 40\n\n    let currentTarget = ''\n\n    for (const item of registeredItems) {\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      ? window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 6\n      : (container as HTMLElement).scrollTop + (container as HTMLElement).clientHeight >=\n        (container as HTMLElement).scrollHeight - 6\n\n    if (atBottom && registeredItems.length > 0) {\n      currentTarget = registeredItems[registeredItems.length - 1]!.value\n    }\n\n    if (!currentTarget && registeredItems.length > 0) {\n      currentTarget = registeredItems[0]!.value\n    }\n\n    if (currentTarget && currentTarget !== activeValue) {\n      setActiveValue(currentTarget)\n    }\n  }\n\n  let rafScroll = 0\n  function onScroll() {\n    if (typeof cancelAnimationFrame !== 'undefined') {\n      cancelAnimationFrame(rafScroll)\n    }\n    if (typeof requestAnimationFrame !== 'undefined') {\n      rafScroll = requestAnimationFrame(() => {\n        recomputeActive()\n      })\n    } else {\n      recomputeActive()\n    }\n  }\n\n  function bindScrollListener() {\n    unbindScrollListener()\n    resolvedContainer = resolveContainer()\n    const c = resolvedContainer\n    if (c && typeof (c as EventTarget).addEventListener === 'function') {\n      ;(c as EventTarget).addEventListener('scroll', onScroll, { passive: true })\n    }\n  }\n\n  function unbindScrollListener() {\n    if (typeof cancelAnimationFrame !== 'undefined') {\n      cancelAnimationFrame(rafScroll)\n    }\n    const c = resolvedContainer\n    if (c && typeof (c as EventTarget).removeEventListener === 'function') {\n      ;(c as EventTarget).removeEventListener('scroll', onScroll)\n    }\n  }\n\n  $effect(() => {\n    // Re-bind when the scroll container identity changes.\n    void scrollContainer\n    bindScrollListener()\n    recomputeActive()\n    const mountTimer = setTimeout(() => {\n      bindScrollListener()\n      recomputeActive()\n    }, 100)\n    return () => {\n      clearTimeout(mountTimer)\n      unbindScrollListener()\n    }\n  })\n</script>\n\n<nav\n  bind:this={ref}\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  class={cn(\n    'relative flex text-sm',\n    resolvedPosition === 'top' || resolvedPosition === 'bottom' ? '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` : undefined}\n  {...restProps}\n>\n  {#if items && items.length > 0}\n    <!-- Top Sticky Stepper Mode -->\n    {#if resolvedPosition === 'top'}\n      <ScrollSpyStepper />\n    {:else if resolvedPosition !== 'bottom'}\n      <!-- Standard Left or Right Vertical Rail -->\n      {#if title}\n        <ScrollSpyTitle>{title}</ScrollSpyTitle>\n      {/if}\n      <ScrollSpyList>\n        <ScrollSpyIndicator />\n        {#each items as item, itemIdx (item.href)}\n          <ScrollSpyItemComp\n            value={item.href}\n            depth={item.depth ?? 1}\n            class={itemIdx > 0 && items[itemIdx - 1]?.children?.length ? 'mt-2' : undefined}\n          >\n            <ScrollSpyLink href={item.href} title={item.title} />\n          </ScrollSpyItemComp>\n          {#each (item.children ?? []) as child, childIdx (child.href)}\n            <ScrollSpyItemComp\n              value={child.href}\n              depth={child.depth ?? 2}\n              class={childIdx === 0 || (childIdx > 0 && item.children?.[childIdx - 1]?.children?.length)\n                ? 'mt-2'\n                : undefined}\n            >\n              <ScrollSpyLink href={child.href} title={child.title} />\n            </ScrollSpyItemComp>\n            {#each (child.children ?? []) as grandchild, gIdx (grandchild.href)}\n              <ScrollSpyItemComp value={grandchild.href} depth={grandchild.depth ?? 3} class={gIdx === 0 ? 'mt-2' : undefined}>\n                <ScrollSpyLink href={grandchild.href} title={grandchild.title} />\n              </ScrollSpyItemComp>\n            {/each}\n          {/each}\n        {/each}\n      </ScrollSpyList>\n    {/if}\n\n    <!-- Bottom Floating Stepper Capsule Mode -->\n    {#if resolvedPosition === 'bottom'}\n      <ScrollSpyStepper />\n    {/if}\n  {:else}\n    {@render children?.()}\n  {/if}\n</nav>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpy.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyIndicator.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { ScrollSpyColor } from './context'\n\n  export interface ScrollSpyIndicatorProps extends HTMLAttributes<HTMLDivElement> {\n    color?: ScrollSpyColor\n  }\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { getScrollSpyContext, resolveScrollSpyColor } from './context'\n\n  let { class: className, color, ...restProps }: ScrollSpyIndicatorProps = $props()\n\n  const ctx = getScrollSpyContext('ScrollSpyIndicator')\n\n  const resolvedColor = $derived(color ?? ctx.color ?? 'primary')\n  const handleColor = $derived(resolveScrollSpyColor(resolvedColor))\n\n  interface Marker {\n    value: string\n    depth: number\n    x: number\n    y: number\n    top: number\n    bottom: number\n  }\n\n  let measurePathEl: SVGPathElement | null = $state(null)\n  let trackPath = $state('')\n  let pathLength = $state(0)\n  let activeStart = $state(0)\n  let activeEnd = $state(0)\n  let straightHighlight = $state({ top: 0, height: 0, visible: false })\n  let canAnimate = $state(false)\n  let activeMarker = $state<Marker | null>(null)\n\n  function 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    // Right side rail\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\n  function collectMarkers(): Marker[] {\n    const list = ctx.getListEl()\n    if (!list) return []\n    const listRect = list.getBoundingClientRect()\n    const validItems = ctx.items.filter((i) => i.el && i.el.isConnected)\n    if (validItems.length === 0) return []\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    return markers.sort((a, b) => a.y - b.y)\n  }\n\n  function 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\n  function measurePathLength(pathD: string): number {\n    const el = measurePathEl\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  async function updateGeometry() {\n    const markers = collectMarkers()\n    const activeValue = ctx.activeValue\n    const activeIndex = markers.findIndex(\n      (m) => m.value === activeValue || m.value.replace(/^#/, '') === activeValue.replace(/^#/, ''),\n    )\n    const turn = ctx.turn\n    const indicator = ctx.indicator\n    const isRightRail = ctx.position === 'left' && ctx.railPosition === 'right'\n\n    if (markers.length === 0) {\n      trackPath = ''\n      pathLength = 0\n      activeStart = 0\n      activeEnd = 0\n      activeMarker = null\n      straightHighlight = { top: 0, height: 0, visible: false }\n      return\n    }\n\n    const first = markers[0]!\n    const last = markers[markers.length - 1]!\n\n    if (turn === 'straight') {\n      const railX = isRightRail ? first.x : 1\n      trackPath = `M ${railX} ${first.top} L ${railX} ${last.bottom}`\n      pathLength = 0\n      activeStart = 0\n      activeEnd = 0\n\n      if (indicator === 'progress') {\n        const list = ctx.getListEl()\n        const totalHeight = list ? list.clientHeight : last.bottom - first.top\n        straightHighlight = {\n          top: first.top,\n          height: Math.max(0, totalHeight * ctx.scrollProgress),\n          visible: true,\n        }\n        activeMarker = null\n        enableAnimation()\n        return\n      }\n\n      if (activeIndex < 0) {\n        straightHighlight = { top: 0, height: 0, visible: false }\n        activeMarker = null\n        return\n      }\n\n      const active = markers[activeIndex]!\n      activeMarker = active\n\n      const isFillMode = indicator === 'fill' || ctx.keepScrolled\n\n      if (isFillMode) {\n        straightHighlight = {\n          top: first.top,\n          height: Math.max(active.bottom - first.top, 14),\n          visible: true,\n        }\n      } else {\n        straightHighlight = {\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 = turn === 'rounded'\n    const fullPath = buildCircuitPath(markers, markers.length - 1, rounded, 'bottom')\n    trackPath = fullPath\n    straightHighlight = { top: 0, height: 0, visible: false }\n\n    await tick()\n\n    const total = measurePathLength(fullPath)\n    pathLength = total\n\n    if (total === 0) {\n      activeStart = 0\n      activeEnd = 0\n      activeMarker = null\n      return\n    }\n\n    if (indicator === 'progress') {\n      activeStart = 0\n      activeEnd = total * ctx.scrollProgress\n      activeMarker = null\n      enableAnimation()\n      return\n    }\n\n    if (activeIndex < 0) {\n      activeStart = 0\n      activeEnd = 0\n      activeMarker = null\n      return\n    }\n\n    const active = markers[activeIndex]!\n    activeMarker = active\n\n    const isFillMode = indicator === 'fill' || ctx.keepScrolled\n    const startLength = isFillMode ? 0 : measurePathLength(buildCircuitPath(markers, activeIndex, rounded, 'top'))\n    const endLength = measurePathLength(buildCircuitPath(markers, activeIndex, rounded, 'bottom'))\n\n    activeStart = startLength\n    activeEnd = endLength\n    enableAnimation()\n  }\n\n  function enableAnimation() {\n    if (!canAnimate) {\n      if (typeof requestAnimationFrame !== 'undefined') {\n        requestAnimationFrame(() => {\n          canAnimate = true\n        })\n      } else {\n        canAnimate = true\n      }\n    }\n  }\n\n  let rafId = 0\n  function scheduleUpdate() {\n    if (typeof window === 'undefined') return\n    if (typeof cancelAnimationFrame !== 'undefined') {\n      cancelAnimationFrame(rafId)\n    }\n    if (typeof requestAnimationFrame !== 'undefined') {\n      rafId = requestAnimationFrame(() => {\n        updateGeometry()\n      })\n    } else {\n      updateGeometry()\n    }\n  }\n\n  $effect(() => {\n    scheduleUpdate()\n    window.addEventListener('resize', scheduleUpdate)\n    return () => {\n      if (typeof cancelAnimationFrame !== 'undefined') {\n        cancelAnimationFrame(rafId)\n      }\n      if (typeof window !== 'undefined') {\n        window.removeEventListener('resize', scheduleUpdate)\n      }\n    }\n  })\n\n  // Re-measure whenever anything the geometry depends on changes.\n  $effect(() => {\n    void ctx.activeValue\n    void ctx.items\n    void ctx.turn\n    void ctx.indicator\n    void ctx.keepScrolled\n    void ctx.resolvedLineWidth\n    void ctx.position\n    void ctx.railPosition\n    void ctx.scrollProgress\n    scheduleUpdate()\n  })\n\n  const turn = $derived(ctx.turn)\n  const indicator = $derived(ctx.indicator)\n  const keepScrolled = $derived(ctx.keepScrolled)\n  const lineWidth = $derived(ctx.resolvedLineWidth)\n  const isRightRail = $derived(ctx.position === 'left' && ctx.railPosition === 'right')\n</script>\n\n<div data-slot=\"scroll-spy-indicator\" class={cn('pointer-events-none absolute inset-0 overflow-visible', className)} aria-hidden=\"true\" {...restProps}>\n  {#if turn !== 'straight'}\n    <svg class=\"absolute inset-0 size-full overflow-visible\" fill=\"none\">\n      <path bind:this={measurePathEl} class=\"invisible\" fill=\"none\" />\n      <path\n        d={trackPath}\n        class=\"stroke-border\"\n        stroke-width={lineWidth}\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n      />\n      {#if trackPath && activeEnd > 0}\n        <path\n          d={trackPath}\n          class={handleColor.strokeClass}\n          stroke-width={lineWidth}\n          stroke-linecap=\"butt\"\n          stroke-linejoin=\"round\"\n          style=\"stroke: {handleColor.customColor}; stroke-dasharray: {Math.max(\n            activeEnd - activeStart,\n            0,\n          )} {Math.max(pathLength, 1)}; stroke-dashoffset: {-activeStart}; transition: {canAnimate &&\n          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      {/if}\n    </svg>\n  {/if}\n\n  {#if turn === 'straight' && straightHighlight.visible && (indicator !== 'segment' || keepScrolled)}\n    <div\n      class={cn('absolute z-10 rounded-none', handleColor.bgClass)}\n      style=\"top: {straightHighlight.top}px; height: {straightHighlight.height}px; width: {lineWidth}px; background-color: {handleColor.customColor}; {isRightRail\n        ? `right: -${lineWidth}px;`\n        : `left: -${lineWidth}px;`} transition: {canAnimate && 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    ></div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyIndicator.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyItem.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollSpyItemProps extends HTMLAttributes<HTMLLIElement> {\n    value: string\n    title?: string\n    depth?: number\n    children?: Snippet\n    ref?: HTMLLIElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getContext, setContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { SCROLL_SPY_CONTEXT_KEY, SCROLL_SPY_ITEM_DEPTH_KEY, type ScrollSpyContext } from './context'\n\n  let { class: className, value, title, depth, children, ref = $bindable(null), ...restProps }: ScrollSpyItemProps =\n    $props()\n\n  const ctx = getContext<ScrollSpyContext | undefined>(SCROLL_SPY_CONTEXT_KEY) ?? null\n  const parentDepth = getContext<number | undefined>(SCROLL_SPY_ITEM_DEPTH_KEY) ?? 0\n  const computedDepth = depth ?? parentDepth + 1\n  setContext(SCROLL_SPY_ITEM_DEPTH_KEY, computedDepth)\n\n  $effect(() => {\n    const v = value\n    if (!ctx) return\n    ctx.registerItem({ value: v, title, depth: computedDepth, el: ref })\n    return () => ctx.unregisterItem(v)\n  })\n</script>\n\n<li bind:this={ref} data-slot=\"scroll-spy-item\" data-depth={computedDepth} class={cn('relative flex flex-col', className)} {...restProps}>\n  {@render children?.()}\n</li>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyItem.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyLink.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAnchorAttributes } from 'svelte/elements'\n\n  export interface ScrollSpyLinkProps extends HTMLAnchorAttributes {\n    href: string\n    title?: string\n    depth?: number\n    children?: Snippet\n    ref?: HTMLAnchorElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { getContext } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { SCROLL_SPY_ITEM_DEPTH_KEY, resolveScrollSpyColor, getScrollSpyContextOptional } from './context'\n\n  let { class: className, href, title, depth, children, ref = $bindable(null), ...restProps }: ScrollSpyLinkProps =\n    $props()\n\n  const ctx = getScrollSpyContextOptional()\n  const itemDepth = getContext<number | undefined>(SCROLL_SPY_ITEM_DEPTH_KEY) ?? 1\n  const selfDepth = $derived(depth ?? itemDepth)\n\n  const isActive = $derived(ctx?.isItemActive(href) ?? false)\n  const isParentActive = $derived(ctx?.isItemParentActive(href) ?? false)\n  const isScrolled = $derived(ctx?.isItemScrolled(href) ?? false)\n\n  const isCircuit = $derived(ctx !== null && ctx.turn !== 'straight')\n  const isLeftWithRightRail = $derived(ctx?.position === 'left' && ctx?.railPosition === 'right')\n  const hasIndicatorBar = $derived(\n    ctx !== null && ctx.turn === 'straight' && (ctx.indicator !== 'segment' || ctx.keepScrolled),\n  )\n\n  const handleColor = $derived(resolveScrollSpyColor(ctx?.color ?? 'primary'))\n\n  const borderActiveClass = $derived.by(() => {\n    if (hasIndicatorBar) {\n      if (isActive || isParentActive) return 'text-foreground font-medium'\n      if (isScrolled) return 'text-foreground/85'\n      return 'text-muted-foreground hover:text-foreground'\n    }\n    if (isActive) return cn(handleColor.borderClass, 'text-foreground font-medium')\n    if (isParentActive) return 'border-border/50 text-foreground font-medium'\n    if (isScrolled) return 'border-border/70 text-foreground/85'\n    return 'text-muted-foreground hover:border-foreground/40 hover:text-foreground'\n  })\n\n  const activeBorderStyle = $derived.by(() => {\n    if (isCircuit || hasIndicatorBar || (!isActive && !isParentActive && !isScrolled)) return undefined\n    const w = ctx?.resolvedLineWidth ?? 2.5\n    if (isLeftWithRightRail) {\n      return `border-right-width: ${w}px; margin-right: -${w}px;${isActive && handleColor.customColor ? ` border-color: ${handleColor.customColor};` : ''}`\n    }\n    return `border-left-width: ${w}px; margin-left: -${w}px;${isActive && handleColor.customColor ? ` border-color: ${handleColor.customColor};` : ''}`\n  })\n\n  function onClick(e: MouseEvent) {\n    e.preventDefault()\n    if (!ctx) return\n    ctx.scrollToHref(href)\n  }\n</script>\n\n<a\n  bind:this={ref}\n  {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={activeBorderStyle}\n  class={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={onClick}\n  {...restProps}\n>\n  {#if children}\n    {@render children()}\n  {:else}\n    {title}\n  {/if}\n</a>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyLink.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyList.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollSpyListProps extends HTMLAttributes<HTMLUListElement> {\n    children?: Snippet\n    ref?: HTMLUListElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getScrollSpyContextOptional } from './context'\n\n  let { class: className, children, ref = $bindable(null), ...restProps }: ScrollSpyListProps = $props()\n\n  const ctx = getScrollSpyContextOptional()\n  const isStraight = $derived(ctx?.turn === 'straight')\n  const isRightRail = $derived(ctx?.position === 'left' && ctx?.railPosition === 'right')\n  const lineWidthPx = $derived(`${ctx?.resolvedLineWidth ?? 2.5}px`)\n\n  $effect(() => {\n    if (ctx && ref) ctx.setListEl(ref)\n  })\n</script>\n\n<ul\n  bind:this={ref}\n  data-slot=\"scroll-spy-list\"\n  style=\"border-left-width: {isStraight && !isRightRail ? lineWidthPx : '0'}; border-right-width: {isStraight && isRightRail ? lineWidthPx : '0'};\"\n  class={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  {...restProps}\n>\n  {@render children?.()}\n</ul>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyList.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyTitle.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollSpyTitleProps extends HTMLAttributes<HTMLParagraphElement> {\n    children?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getScrollSpyContextOptional } from './context'\n\n  let { class: className, children, ...restProps }: ScrollSpyTitleProps = $props()\n\n  const ctx = getScrollSpyContextOptional()\n  const isRightRail = $derived(ctx?.position === 'left' && ctx?.railPosition === 'right')\n</script>\n\n<p data-slot=\"scroll-spy-title\" class={cn('text-foreground mb-3 text-sm font-semibold tracking-tight', isRightRail && 'pr-3 text-right', className)} {...restProps}>\n  {@render children?.()}\n</p>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyTitle.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/ScrollSpyStepper.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface ScrollSpyStepperProps extends HTMLAttributes<HTMLDivElement> {}\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { getScrollSpyContext } from './context'\n\n  let { class: className, ...restProps }: ScrollSpyStepperProps = $props()\n\n  const ctx = getScrollSpyContext('ScrollSpyStepper')\n\n  const items = $derived(ctx.items)\n  const activeValue = $derived(ctx.activeValue)\n  const scrollProgress = $derived(ctx.scrollProgress)\n  const position = $derived(ctx.position)\n\n  const activeIndex = $derived.by(() => {\n    const list = items\n    const active = activeValue\n    const idx = list.findIndex((i) => i.value === active || i.value.replace(/^#/, '') === active.replace(/^#/, ''))\n    return idx >= 0 ? idx : 0\n  })\n\n  function displayTitle(value: string | undefined, title: string | undefined): string {\n    if (title) return title\n    if (!value) return ''\n    return value\n      .replace(/^#/, '')\n      .replace(/[-_]/g, ' ')\n      .replace(/\\b\\w/g, (c) => c.toUpperCase())\n  }\n\n  const activeItem = $derived(items[activeIndex])\n  const activeTitle = $derived(displayTitle(activeItem?.value, activeItem?.title))\n\n  const isTop = $derived(position === 'top')\n  const isBottom = $derived(position === 'bottom')\n  const variant = $derived(ctx.variant)\n  const indicator = $derived(ctx.indicator)\n  const isScrollSpy = $derived(\n    variant === 'scrollspy' ||\n      variant === 'tabs' ||\n      variant === 'pills' ||\n      indicator === 'pill' ||\n      indicator === 'dot',\n  )\n</script>\n\n<!-- Top Sticky Scroll Spy Bar (Highlights ONLY the active section) -->\n{#if isTop && isScrollSpy}\n  <div\n    data-slot=\"scroll-spy-top\"\n    class={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    {...restProps}\n  >\n    {#each items as item, idx (item.value)}\n      <button\n        type=\"button\"\n        data-active={idx === activeIndex ? 'true' : 'false'}\n        class={cn(\n          'group relative flex shrink-0 items-center gap-2 rounded-lg px-3 py-1.5 text-xs font-medium transition-[color,background-color,box-shadow,scale] 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          idx === activeIndex\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        <!-- Active Dot Indicator -->\n        {#if idx === activeIndex}\n          <span class=\"bg-primary size-1.5 shrink-0 rounded-full\"></span>\n        {/if}\n        <!-- Section Title -->\n        <span class=\"truncate\">\n          {displayTitle(item.value, item.title)}\n        </span>\n      </button>\n    {/each}\n\n    <!-- Slim Bottom Reading Progress Line -->\n    <div class=\"bg-border/20 absolute inset-x-0 bottom-0 h-0.5 overflow-hidden rounded-b-xl\">\n      <div class=\"bg-primary h-full transition-[width] duration-150 ease-out\" style=\"width: {Math.round(scrollProgress * 100)}%\"></div>\n    </div>\n  </div>\n{:else if isTop}\n  <!-- Top Sticky Stepper Bar (Cumulative Step Wizard with Step Numbers & Connecting Lines) -->\n  <div\n    data-slot=\"scroll-spy-stepper-top\"\n    class={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    {...restProps}\n  >\n    {#each items as item, idx (item.value)}\n      <!-- Step Node Button -->\n      <button\n        type=\"button\"\n        data-active={idx === activeIndex ? 'true' : 'false'}\n        class={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,scale] 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          idx === activeIndex\n            ? 'bg-primary/10 text-foreground font-medium shadow-2xs'\n            : idx < activeIndex\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        <!-- Step Number / Dot -->\n        <span\n          class={cn(\n            'flex size-5 shrink-0 items-center justify-center rounded-full font-mono text-[10px] transition-[color,background-color,box-shadow,scale] duration-200',\n            idx === activeIndex\n              ? 'bg-primary text-primary-foreground scale-105 font-semibold shadow-2xs'\n              : idx < activeIndex\n                ? 'bg-primary/15 text-primary font-medium'\n                : 'bg-muted text-muted-foreground/70',\n          )}\n        >\n          {#if idx < activeIndex}\n            <svg class=\"size-3 stroke-[2.5]\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n              <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 12.75l6 6 9-13.5\" />\n            </svg>\n          {:else}\n            <span>{idx + 1}</span>\n          {/if}\n        </span>\n\n        <!-- Step Title -->\n        <span class=\"max-w-[120px] truncate\">\n          {displayTitle(item.value, item.title)}\n        </span>\n      </button>\n\n      <!-- Connecting Progress Line -->\n      {#if idx < items.length - 1}\n        <div\n          class={cn(\n            'bg-border/70 h-0.5 max-w-10 min-w-4 flex-1 rounded-full transition-colors duration-200',\n            idx < activeIndex && 'bg-primary',\n          )}\n        ></div>\n      {/if}\n    {/each}\n  </div>\n{:else if isBottom}\n  <!-- Bottom Floating Stepper Capsule -->\n  <div\n    data-slot=\"scroll-spy-stepper-bottom\"\n    class={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    {...restProps}\n  >\n    <!-- Previous Section Button -->\n    <button\n      type=\"button\"\n      aria-label=\"Previous section\"\n      disabled={activeIndex <= 0}\n      class=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground flex size-7 items-center justify-center rounded-full transition-[color,background-color,opacity,scale] active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n      onclick={ctx.goToPrev}\n    >\n      <svg class=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15.75 19.5L8.25 12l7.5-7.5\" />\n      </svg>\n    </button>\n\n    <!-- Segment / Dot Indicators -->\n    <div class=\"flex items-center gap-1.5 px-1\">\n      {#each items as item, idx (item.value)}\n        <button\n          type=\"button\"\n          aria-label={`Jump to section ${idx + 1}`}\n          class={cn(\n            'h-1.5 cursor-pointer rounded-full transition-[width,background-color] 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        ></button>\n      {/each}\n    </div>\n\n    <!-- Active Section Name -->\n    <div class=\"border-border/60 flex items-center gap-2 border-l pl-2\">\n      <span class=\"text-foreground max-w-[130px] truncate text-xs font-medium tracking-tight\">\n        {activeTitle}\n      </span>\n      <span class=\"bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 font-mono text-[10px] tabular-nums\">\n        {Math.round(scrollProgress * 100)}%\n      </span>\n    </div>\n\n    <!-- Next Section Button -->\n    <button\n      type=\"button\"\n      aria-label=\"Next section\"\n      disabled={activeIndex >= items.length - 1}\n      class=\"text-muted-foreground hover:bg-muted/80 hover:text-foreground flex size-7 items-center justify-center rounded-full transition-[color,background-color,opacity,scale] active:scale-95 disabled:pointer-events-none disabled:opacity-25\"\n      onclick={ctx.goToNext}\n    >\n      <svg class=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\n        <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.25 4.5l7.5 7.5-7.5 7.5\" />\n      </svg>\n    </button>\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/ScrollSpyStepper.svelte"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/context.ts",
      "content": "import { getContext } from 'svelte'\n\nexport type ScrollSpyTurn = 'straight' | 'sharp' | 'rounded'\nexport type ScrollSpyVariant =\n  'default' | 'line' | 'angle' | 'sharp' | 'rounded' | 'stepper' | 'scrollspy' | 'tabs' | 'pills'\nexport type ScrollSpyIndicatorMode = 'segment' | 'fill' | 'progress' | 'pill' | 'dot' | 'line'\nexport type ScrollSpyPosition = 'right' | 'left' | 'top' | 'bottom'\nexport type ScrollSpyRailPosition = 'left' | 'right'\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\nexport interface RegisteredItem {\n  value: string\n  depth: number\n  el: HTMLElement | null\n  title?: string\n  parentValue?: string | null\n}\n\n/**\n * Shared scroll-spy state, provided by `ScrollSpy` and consumed by the parts.\n * Readonly fields are implemented as getters over runes state in `ScrollSpy`,\n * so `$derived(ctx.activeValue)` in a part stays reactive.\n */\nexport interface ScrollSpyContext {\n  readonly activeValue: string\n  setActiveValue: (value: string) => void\n  readonly scrollProgress: number\n  registerItem: (item: RegisteredItem) => void\n  unregisterItem: (value: string) => void\n  readonly variant: ScrollSpyVariant\n  readonly turn: ScrollSpyTurn\n  readonly indicator: ScrollSpyIndicatorMode\n  readonly keepScrolled: boolean\n  readonly highlightParent: boolean\n  readonly lineWidth: ScrollSpyLineWidth\n  readonly resolvedLineWidth: number\n  readonly color: ScrollSpyColor\n  readonly position: ScrollSpyPosition\n  readonly railPosition: ScrollSpyRailPosition\n  readonly scrollContainer: HTMLElement | Window | null\n  readonly offsetTop: number\n  readonly items: RegisteredItem[]\n  getListEl: () => HTMLElement | null\n  setListEl: (el: HTMLElement | null) => 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\nexport const SCROLL_SPY_CONTEXT_KEY = Symbol('uipkge-scroll-spy-context')\nexport const SCROLL_SPY_ITEM_DEPTH_KEY = Symbol('uipkge-scroll-spy-depth')\n\nexport function getScrollSpyContext(component: string): ScrollSpyContext {\n  const ctx = getContext<ScrollSpyContext | undefined>(SCROLL_SPY_CONTEXT_KEY)\n  if (!ctx) throw new Error(`${component} must be used inside <ScrollSpy>.`)\n  return ctx\n}\n\nexport function getScrollSpyContextOptional(): ScrollSpyContext | null {\n  return getContext<ScrollSpyContext | undefined>(SCROLL_SPY_CONTEXT_KEY) ?? null\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/scroll-spy/context.ts"
    },
    {
      "path": "packages/registry-svelte/components/scroll-spy/index.ts",
      "content": "export { default as ScrollSpy, type ScrollSpyProps, type ScrollSpyItemData } from './ScrollSpy.svelte'\nexport { default as ScrollSpyRoot, type ScrollSpyProps as ScrollSpyRootProps } from './ScrollSpy.svelte'\nexport { default as ScrollSpyTitle, type ScrollSpyTitleProps } from './ScrollSpyTitle.svelte'\nexport { default as ScrollSpyList, type ScrollSpyListProps } from './ScrollSpyList.svelte'\nexport { default as ScrollSpyIndicator, type ScrollSpyIndicatorProps } from './ScrollSpyIndicator.svelte'\nexport { default as ScrollSpyItem, type ScrollSpyItemProps } from './ScrollSpyItem.svelte'\nexport { default as ScrollSpyLink, type ScrollSpyLinkProps } from './ScrollSpyLink.svelte'\nexport { default as ScrollSpyStepper, type ScrollSpyStepperProps } from './ScrollSpyStepper.svelte'\nexport {\n  type ScrollSpyVariant,\n  type ScrollSpyTurn,\n  type ScrollSpyIndicatorMode,\n  type ScrollSpyPosition,\n  type ScrollSpyRailPosition,\n  type ScrollSpyLineWidth,\n  type ScrollSpyColor,\n} from './context'\n",
      "type": "registry:ui",
      "target": "~/src/lib/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"
  ]
}