{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "specials-linear-carousel",
  "type": "registry:component",
  "title": "Linear Carousel",
  "description": "A stunning, smooth-scrolling 3D carousel inspired by Linear.",
  "dependencies": [
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/animations/specials/linear-carousel.tsx",
      "content": "\"use client\";\n\nimport React, {\n    useEffect,\n    useRef,\n    useState,\n    createContext,\n    useContext,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion } from \"motion/react\";\nimport type { ImgHTMLAttributes } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\ninterface CarouselProps {\n    items: React.JSX.Element[];\n    initialScroll?: number;\n}\n\ntype Card = {\n    src: string;\n    title: string;\n    category?: string;\n    content: React.ReactNode;\n};\n\nexport const CarouselContext = createContext<{\n    onCardClose: (index: number) => void;\n    currentIndex: number;\n}>({\n    onCardClose: () => { },\n    currentIndex: 0,\n});\n\nexport const Carousel = ({\n    items,\n    initialScroll = 0,\n    autoplay = false,\n    autoplaySpeed = 0.5,\n}: CarouselProps & { autoplay?: boolean; autoplaySpeed?: number }) => {\n    const carouselRef = React.useRef<HTMLDivElement>(null);\n    const [canScrollLeft, setCanScrollLeft] = React.useState(false);\n    const [canScrollRight, setCanScrollRight] = React.useState(true);\n    const [currentIndex, setCurrentIndex] = useState(0);\n    const [isHovered, setIsHovered] = useState(false);\n    const animationRef = useRef<number>(null);\n    // Duplicate items to create infinite effect\n    const loopedItems = [\n        ...items,\n        ...items.map((item) => React.cloneElement(item, { key: item.key + \"-duplicate\", index: items.indexOf(item) + items.length }))\n    ];\n\n    useEffect(() => {\n        if (carouselRef.current) {\n            carouselRef.current.scrollLeft = initialScroll;\n            checkScrollability();\n        }\n    }, [initialScroll]);\n\n    // Auto-scroll logic\n    useEffect(() => {\n        if (!autoplay || isHovered) {\n            if (animationRef.current) cancelAnimationFrame(animationRef.current!);\n            return;\n        }\n\n        const scroll = () => {\n            if (carouselRef.current) {\n                // Scroll by speed\n                carouselRef.current.scrollLeft += autoplaySpeed;\n\n                const scrollWidth = carouselRef.current.scrollWidth;\n\n                if (carouselRef.current.scrollLeft >= scrollWidth / 2) {\n                    carouselRef.current.scrollLeft = 0;\n                }\n\n                checkScrollability();\n                animationRef.current = requestAnimationFrame(scroll);\n            }\n        };\n\n        animationRef.current = requestAnimationFrame(scroll);\n\n        return () => {\n            if (animationRef.current) cancelAnimationFrame(animationRef.current);\n        };\n    }, [autoplay, autoplaySpeed, isHovered]);\n\n    const checkScrollability = () => {\n        if (carouselRef.current) {\n            const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;\n            setCanScrollLeft(scrollLeft > 0);\n            setCanScrollRight(scrollLeft < scrollWidth - clientWidth);\n        }\n    };\n\n    const scrollLeft = () => {\n        if (carouselRef.current) {\n            carouselRef.current.scrollTo({ left: 0, behavior: \"smooth\" });\n        }\n    };\n\n    const scrollRight = () => {\n        if (carouselRef.current) {\n            const container = carouselRef.current;\n            container.scrollTo({ left: container.scrollWidth, behavior: \"smooth\" });\n        }\n    };\n\n    const handleCardClose = (index: number) => {\n        if (carouselRef.current) {\n            const cardWidth = isMobile() ? 230 : 320; // (md:w-80)\n            const gap = isMobile() ? 4 : 8;\n            const scrollPosition = (cardWidth + gap) * (index + 1);\n            carouselRef.current.scrollTo({\n                left: scrollPosition,\n                behavior: \"smooth\",\n            });\n            setCurrentIndex(index);\n        }\n    };\n\n    const isMobile = () => {\n        return window && window.innerWidth < 768;\n    };\n\n    // Drag to scroll logic\n    const [isDragging, setIsDragging] = useState(false);\n    const [startX, setStartX] = useState(0);\n    const [scrollLeftState, setScrollLeftState] = useState(0);\n\n    const handleMouseDown = (e: React.MouseEvent) => {\n        setIsDragging(true);\n        setStartX(e.pageX - (carouselRef.current?.offsetLeft || 0));\n        setScrollLeftState(carouselRef.current?.scrollLeft || 0);\n    };\n\n    const handleMouseLeave = () => {\n        setIsDragging(false);\n    };\n\n    const handleMouseUp = () => {\n        setIsDragging(false);\n    };\n\n    const handleMouseMove = (e: React.MouseEvent) => {\n        if (!isDragging) return;\n        e.preventDefault();\n        const x = e.pageX - (carouselRef.current?.offsetLeft || 0);\n        const walk = (x - startX) * 2; // Scroll-fast\n        if (carouselRef.current) {\n            carouselRef.current.scrollLeft = scrollLeftState - walk;\n        }\n    };\n\n\n    return (\n        <CarouselContext.Provider\n            value={{ onCardClose: handleCardClose, currentIndex }}\n        >\n            <div\n                className=\"relative w-full mx-auto px-4 md:px-8\"\n                onTouchStart={() => setIsHovered(true)}\n                onTouchEnd={() => setIsHovered(false)}\n            >\n                <div\n                    className={cn(\n                        \"flex w-full overflow-x-scroll overscroll-x-auto scroll-smooth py-10 [scrollbar-width:none] md:py-20 cursor-grab active:cursor-grabbing\",\n                        isDragging && \"cursor-grabbing scroll-auto\"\n                    )}\n                    ref={carouselRef}\n                    onScroll={checkScrollability}\n                    onMouseDown={handleMouseDown}\n                    onMouseLeave={handleMouseLeave}\n                    onMouseUp={handleMouseUp}\n                    onMouseMove={handleMouseMove}\n                >\n                    <div\n                        className={cn(\n                            \"absolute right-0 z-1000 h-auto w-[5%] overflow-hidden bg-gradient-to-l from-white dark:from-background to-transparent pointer-events-none\"\n                        )}\n                    ></div>\n\n                    <div\n                        className={cn(\"flex flex-row justify-start gap-4\")}\n                    >\n                        {loopedItems.map((item, index) => (\n                            <motion.div\n                                initial={{\n                                    opacity: 0,\n                                    y: 20,\n                                }}\n                                animate={{\n                                    opacity: 1,\n                                    y: 0,\n                                }}\n                                transition={{\n                                    duration: 0.5,\n                                    delay: 0.2 * (index % items.length), // Stagger only relevant to original set length ideally\n                                    ease: \"easeOut\",\n                                }}\n                                key={\"card\" + index}\n                                className=\"rounded-3xl\"\n                                onMouseEnter={() => setIsHovered(true)}\n                                onMouseLeave={() => setIsHovered(false)}\n                            >\n                                {item}\n                            </motion.div>\n                        ))}\n                    </div>\n                </div>\n                <div className=\"flex justify-center gap-3 mt-4\">\n                    <button\n                        className=\"relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-card border border-border hover:bg-muted disabled:opacity-50 transition-colors\"\n                        onClick={scrollLeft}\n                        disabled={!canScrollLeft}\n                        onMouseEnter={() => setIsHovered(true)}\n                        onMouseLeave={() => setIsHovered(false)}\n                    >\n                        <ChevronLeft className=\"h-6 w-6 text-muted-foreground\" />\n                    </button>\n                    <button\n                        className=\"relative z-40 flex h-10 w-10 items-center justify-center rounded-full bg-card border border-border hover:bg-muted disabled:opacity-50 transition-colors\"\n                        onClick={scrollRight}\n                        disabled={!canScrollRight}\n                        onMouseEnter={() => setIsHovered(true)}\n                        onMouseLeave={() => setIsHovered(false)}\n                    >\n                        <ChevronRight className=\"h-6 w-6 text-muted-foreground\" />\n                    </button>\n                </div>\n            </div>\n        </CarouselContext.Provider>\n    );\n};\n\nexport const Card = ({\n    card,\n    index,\n    layout = false,\n}: {\n    card: Card;\n    index: number;\n    layout?: boolean;\n}) => {\n    return (\n        <motion.button\n            layoutId={layout ? `card-${card.title}-${index}` : undefined}\n            className=\"relative z-10 flex h-60 w-56 flex-col items-start justify-end overflow-hidden rounded-3xl bg-gray-100 md:h-96 md:w-80 dark:bg-neutral-900\"\n        >\n            <div className=\"pointer-events-none absolute inset-x-0 bottom-0 z-30 h-2/3 bg-gradient-to-t from-black/80 via-black/40 to-transparent\" />\n            <div className=\"relative z-40 p-8 w-full\">\n                {card.category && (\n                    <motion.p\n                        layoutId={layout ? `category-${card.category}-${index}` : undefined}\n                        className=\"text-left font-mono text-sm font-medium text-white md:text-base\"\n                    >\n                        {card.category}\n                    </motion.p>\n                )}\n                <motion.p\n                    layoutId={layout ? `title-${card.title}-${index}` : undefined}\n                    className=\"mt-2 max-w-xs text-left font-mono text-xl font-semibold [text-wrap:balance] text-white md:text-3xl\"\n                >\n                    {card.title}\n                </motion.p>\n            </div>\n            <img\n                src={card.src}\n                alt={card.title}\n                className=\"absolute inset-0 z-10 w-full h-full object-cover grayscale\"\n            />\n        </motion.button>\n    );\n};\n\nexport const BlurImage = ({\n    height,\n    width,\n    src,\n    className,\n    alt,\n    ...rest\n}: ImgHTMLAttributes<HTMLImageElement> & { src: string; alt: string }) => {\n    const [isLoading, setLoading] = useState(true);\n    return (\n        <img\n            className={cn(\n                \"h-full w-full transition duration-300\",\n                isLoading ? \"blur-sm\" : \"blur-0\",\n                className\n            )}\n            onLoad={() => setLoading(false)}\n            src={src as string}\n            width={width}\n            height={height}\n            loading=\"lazy\"\n            decoding=\"async\"\n            alt={alt ? alt : \"Background of a beautiful view\"}\n            {...rest}\n        />\n    );\n};\n",
      "type": "registry:component"
    }
  ]
}