{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"infinite-gallery","type":"registry:component","title":"Infinite Gallery","description":"A gallery that loops forever.","meta":{"pro":false},"docs":"Usage: https://atelier-ui.com/r/infinite-gallery.md\nDocs: https://atelier-ui.com/docs/components/scroll/infinite-gallery","dependencies":[],"registryDependencies":["https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/infinite-gallery/infinite-gallery.tsx","type":"registry:component","target":"components/infinite-gallery/infinite-gallery.tsx","content":"\"use client\"\n\nimport {\n    Children,\n    type ComponentRef,\n    isValidElement,\n    type ReactNode,\n    useCallback,\n    useEffect,\n    useRef,\n} from \"react\"\nimport { useFrameLoop } from \"../../hooks/use-frame-loop\"\n\nconst DEFAULT_SCROLL_STATE = {\n    position: 0,\n    target: 0,\n    momentum: 0,\n    prev: 0,\n}\n\nexport type InfiniteGalleryProps = {\n    children: ReactNode\n    perView?: number\n    gap?: number\n    speed?: number\n    inertia?: number\n    dragMultiplier?: number\n    className?: string\n}\n\nfunction lerp(start: number, end: number, factor: number) {\n    return start + (end - start) * factor\n}\n\nfunction clamp(min: number, max: number, value: number) {\n    return Math.min(max, Math.max(min, value))\n}\n\nexport function InfiniteGallery({\n    children,\n    perView = 4,\n    gap = 5,\n    className,\n    speed = 6,\n    inertia = 0.6,\n    dragMultiplier = 3,\n}: InfiniteGalleryProps) {\n    const sourceItems = Children.toArray(children).filter(isValidElement)\n    const repeats = sourceItems.length ? Math.ceil((perView + 2) / sourceItems.length) : 0\n    const items = Array.from({ length: repeats }, () => sourceItems).flat()\n\n    const containerRef = useRef<ComponentRef<\"div\">>(null)\n    const measureRef = useRef({ containerWidth: 0, itemWidth: 0 })\n    const scrollRef = useRef({ ...DEFAULT_SCROLL_STATE }).current\n\n    const calcItemsPositions = useCallback(\n        (scrollOffset: number, velocity: number) => {\n            const container = containerRef.current\n            const { containerWidth, itemWidth } = measureRef.current\n\n            if (!container) return\n\n            const totalWidth = items.length * itemWidth\n\n            for (let i = 0; i < items.length; i++) {\n                const itemElement = container.children[i] as HTMLElement\n                if (!itemElement) return\n\n                const totalOffset = i * itemWidth + scrollOffset\n\n                let xPos = ((totalOffset % totalWidth) + totalWidth) % totalWidth\n\n                if (xPos > totalWidth - itemWidth) xPos -= totalWidth\n\n                const itemInView = xPos > -itemWidth && xPos < containerWidth\n\n                if (itemInView) {\n                    const maxDeg = 85\n                    const rotate = itemWidth\n                        ? clamp(-maxDeg, maxDeg, (velocity / itemWidth) * 6)\n                        : 0\n                    const transform = `translate3d(${xPos - i * itemWidth}px, 0, 0) perspective(800px) rotateY(${rotate}deg)`\n\n                    itemElement.style.transform = transform\n                    itemElement.style.visibility = \"visible\"\n                } else {\n                    itemElement.style.visibility = \"hidden\"\n                }\n            }\n        },\n        [items],\n    )\n\n    const measure = useCallback(() => {\n        const container = containerRef.current\n        if (!container) return\n        if (!container.children.length) return\n\n        const firstChild = container.children[0] as HTMLElement\n        const computedGap = parseFloat(getComputedStyle(container).gap) || 0\n\n        measureRef.current = {\n            containerWidth: container.getBoundingClientRect().width,\n            itemWidth: firstChild.offsetWidth + computedGap,\n        }\n    }, [])\n\n    useFrameLoop((_, delta) => {\n        scrollRef.target += scrollRef.momentum\n        scrollRef.momentum *= inertia\n\n        const pos = scrollRef.position\n        const target = scrollRef.target\n\n        if (Math.abs(target - pos) > 0.01 || Math.abs(scrollRef.momentum) > 0.01) {\n            const factor = 1 - Math.exp(-speed * delta)\n            const next = lerp(pos, target, factor)\n            scrollRef.position = next\n\n            const frameDelta = (next - scrollRef.prev) * 60\n            scrollRef.prev = next\n\n            calcItemsPositions(next, frameDelta)\n        }\n    })\n\n    useEffect(() => {\n        const container = containerRef.current\n        if (!container) return\n\n        const resetScroll = () => {\n            Object.assign(scrollRef, DEFAULT_SCROLL_STATE)\n            measure()\n            calcItemsPositions(0, 0)\n        }\n\n        resetScroll()\n\n        const onWheel = (event: WheelEvent) => {\n            event.preventDefault()\n            scrollRef.momentum -= event.deltaY\n        }\n\n        const dragState = {\n            startX: 0,\n            isDragging: false,\n            start: 0,\n        }\n\n        const onPointerDown = (event: PointerEvent) => {\n            dragState.isDragging = true\n            dragState.startX = event.clientX\n            dragState.start = scrollRef.target\n            scrollRef.momentum = 0\n            container.setPointerCapture(event.pointerId)\n        }\n\n        const onPointerMove = (event: PointerEvent) => {\n            if (!dragState.isDragging) return\n            const delta = (event.clientX - dragState.startX) * dragMultiplier\n            scrollRef.target = dragState.start + delta\n        }\n\n        const onPointerUp = () => {\n            dragState.isDragging = false\n        }\n\n        const resizeObserver = new ResizeObserver(resetScroll)\n        resizeObserver.observe(container)\n\n        container.addEventListener(\"pointerdown\", onPointerDown)\n        container.addEventListener(\"pointermove\", onPointerMove)\n        container.addEventListener(\"pointerup\", onPointerUp)\n        container.addEventListener(\"wheel\", onWheel, { passive: false })\n\n        return () => {\n            container.removeEventListener(\"pointerdown\", onPointerDown)\n            container.removeEventListener(\"pointermove\", onPointerMove)\n            container.removeEventListener(\"pointerup\", onPointerUp)\n            container.removeEventListener(\"wheel\", onWheel)\n            resizeObserver.disconnect()\n        }\n    }, [calcItemsPositions, perView, scrollRef, inertia, speed, dragMultiplier, measure])\n\n    return (\n        <div\n            ref={containerRef}\n            data-lenis-prevent\n            style={{ gap: `${gap}px` }}\n            className={`overflow-hidden touch-pan-y user-select-none ${className}`}\n        >\n            {items.map((item, index) => (\n                <div\n                    key={index}\n                    className=\"shrink-0 will-change-transform\"\n                    style={{ width: `calc(${100 / perView}%)` }}\n                >\n                    {item}\n                </div>\n            ))}\n        </div>\n    )\n}\n"},{"path":"registry/hooks/use-frame-loop.ts","type":"registry:hook","target":"hooks/use-frame-loop.ts","content":"import { useEffect, useRef } from \"react\"\n\nconst DELTA_MAX = 0.1\n\ntype FrameLoopCallback = (time: number, delta: number) => void\n\nexport function useFrameLoop(callback: FrameLoopCallback, interval?: number) {\n    const ref = useRef(callback)\n    ref.current = callback\n\n    useEffect(() => {\n        let frameId = 0\n        let lastTime = 0\n        let lastTick = 0\n\n        const tick = (now: number) => {\n            frameId = requestAnimationFrame(tick)\n\n            if (interval && now - lastTick < interval) return\n            if (interval) lastTick = now\n\n            const time = now * 0.001\n            const delta = lastTime ? Math.min(time - lastTime, DELTA_MAX) : 0\n            lastTime = time\n\n            ref.current(time, delta)\n        }\n\n        frameId = requestAnimationFrame(tick)\n\n        return () => {\n            cancelAnimationFrame(frameId)\n        }\n    }, [interval])\n}\n"}]}