"use client" import React, { ElementType, useMemo, useRef } from "react" import { motion, useScroll, useTransform, useSpring } from "motion/react" import { cn } from "@/lib/utils" // handy function to extract text from children const extractTextFromChildren = (children: React.ReactNode): string | undefined => { // Handle null/undefined if (children == null) return "" // Handle string if (typeof children === "string") return children // Handle number if (typeof children === "number") return String(children) // Handle arrays (including fragments) if (Array.isArray(children)) { return children.map(extractTextFromChildren).join("") } // Handle React elements if (React.isValidElement(children)) { const props = (children as React.ReactElement).props const childText = (props as any).children as React.ReactNode // Recursively extract text from children if (childText != null) { return extractTextFromChildren(childText) } return "" } } interface ScrollAndSwapTextProps { /** * The content to be displayed and animated */ children: React.ReactNode /** * HTML Tag to render the component as * @default "span" */ as?: ElementType /** * Reference to the container element for scroll tracking */ containerRef: React.RefObject /** * Offset configuration for when the animation should start and end relative to the scroll container. Check motion documentation for more details. * @default ["0 0", "0 1"] */ offset?: [string, string] /** * Additional CSS classes for styling the component */ className?: string /** * Spring animation configuration for smoothing the scroll-based animation * @default { stiffness: 200, damping: 30 } */ springConfig?: { stiffness?: number damping?: number mass?: number } } /** * ScrollAndSwapText creates a scroll-triggered text animation where text slides vertically * based on scroll progress. */ const ScrollAndSwapText = ({ children, as = "span", offset = ["0 0", "0 1"], className, containerRef, springConfig = { stiffness: 200, damping: 30 }, ...props }: ScrollAndSwapTextProps) => { const ref = useRef(null) // Convert children to string for processing with error handling const text = useMemo(() => { try { return extractTextFromChildren(children) } catch (error) { console.error(error) return "" } }, [children]) // Track scroll progress within the specified container and target element const { scrollYProgress } = useScroll({ container: containerRef, target: ref, offset: offset as any, // framer motion doesnt export the type, so we have to cast it, sorry :/ }) // Apply spring physics to smooth the scroll-based animation const springScrollYProgress = useSpring(scrollYProgress, springConfig) // Transform scroll progress into vertical translation values // Original text moves from 0% to -100% (slides up and out) const top = useTransform(springScrollYProgress, [0, 1], ["0%", "-100%"]) // Replacement text moves from 100% to 0% (slides up from below) const bottom = useTransform(springScrollYProgress, [0, 1], ["100%", "0%"]) const ElementTag = as return ( {text} ) } ScrollAndSwapText.displayName = "ScrollAndSwapText" export default ScrollAndSwapText