{
  "name": "scroll-spy",
  "type": "registry:ui",
  "dependencies": [
    "cn",
    "radix-ui"
  ],
  "registryDependencies": [
    "@diceui/use-value-as-ref",
    "@diceui/use-ref-with-init"
  ],
  "files": [
    {
      "path": "ui/scroll-spy.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport {\n  Direction as DirectionPrimitive,\n  Slot as SlotPrimitive,\n} from \"radix-ui\";\nimport { useComposedRefs, useLayoutEffect } from \"radix-ui/internal\";\nimport * as React from \"react\";\n\nimport { useRefWithInit } from \"@/registry/bases/radix/hooks/use-ref-with-init\";\nimport { useValueAsRef } from \"@/registry/bases/radix/hooks/use-value-as-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 = useRefWithInit<StoreState>(() => ({\n    value: value ?? defaultValue ?? \"\",\n  }));\n  const listenersRef = useRefWithInit(() => new Set<() => void>());\n  const onValueChangeRef = useValueAsRef(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  useLayoutEffect(() => {\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  useLayoutEffect(() => {\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(\"rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-accent-foreground data-[state=active]:bg-accent data-[state=active]:text-foreground\", className)}\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  useLayoutEffect(() => {\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": ""
    }
  ]
}