{
  "name": "scroll-spy",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-as-ref",
    "@diceui/use-isomorphic-layout-effect",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/scroll-spy.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/registry/bases/radix/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\n\nconst ROOT_NAME = \"ScrollSpy\";\nconst NAV_NAME = \"ScrollSpyNav\";\nconst LINK_NAME = \"ScrollSpyLink\";\nconst VIEWPORT_NAME = \"ScrollSpyViewport\";\nconst SECTION_NAME = \"ScrollSpySection\";\n\ntype Direction = \"ltr\" | \"rtl\";\ntype Orientation = \"horizontal\" | \"vertical\";\n\ntype LinkElement = React.ComponentRef<typeof ScrollSpyLink>;\ntype SectionElement = React.ComponentRef<typeof ScrollSpySection>;\n\nfunction getDefaultScrollBehavior(): ScrollBehavior {\n  if (typeof window === \"undefined\") return \"smooth\";\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n    ? \"auto\"\n    : \"smooth\";\n}\n\ninterface StoreState {\n  value: string;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;\n  notify: () => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStore<T>(\n  selector: (state: StoreState) => T,\n  ogStore?: Store | null,\n): T {\n  const contextStore = React.useContext(StoreContext);\n\n  const store = ogStore ?? contextStore;\n\n  if (!store) {\n    throw new Error(`\\`useStore\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n\n  const getSnapshot = React.useCallback(\n    () => selector(store.getState()),\n    [store, selector],\n  );\n\n  return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface ScrollSpyContextValue {\n  offset: number;\n  scrollBehavior: ScrollBehavior;\n  dir: Direction;\n  orientation: Orientation;\n  scrollContainer: HTMLElement | null;\n  isScrollingRef: React.RefObject<boolean>;\n  onSectionRegister: (id: string, element: SectionElement) => void;\n  onSectionUnregister: (id: string) => void;\n  onScrollToSection: (sectionId: string) => void;\n}\n\nconst ScrollSpyContext = React.createContext<ScrollSpyContextValue | null>(\n  null,\n);\n\nfunction useScrollSpyContext(consumerName: string) {\n  const context = React.useContext(ScrollSpyContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface ScrollSpyProps extends React.ComponentProps<\"div\"> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  rootMargin?: string;\n  threshold?: number | number[];\n  offset?: number;\n  scrollBehavior?: ScrollBehavior;\n  scrollContainer?: HTMLElement | null;\n  dir?: Direction;\n  orientation?: Orientation;\n  asChild?: boolean;\n}\n\nfunction ScrollSpy(props: ScrollSpyProps) {\n  const {\n    value,\n    defaultValue,\n    onValueChange,\n    rootMargin,\n    threshold = 0.1,\n    offset = 0,\n    scrollBehavior = getDefaultScrollBehavior(),\n    scrollContainer = null,\n    dir: dirProp,\n    orientation = \"horizontal\",\n    asChild,\n    className,\n    ...rootProps\n  } = props;\n\n  const dir = DirectionPrimitive.useDirection(dirProp);\n\n  const stateRef = useLazyRef<StoreState>(() => ({\n    value: value ?? defaultValue ?? \"\",\n  }));\n  const listenersRef = useLazyRef(() => new Set<() => void>());\n  const onValueChangeRef = useAsRef(onValueChange);\n\n  const store = React.useMemo<Store>(() => {\n    return {\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => {\n        return stateRef.current;\n      },\n      setState: (key, value) => {\n        if (Object.is(stateRef.current[key], value)) return;\n\n        stateRef.current[key] = value;\n\n        if (key === \"value\" && value) {\n          onValueChangeRef.current?.(value);\n        }\n\n        store.notify();\n      },\n      notify: () => {\n        for (const cb of listenersRef.current) {\n          cb();\n        }\n      },\n    };\n  }, [listenersRef, stateRef, onValueChangeRef]);\n\n  const sectionMapRef = React.useRef(new Map<string, Element>());\n  const isScrollingRef = React.useRef(false);\n  const rafIdRef = React.useRef<number | null>(null);\n  const isMountedRef = React.useRef(false);\n  const scrollTimeoutRef = React.useRef<number | null>(null);\n\n  const onSectionRegister = React.useCallback(\n    (id: string, element: SectionElement) => {\n      sectionMapRef.current.set(id, element);\n    },\n    [],\n  );\n\n  const onSectionUnregister = React.useCallback((id: string) => {\n    sectionMapRef.current.delete(id);\n  }, []);\n\n  const onScrollToSection = React.useCallback(\n    (sectionId: string) => {\n      const section = scrollContainer\n        ? scrollContainer.querySelector(`#${sectionId}`)\n        : document.getElementById(sectionId);\n\n      if (!section) {\n        store.setState(\"value\", sectionId);\n        return;\n      }\n\n      // Set flag to prevent observer from firing during programmatic scroll\n      isScrollingRef.current = true;\n      store.setState(\"value\", sectionId);\n\n      if (scrollContainer) {\n        const containerRect = scrollContainer.getBoundingClientRect();\n        const sectionRect = section.getBoundingClientRect();\n        const scrollTop = scrollContainer.scrollTop;\n        const offsetPosition =\n          sectionRect.top - containerRect.top + scrollTop - offset;\n\n        scrollContainer.scrollTo({\n          top: offsetPosition,\n          behavior: scrollBehavior,\n        });\n      } else {\n        const sectionPosition = section.getBoundingClientRect().top;\n        const offsetPosition = sectionPosition + window.scrollY - offset;\n\n        window.scrollTo({\n          top: offsetPosition,\n          behavior: scrollBehavior,\n        });\n      }\n\n      if (scrollTimeoutRef.current !== null) {\n        clearTimeout(scrollTimeoutRef.current);\n      }\n\n      scrollTimeoutRef.current = window.setTimeout(() => {\n        isScrollingRef.current = false;\n      }, 500);\n    },\n    [scrollContainer, offset, scrollBehavior, store],\n  );\n\n  useIsomorphicLayoutEffect(() => {\n    const currentValue = value ?? defaultValue;\n    if (currentValue === undefined) return;\n\n    if (!isMountedRef.current) {\n      isMountedRef.current = true;\n      store.setState(\"value\", currentValue);\n      return;\n    }\n\n    onScrollToSection(currentValue);\n  }, [value, onScrollToSection]);\n\n  useIsomorphicLayoutEffect(() => {\n    const sectionMap = sectionMapRef.current;\n    if (sectionMap.size === 0) return;\n\n    const observerRootMargin = rootMargin ?? `${-offset}px 0px -70% 0px`;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (isScrollingRef.current) return;\n\n        if (rafIdRef.current !== null) {\n          cancelAnimationFrame(rafIdRef.current);\n        }\n\n        rafIdRef.current = requestAnimationFrame(() => {\n          const intersecting = entries.filter((entry) => entry.isIntersecting);\n\n          if (intersecting.length === 0) return;\n\n          const topmost = intersecting.reduce((prev, curr) => {\n            return curr.boundingClientRect.top < prev.boundingClientRect.top\n              ? curr\n              : prev;\n          });\n\n          const id = topmost.target.id;\n          if (id && sectionMap.has(id)) {\n            store.setState(\"value\", id);\n          }\n        });\n      },\n      {\n        root: scrollContainer,\n        rootMargin: observerRootMargin,\n        threshold,\n      },\n    );\n\n    for (const element of sectionMap.values()) {\n      observer.observe(element);\n    }\n\n    return () => {\n      observer.disconnect();\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current);\n      }\n      if (scrollTimeoutRef.current !== null) {\n        clearTimeout(scrollTimeoutRef.current);\n      }\n    };\n  }, [offset, rootMargin, threshold, scrollContainer]);\n\n  const contextValue = React.useMemo<ScrollSpyContextValue>(\n    () => ({\n      dir,\n      orientation,\n      offset,\n      scrollBehavior,\n      scrollContainer,\n      isScrollingRef,\n      onSectionRegister,\n      onSectionUnregister,\n      onScrollToSection,\n    }),\n    [\n      dir,\n      orientation,\n      offset,\n      scrollBehavior,\n      scrollContainer,\n      onSectionRegister,\n      onSectionUnregister,\n      onScrollToSection,\n    ],\n  );\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <StoreContext.Provider value={store}>\n      <ScrollSpyContext.Provider value={contextValue}>\n        <RootPrimitive\n          data-orientation={orientation}\n          data-slot=\"scroll-spy\"\n          dir={dir}\n          {...rootProps}\n          className={cn(\n            \"flex\",\n            orientation === \"horizontal\" ? \"flex-row\" : \"flex-col\",\n            className,\n          )}\n        />\n      </ScrollSpyContext.Provider>\n    </StoreContext.Provider>\n  );\n}\n\ninterface ScrollSpyNavProps extends React.ComponentProps<\"nav\"> {\n  asChild?: boolean;\n}\n\nfunction ScrollSpyNav(props: ScrollSpyNavProps) {\n  const { asChild, className, ...navProps } = props;\n\n  const { dir, orientation } = useScrollSpyContext(NAV_NAME);\n\n  const NavPrimitive = asChild ? SlotPrimitive.Slot : \"nav\";\n\n  return (\n    <NavPrimitive\n      data-orientation={orientation}\n      data-slot=\"scroll-spy-nav\"\n      dir={dir}\n      {...navProps}\n      className={cn(\n        \"flex gap-2\",\n        orientation === \"horizontal\" ? \"flex-col\" : \"flex-row\",\n        className,\n      )}\n    />\n  );\n}\n\ninterface ScrollSpyLinkProps extends React.ComponentProps<\"a\"> {\n  value: string;\n  asChild?: boolean;\n}\n\nfunction ScrollSpyLink(props: ScrollSpyLinkProps) {\n  const { value: linkValue, asChild, onClick, className, ...linkProps } = props;\n\n  const { orientation, onScrollToSection } = useScrollSpyContext(LINK_NAME);\n  const value = useStore((state) => state.value);\n  const isActive = value === linkValue;\n\n  const onLinkClick = React.useCallback(\n    (event: React.MouseEvent<LinkElement>) => {\n      event.preventDefault();\n      onClick?.(event);\n      onScrollToSection(linkValue);\n    },\n    [linkValue, onClick, onScrollToSection],\n  );\n\n  const LinkPrimitive = asChild ? SlotPrimitive.Slot : \"a\";\n\n  return (\n    <LinkPrimitive\n      data-orientation={orientation}\n      data-slot=\"scroll-spy-link\"\n      data-state={isActive ? \"active\" : \"inactive\"}\n      {...linkProps}\n      href={asChild ? undefined : `#${linkValue}`}\n      className={cn(\n        \"rounded px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground data-[state=active]:bg-accent data-[state=active]:text-foreground\",\n        className,\n      )}\n      onClick={onLinkClick}\n    />\n  );\n}\n\ninterface ScrollSpyViewportProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\nfunction ScrollSpyViewport(props: ScrollSpyViewportProps) {\n  const { asChild, className, ...viewportProps } = props;\n\n  const { dir, orientation } = useScrollSpyContext(VIEWPORT_NAME);\n\n  const ViewportPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ViewportPrimitive\n      data-orientation={orientation}\n      data-slot=\"scroll-spy-viewport\"\n      dir={dir}\n      {...viewportProps}\n      className={cn(\"flex flex-1 flex-col gap-8\", className)}\n    />\n  );\n}\n\ninterface ScrollSpySectionProps extends React.ComponentProps<\"div\"> {\n  value: string;\n  asChild?: boolean;\n}\n\nfunction ScrollSpySection(props: ScrollSpySectionProps) {\n  const { asChild, ref, value, ...sectionProps } = props;\n\n  const { orientation, onSectionRegister, onSectionUnregister } =\n    useScrollSpyContext(SECTION_NAME);\n  const sectionRef = React.useRef<SectionElement>(null);\n  const composedRef = useComposedRefs(ref, sectionRef);\n\n  useIsomorphicLayoutEffect(() => {\n    const element = sectionRef.current;\n    if (!element || !value) return;\n\n    onSectionRegister(value, element);\n\n    return () => {\n      onSectionUnregister(value);\n    };\n  }, [value, onSectionRegister, onSectionUnregister]);\n\n  const SectionPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SectionPrimitive\n      data-orientation={orientation}\n      data-slot=\"scroll-spy-section\"\n      {...sectionProps}\n      id={value}\n      ref={composedRef}\n    />\n  );\n}\n\nexport {\n  ScrollSpy,\n  ScrollSpyLink,\n  ScrollSpyNav,\n  type ScrollSpyProps,\n  ScrollSpySection,\n  ScrollSpyViewport,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}