{
  "title": "Infinite Scroller",
  "description": "A smooth, physics-driven infinite scroller featuring drag momentum and scroll acceleration.",
  "name": "motion-infinite-scroller",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "type": "registry:ui",
  "files": [
    {
      "path": "components/paceui/motion/infinite-scroller.tsx",
      "type": "registry:ui",
      "content": "import React, { type ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n    animate,\n    motion,\n    useAnimationFrame,\n    useMotionValue,\n    useReducedMotion,\n    useScroll,\n    useSpring,\n    useTransform,\n    useVelocity,\n    wrap,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Props = {\n    children: ReactNode;\n    shiftHalf?: boolean;\n    direction?: \"left\" | \"right\" | \"up\" | \"down\";\n    className?: string;\n    speed?: number;\n    scrollVelocityMultiplier?: number;\n    hoverSpeed?: number;\n    copies?: number;\n};\n\nconst SPRING = { damping: 40, stiffness: 300 };\nconst DRAG_THRESHOLD = 8;\n\nexport const InfiniteScroller = ({\n    children,\n    shiftHalf,\n    direction = \"left\",\n    className = \"\",\n    speed = 1,\n    scrollVelocityMultiplier = 1,\n    hoverSpeed = 0,\n    copies = 3,\n}: Props) => {\n    const trackRef = useRef<HTMLDivElement>(null);\n    const rootRef = useRef<HTMLDivElement>(null);\n    const firstCopyRef = useRef<HTMLDivElement>(null);\n    const contentSizeRef = useRef(0);\n    const itemOffsets = useRef<number[]>([]);\n    const animation = useRef<ReturnType<typeof animate> | undefined>(undefined);\n    const isHovered = useRef(false);\n    const isDragging = useRef(false);\n    const inViewRef = useRef(true);\n    const canAnimateRef = useRef(false);\n    const reduceMotion = useReducedMotion();\n    const [copyCount, setCopyCount] = useState(1);\n\n    const drag = useRef({\n        pending: false,\n        startPointer: 0,\n        startPos: 0,\n        history: [] as { t: number; pos: number }[],\n        raf: null as number | null,\n        pendingPos: null as number | null,\n    });\n\n    const isHorizontal = direction === \"left\" || direction === \"right\";\n    const reverse = direction === \"left\" || direction === \"up\";\n    const safeCopies = Math.max(3, copies);\n\n    const items = useMemo(() => {\n        const list = React.Children.toArray(children);\n        if (!shiftHalf) return list;\n        const mid = Math.floor(list.length / 2);\n        return [...list.slice(mid), ...list.slice(0, mid)];\n    }, [children, shiftHalf]);\n\n    const [contentSize, setContentSize] = useState(0);\n    contentSizeRef.current = contentSize;\n\n    useEffect(() => {\n        if (reduceMotion) return;\n        const start = () => {\n            canAnimateRef.current = true;\n            setCopyCount(safeCopies);\n        };\n        const idle = window as Window & {\n            requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;\n            cancelIdleCallback?: (id: number) => void;\n        };\n        const idleId = idle.requestIdleCallback\n            ? idle.requestIdleCallback(start, { timeout: 1800 })\n            : window.setTimeout(start, 800);\n        return () => {\n            if (\"cancelIdleCallback\" in idle && typeof idle.cancelIdleCallback === \"function\") {\n                idle.cancelIdleCallback(idleId);\n            } else {\n                window.clearTimeout(idleId);\n            }\n        };\n    }, [reduceMotion, safeCopies]);\n\n    useEffect(() => {\n        const node = rootRef.current;\n        if (!node) return;\n        const observer = new IntersectionObserver(\n            ([entry]) => {\n                inViewRef.current = entry.isIntersecting;\n            },\n            { rootMargin: \"200px 0px\" },\n        );\n        observer.observe(node);\n        return () => observer.disconnect();\n    }, []);\n\n    useLayoutEffect(() => {\n        const node = firstCopyRef.current;\n        if (!node) return;\n\n        const measure = () => {\n            let total = 0;\n            const offsets: number[] = [];\n            for (const child of Array.from(node.children) as HTMLElement[]) {\n                offsets.push(total);\n                total += isHorizontal ? child.getBoundingClientRect().width : child.getBoundingClientRect().height;\n            }\n            itemOffsets.current = offsets;\n            setContentSize(total);\n        };\n\n        measure();\n        const observer = new ResizeObserver(measure);\n        observer.observe(node);\n        window.addEventListener(\"resize\", measure);\n        return () => {\n            observer.disconnect();\n            window.removeEventListener(\"resize\", measure);\n        };\n    }, [items, isHorizontal]);\n\n    const basePosition = useMotionValue(0);\n    const position = useTransform(basePosition, (v) =>\n        contentSizeRef.current ? wrap(-contentSizeRef.current, 0, v) : 0,\n    );\n\n    const { scrollY } = useScroll();\n    const scrollFactor = useTransform(useSpring(useVelocity(scrollY), SPRING), [-1000, 1000], [-3, 3], {\n        clamp: false,\n    });\n    const speedMultiplier = useSpring(1, SPRING);\n\n    useEffect(\n        () =>\n            scrollFactor.on(\"change\", (v) => {\n                if (!isDragging.current) speedMultiplier.set(1 + Math.abs(v) * scrollVelocityMultiplier);\n            }),\n        [scrollFactor, scrollVelocityMultiplier, speedMultiplier],\n    );\n\n    const wrapPos = (v: number) => (contentSizeRef.current ? wrap(-contentSizeRef.current, 0, v) : v);\n\n    const snapTo = (current: number, throwVelocity: number) => {\n        const size = contentSizeRef.current;\n        if (!size || !itemOffsets.current.length) return current;\n\n        const projected = current + throwVelocity * 0.12;\n        const candidates = itemOffsets.current.flatMap((offset) => {\n            const base = -offset;\n            const lane = Math.round((projected - base) / size);\n            return [lane - 1, lane, lane + 1].map((n) => base + n * size);\n        });\n\n        const pick = (list: number[]) =>\n            list.reduce((best, c) => (Math.abs(c - projected) < Math.abs(best - projected) ? c : best));\n\n        if (Math.abs(throwVelocity) >= 120) {\n            const dir = Math.sign(throwVelocity);\n            const ahead = candidates.filter((c) => dir * (c - current) > 0);\n            if (ahead.length) return pick(ahead);\n        }\n\n        return pick(candidates);\n    };\n\n    useAnimationFrame((_, delta) => {\n        if (reduceMotion || !canAnimateRef.current || !inViewRef.current || !contentSize || isDragging.current) return;\n        const move = ((contentSize * speed) / 40000) * speedMultiplier.get() * Math.min(delta, 50);\n        basePosition.set(wrapPos(basePosition.get() + move * (reverse ? -1 : 1)));\n    });\n\n    const pointer = (e: React.PointerEvent) => (isHorizontal ? e.clientX : e.clientY);\n\n    const pushHistory = (pos: number) => {\n        const h = drag.current.history;\n        h.push({ t: performance.now(), pos });\n        if (h.length > 8) h.shift();\n    };\n\n    const throwVelocity = () => {\n        const h = drag.current.history;\n        if (h.length < 2) return 0;\n        const last = h[h.length - 1];\n        let start = h[0];\n        for (let i = h.length - 2; i >= 0; i -= 1) {\n            start = h[i];\n            if (last.t - start.t >= 100) break;\n        }\n        const dt = last.t - start.t;\n        return dt > 8 ? ((last.pos - start.pos) / dt) * 1000 : 0;\n    };\n\n    const flushDrag = () => {\n        drag.current.raf = null;\n        if (drag.current.pendingPos !== null) {\n            basePosition.set(drag.current.pendingPos);\n            drag.current.pendingPos = null;\n        }\n    };\n\n    const setDragPos = (next: number) => {\n        drag.current.pendingPos = next;\n        if (drag.current.raf === null) drag.current.raf = requestAnimationFrame(flushDrag);\n    };\n\n    const beginDrag = (e: React.PointerEvent<HTMLDivElement>) => {\n        isDragging.current = true;\n        animation.current?.stop();\n        speedMultiplier.jump(0);\n        e.currentTarget.setPointerCapture(e.pointerId);\n        trackRef.current?.classList.add(\"will-change-transform\");\n    };\n\n    const finishDrag = (e: React.PointerEvent<HTMLDivElement>) => {\n        if (drag.current.raf !== null) cancelAnimationFrame(drag.current.raf);\n        drag.current.raf = null;\n        if (drag.current.pendingPos !== null) {\n            basePosition.set(drag.current.pendingPos);\n            drag.current.pendingPos = null;\n        }\n\n        if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);\n        trackRef.current?.classList.remove(\"will-change-transform\");\n\n        const velocity = throwVelocity();\n        animation.current = animate(basePosition, snapTo(basePosition.get(), velocity), {\n            type: \"spring\",\n            velocity,\n            stiffness: 220,\n            damping: 28,\n            onComplete: () => basePosition.set(wrapPos(basePosition.get())),\n        });\n\n        drag.current.history = [];\n        speedMultiplier.set(isHovered.current ? hoverSpeed : 1);\n    };\n\n    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n        if (e.button !== 0) return;\n        animation.current?.stop();\n        drag.current = {\n            pending: true,\n            startPointer: pointer(e),\n            startPos: basePosition.get(),\n            history: [{ t: performance.now(), pos: pointer(e) }],\n            raf: null,\n            pendingPos: null,\n        };\n    };\n\n    const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n        if (!drag.current.pending && !isDragging.current) return;\n\n        const pos = pointer(e);\n        pushHistory(pos);\n\n        if (!isDragging.current) {\n            if (Math.abs(pos - drag.current.startPointer) < DRAG_THRESHOLD) return;\n            beginDrag(e);\n        }\n\n        e.preventDefault();\n        setDragPos(drag.current.startPos + pos - drag.current.startPointer);\n    };\n\n    const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n        if (!drag.current.pending && !isDragging.current) return;\n\n        const dragged = isDragging.current;\n        drag.current.pending = false;\n        isDragging.current = false;\n\n        if (!dragged) {\n            drag.current.history = [];\n            return;\n        }\n\n        e.preventDefault();\n        pushHistory(pointer(e));\n        finishDrag(e);\n    };\n\n    const mask = isHorizontal\n        ? \"mask-[linear-gradient(to_right,transparent_0,black_64px,black_calc(100%-64px),transparent_100%)] md:mask-[linear-gradient(to_right,transparent_0,black_128px,black_calc(100%-128px),transparent_100%)]\"\n        : \"mask-[linear-gradient(to_bottom,transparent_0,black_64px,black_calc(100%-64px),transparent_100%)] md:mask-[linear-gradient(to_bottom,transparent_0,black_128px,black_calc(100%-128px),transparent_100%)]\";\n\n    return (\n        <div\n            ref={rootRef}\n            className={cn(\"relative flex overflow-hidden\", mask, className)}\n            onMouseEnter={() => {\n                isHovered.current = true;\n                if (!isDragging.current) speedMultiplier.set(hoverSpeed);\n            }}\n            onMouseLeave={() => {\n                isHovered.current = false;\n                if (!isDragging.current) speedMultiplier.set(1);\n            }}>\n            <motion.div\n                ref={trackRef}\n                style={isHorizontal ? { x: position } : { y: position }}\n                className={cn(\n                    \"flex cursor-grab touch-pan-y active:cursor-grabbing\",\n                    isHorizontal ? \"w-max touch-pan-y flex-row\" : \"w-full h-max touch-pan-x flex-col\",\n                )}\n                onPointerDown={onPointerDown}\n                onPointerMove={onPointerMove}\n                onPointerUp={onPointerUp}\n                onPointerCancel={onPointerUp}>\n                {Array.from({ length: copyCount }, (_, copy) => (\n                    <div\n                        key={copy}\n                        ref={copy === 0 ? firstCopyRef : undefined}\n                        aria-hidden={copy > 0}\n                        className={cn(\"flex shrink-0\", isHorizontal ? \"flex-row\" : \"w-full flex-col\")}>\n                        {items.map((child, i) => (\n                            <div key={`${copy}-${i}`} className={cn(\"shrink-0\", !isHorizontal && \"w-full\")}>\n                                {child}\n                            </div>\n                        ))}\n                    </div>\n                ))}\n            </motion.div>\n        </div>\n    );\n};\n"
    }
  ],
  "mcp": {
    "description": "Provides refined UI interactions and accessible controls. A smooth, physics-driven infinite scroller featuring drag momentum and scroll acceleration. Configurable props include: {; children: ReactNode;; shiftHalf?: boolean;; direction?: \"left\" | \"right\" | \"up\" | \"down\";. Relies on motion. Optimal for building reusable interface elements and user input flows.",
    "tags": [
      "component",
      "motion",
      "animation"
    ]
  }
}