{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"smooth-scroll","type":"registry:component","title":"Scroll Animation Smooth Scroll","description":"Smooth-scroll provider over Lenis with a useSmoothScroll hook exposing scroll offset, progress and velocity. Reduced-motion safe.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["lenis","motion"],"registryDependencies":[],"files":[{"path":"components/motion/smooth-scroll.tsx","type":"registry:component","target":"@components/motion/smooth-scroll.tsx","content":"\"use client\";\n// beui.dev/components/motion/scroll-animation\n\nimport type Lenis from \"lenis\";\nimport { ReactLenis, useLenis } from \"lenis/react\";\nimport { type MotionValue, useMotionValue, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n} from \"react\";\n\n// Lenis' own expo-out curve — the canonical smooth-scroll easing. Kept as a\n// named local fn (not a lib/ease token) because tokens are bezier control\n// points for the motion lib, while Lenis needs a (t) => number easing fn.\nconst EASE_SCROLL = (t: number) => Math.min(1, 1.001 - 2 ** (-10 * t));\n\nexport type ScrollTarget = number | string | HTMLElement;\n\nexport type ScrollToOptions = {\n  offset?: number;\n  immediate?: boolean;\n  duration?: number;\n};\n\nexport type SmoothScrollApi = {\n  /** Underlying Lenis instance, or null on the reduced-motion / native path. */\n  lenis: Lenis | null;\n  /** Current scroll offset in px. */\n  scrollY: MotionValue<number>;\n  /** Scroll position as 0..1 of the scrollable height. */\n  progress: MotionValue<number>;\n  /** Signed scroll velocity (px/frame); drives velocity-based effects. */\n  velocity: MotionValue<number>;\n  /** Programmatic smooth scroll. Respects reduced motion (jumps instantly). */\n  scrollTo: (target: ScrollTarget, options?: ScrollToOptions) => void;\n};\n\nconst SmoothScrollContext = createContext<SmoothScrollApi | null>(null);\n\nexport interface SmoothScrollProps {\n  children: ReactNode;\n  /** Drive the page (window) when true, or a contained scroll area when false. */\n  root?: boolean;\n  /** Smoothing factor; lower is smoother and heavier. */\n  lerp?: number;\n  /** Wheel / programmatic ease duration in seconds. */\n  duration?: number;\n  orientation?: \"vertical\" | \"horizontal\";\n  /** Wheel scroll speed multiplier. */\n  wheelMultiplier?: number;\n  /** Smooth touch scrolling. Off by default — native momentum is good on mobile. */\n  touch?: boolean;\n  className?: string;\n}\n\ntype ScrollSource = Window | HTMLElement;\n\nfunction readMetrics(target: ScrollSource) {\n  if (target instanceof Window) {\n    const max = Math.max(\n      0,\n      document.documentElement.scrollHeight - window.innerHeight,\n    );\n    return { y: window.scrollY, max };\n  }\n  return {\n    y: target.scrollTop,\n    max: Math.max(0, target.scrollHeight - target.clientHeight),\n  };\n}\n\nfunction resolveTop(\n  target: ScrollTarget,\n  source: ScrollSource,\n  offset = 0,\n): number {\n  if (typeof target === \"number\") return target + offset;\n  if (source instanceof Window) {\n    const el =\n      typeof target === \"string\" ? document.querySelector(target) : target;\n    if (!el) return window.scrollY;\n    return el.getBoundingClientRect().top + window.scrollY + offset;\n  }\n  const el =\n    typeof target === \"string\" ? source.querySelector(target) : target;\n  if (!(el instanceof HTMLElement)) return source.scrollTop;\n  return el.offsetTop + offset;\n}\n\n/** Pushes Lenis' live scroll state into the shared motion values. */\nfunction LenisBridge({\n  scrollY,\n  progress,\n  velocity,\n  lenisRef,\n}: {\n  scrollY: MotionValue<number>;\n  progress: MotionValue<number>;\n  velocity: MotionValue<number>;\n  lenisRef: { current: Lenis | null };\n}) {\n  const lenis = useLenis((instance) => {\n    scrollY.set(instance.scroll);\n    progress.set(instance.progress);\n    velocity.set(instance.velocity);\n  });\n  useEffect(() => {\n    lenisRef.current = lenis ?? null;\n    return () => {\n      lenisRef.current = null;\n    };\n  }, [lenis, lenisRef]);\n  return null;\n}\n\n/** Native scroll listener for the reduced-motion path and the no-provider fallback. */\nfunction useNativeScrollSync(\n  enabled: boolean,\n  getTarget: () => ScrollSource | null,\n  scrollY: MotionValue<number>,\n  progress: MotionValue<number>,\n  velocity: MotionValue<number>,\n) {\n  useEffect(() => {\n    if (!enabled) return;\n    const target = getTarget();\n    if (!target) return;\n    let lastY = readMetrics(target).y;\n    let lastT = performance.now();\n    const onScroll = () => {\n      const { y, max } = readMetrics(target);\n      const now = performance.now();\n      const dt = now - lastT || 16;\n      scrollY.set(y);\n      progress.set(max > 0 ? y / max : 0);\n      velocity.set(((y - lastY) / dt) * 16);\n      lastY = y;\n      lastT = now;\n    };\n    onScroll();\n    target.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => target.removeEventListener(\"scroll\", onScroll);\n  }, [enabled, getTarget, scrollY, progress, velocity]);\n}\n\nexport function SmoothScroll({\n  children,\n  root = true,\n  lerp = 0.1,\n  duration = 1.2,\n  orientation = \"vertical\",\n  wheelMultiplier = 1,\n  touch = false,\n  className,\n}: SmoothScrollProps) {\n  const reduce = useReducedMotion();\n  const scrollY = useMotionValue(0);\n  const progress = useMotionValue(0);\n  const velocity = useMotionValue(0);\n  const lenisRef = useRef<Lenis | null>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const nativeSource = useCallback(\n    (): ScrollSource | null => (root ? window : containerRef.current),\n    [root],\n  );\n\n  const scrollTo = useCallback(\n    (target: ScrollTarget, options?: ScrollToOptions) => {\n      const lenis = lenisRef.current;\n      if (lenis && !reduce) {\n        lenis.scrollTo(target, {\n          offset: options?.offset,\n          duration: options?.duration,\n          immediate: options?.immediate,\n        });\n        return;\n      }\n      const source = nativeSource();\n      const behavior = reduce || options?.immediate ? \"auto\" : \"smooth\";\n      const top = resolveTop(target, source ?? window, options?.offset);\n      (source ?? window).scrollTo({ top, behavior });\n    },\n    [reduce, nativeSource],\n  );\n\n  // Reduced motion drives the native listener; the Lenis path leaves it\n  // disabled and lets LenisBridge feed the values instead.\n  useNativeScrollSync(!!reduce, nativeSource, scrollY, progress, velocity);\n\n  const api = useMemo<SmoothScrollApi>(\n    () => ({ lenis: lenisRef.current, scrollY, progress, velocity, scrollTo }),\n    [scrollY, progress, velocity, scrollTo],\n  );\n\n  if (reduce) {\n    return (\n      <SmoothScrollContext.Provider value={api}>\n        <div ref={containerRef} className={className}>\n          {children}\n        </div>\n      </SmoothScrollContext.Provider>\n    );\n  }\n\n  return (\n    <SmoothScrollContext.Provider value={api}>\n      <ReactLenis\n        root={root}\n        className={className}\n        options={{\n          lerp,\n          duration,\n          orientation,\n          wheelMultiplier,\n          smoothWheel: true,\n          syncTouch: touch,\n          easing: EASE_SCROLL,\n        }}\n      >\n        <LenisBridge\n          scrollY={scrollY}\n          progress={progress}\n          velocity={velocity}\n          lenisRef={lenisRef}\n        />\n        {children}\n      </ReactLenis>\n    </SmoothScrollContext.Provider>\n  );\n}\n\n/**\n * Read the page's smooth-scroll state. Inside <SmoothScroll> it returns the\n * shared motion values; outside it falls back to a native window scroll\n * listener so scroll-driven components still work without the provider.\n */\nexport function useSmoothScroll(): SmoothScrollApi {\n  const ctx = useContext(SmoothScrollContext);\n  const reduce = useReducedMotion();\n  const scrollY = useMotionValue(0);\n  const progress = useMotionValue(0);\n  const velocity = useMotionValue(0);\n\n  const windowSource = useCallback((): ScrollSource => window, []);\n  useNativeScrollSync(ctx === null, windowSource, scrollY, progress, velocity);\n\n  const scrollTo = useCallback((target: ScrollTarget, options?: ScrollToOptions) => {\n    window.scrollTo({\n      top: resolveTop(target, window, options?.offset),\n      behavior: reduce || options?.immediate ? \"auto\" : \"smooth\",\n    });\n  }, [reduce]);\n\n  const fallback = useMemo<SmoothScrollApi>(\n    () => ({ lenis: null, scrollY, progress, velocity, scrollTo }),\n    [scrollY, progress, velocity, scrollTo],\n  );\n\n  return ctx ?? fallback;\n}\n"}]}