{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "squircle-carousel",
  "title": "Squircle Carousel",
  "author": "AlgorizeTech",
  "description": "Horizontal flowing carousel where images are masked behind a barrel-distorted grid of rounded cubes with snap physics and smooth inertia.",
  "dependencies": [
    "next-themes"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/squircle-carousel.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useRef, useEffect, useCallback } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SquircleCarouselItem {\n    id: string | number;\n    title?: string;\n    imageUrl: string;\n}\n\nexport interface SquircleCarouselProps {\n    /** List of carousel image items */\n    items?: SquircleCarouselItem[];\n    /** Grid rows count (default: 4) */\n    rows?: number;\n    /** Tile gap in pixels (default: 8) */\n    gridGap?: number;\n    /** Edge fade overlay width percentage (0-50, default: 36) */\n    fadeWidthPercent?: number;\n    /** Show outer header shell (default: false) */\n    showHeaderShell?: boolean;\n    /** Header title */\n    title?: string;\n    /** Category tag */\n    categoryTag?: string;\n    /** Subtitle description */\n    description?: string;\n    /** Tags list */\n    tags?: string[];\n    /** Preview button text */\n    previewText?: string;\n    /** CTA button text */\n    ctaText?: string;\n    /** Footer caption text */\n    footerCaption?: string;\n    /** Custom class name */\n    className?: string;\n}\n\nconst DEFAULT_ITEMS: SquircleCarouselItem[] = [\n    {\n        id: \"1\",\n        title: \"Misty Pine Forest\",\n        imageUrl: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146209/photo-1470071459604-3b5ec3a7fe05-1790146212205.jpg\",\n    },\n    {\n        id: \"2\",\n        title: \"Alpine Glacier Lake\",\n        imageUrl: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145141/photo-1464822759023-fed622ff2c3b-1790145143491.jpg\",\n    },\n    {\n        id: \"3\",\n        title: \"Sunset Mountain Range\",\n        imageUrl: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790146210/photo-1506744038136-46273834b3fb-1790146212208.jpg\",\n    },\n    {\n        id: \"4\",\n        title: \"Lush River Valley\",\n        imageUrl: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145140/photo-1426604966848-d7adac402bff-1790145143490.jpg\",\n    },\n    {\n        id: \"5\",\n        title: \"Autumn Woodland Path\",\n        imageUrl: \"https://res.cloudinary.com/dodndg7ai/image/upload/f_webp,q_auto,w_1200,c_limit/v1790145141/photo-1448375240586-882707db888b-1790145143491.jpg\",\n    },\n];\n\nconst DEFAULT_TAGS = [\n    \"carousel\",\n    \"squircle\",\n    \"grid\",\n    \"mask\",\n    \"nature\",\n    \"drag\",\n    \"inertia\",\n];\n\nexport function SquircleCarousel({\n    items = DEFAULT_ITEMS,\n    rows = 4,\n    gridGap = 8,\n    fadeWidthPercent = 36,\n    showHeaderShell = false,\n    title = \"Squircle Carousel\",\n    categoryTag = \"⟲ CAROUSELS\",\n    description = \"Horizontal flowing carousel where images are masked behind a grid of rounded cubes with snap physics and smooth inertia.\",\n    tags = DEFAULT_TAGS,\n    previewText = \"PREVIEW\",\n    ctaText = \"GET IT FOR $10.00\",\n    footerCaption = \"Drag to explore; snaps between images\",\n    className,\n}: SquircleCarouselProps) {\n    const { resolvedTheme } = useTheme();\n    const isDark = resolvedTheme === \"dark\";\n\n    const containerRef = useRef<HTMLDivElement | null>(null);\n    const canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n    const [isDragging, setIsDragging] = useState(false);\n    const [loadedImages, setLoadedImages] = useState<HTMLImageElement[]>([]);\n\n    // Drag Physics State\n    const dragRef = useRef({\n        startX: 0,\n        scrollPos: 0,\n        targetPos: 0,\n        velocity: 0,\n        lastX: 0,\n        lastTime: 0,\n        isDown: false,\n    });\n\n    // Preload images into HTMLImageElement instances\n    useEffect(() => {\n        let isMounted = true;\n        const imagePromises = items.map((item) => {\n            return new Promise<HTMLImageElement>((resolve) => {\n                const img = new Image();\n                img.crossOrigin = \"anonymous\";\n                img.src = item.imageUrl;\n                img.onload = () => resolve(img);\n                img.onerror = () => resolve(img);\n            });\n        });\n\n        Promise.all(imagePromises).then((imgs) => {\n            if (isMounted) {\n                setLoadedImages(imgs);\n            }\n        });\n\n        return () => {\n            isMounted = false;\n        };\n    }, [items]);\n\n    // Canvas Render Loop\n    const renderCanvas = useCallback(() => {\n        const canvas = canvasRef.current;\n        if (!canvas) return;\n        const ctx = canvas.getContext(\"2d\");\n        if (!ctx) return;\n\n        const width = canvas.width;\n        const height = canvas.height;\n\n        let bgStyle: string;\n        const isDarkTheme = isDark;\n\n        if (containerRef.current) {\n            const computedBg = getComputedStyle(containerRef.current).backgroundColor;\n            if (computedBg && computedBg !== \"rgba(0, 0, 0, 0)\" && computedBg !== \"transparent\") {\n                bgStyle = computedBg;\n            } else {\n                bgStyle = isDarkTheme ? \"#09090b\" : \"#ffffff\";\n            }\n        } else {\n            bgStyle = isDarkTheme ? \"#09090b\" : \"#ffffff\";\n        }\n\n        ctx.fillStyle = bgStyle;\n        ctx.fillRect(0, 0, width, height);\n\n        if (loadedImages.length === 0) return;\n\n        const totalItems = loadedImages.length;\n        const currentPos = dragRef.current.scrollPos;\n\n        const isMobile = width < 640;\n\n        const gap = isMobile ? Math.max(4, gridGap - 2) : gridGap;\n        const padY = isMobile ? 12 : 24;\n        const padX = isMobile ? 8 : 16;\n        const availW = width - padX * 2;\n        const availH = height - padY * 2;\n        const stageCenterX = width / 2;\n\n        const cardCols = isMobile ? 4 : 6;\n        const cellW = Math.round((availW * (isMobile ? 0.82 : 0.68)) / cardCols);\n        const tileStride = cellW + gap;\n        const cellH = (availH - (rows - 1) * gap) / rows;\n        const cornerRadius = isMobile ? 10 : 14;\n\n        ctx.save();\n\n        const colsHalfStage = Math.ceil((stageCenterX + 200) / tileStride);\n        const centerColIdx = Math.floor(currentPos / tileStride);\n        const startGlobalCol = centerColIdx - colsHalfStage;\n        const endGlobalCol = centerColIdx + colsHalfStage;\n\n        for (let r = 0; r < rows; r++) {\n            const cellY = padY + r * (cellH + gap);\n\n            for (let globalCol = startGlobalCol; globalCol <= endGlobalCol; globalCol++) {\n                const cellX = globalCol * tileStride - currentPos + stageCenterX - cellW / 2;\n\n                if (cellX + cellW < -60 || cellX > width + 60) continue;\n\n                const normalizedColIdx = ((globalCol % (cardCols * totalItems)) + (cardCols * totalItems)) % (cardCols * totalItems);\n                const cardIdx = Math.floor(normalizedColIdx / cardCols);\n                const colInCard = normalizedColIdx % cardCols;\n\n                const img = loadedImages[cardIdx];\n\n                ctx.save();\n\n                ctx.beginPath();\n                if (ctx.roundRect) {\n                    ctx.roundRect(cellX, cellY, cellW, cellH, cornerRadius);\n                } else {\n                    ctx.rect(cellX, cellY, cellW, cellH);\n                }\n                ctx.clip();\n\n                if (img && img.complete && img.naturalWidth > 0) {\n                    const imgWidthSpan = cardCols * tileStride - gap;\n                    const scaleFactor = Math.max(imgWidthSpan / img.naturalWidth, availH / img.naturalHeight);\n                    const fullImgW = img.naturalWidth * scaleFactor;\n                    const fullImgH = img.naturalHeight * scaleFactor;\n\n                    const cardOriginX = (globalCol - colInCard) * tileStride - currentPos + stageCenterX - cellW / 2;\n                    const drawX = cardOriginX + (imgWidthSpan - fullImgW) / 2;\n                    const drawY = padY + (availH - fullImgH) / 2;\n\n                    ctx.drawImage(img, drawX, drawY, fullImgW, fullImgH);\n                }\n\n                ctx.restore();\n            }\n        }\n\n        const fadeWidth = width * (Math.min(50, Math.max(0, fadeWidthPercent)) / 100);\n\n        if (fadeWidth > 0) {\n            const leftGrad = ctx.createLinearGradient(0, 0, fadeWidth, 0);\n            const rightGrad = ctx.createLinearGradient(width - fadeWidth, 0, width, 0);\n\n            if (isDarkTheme) {\n                leftGrad.addColorStop(0, \"rgba(9, 9, 11, 1.0)\");\n                leftGrad.addColorStop(0.5, \"rgba(9, 9, 11, 0.78)\");\n                leftGrad.addColorStop(1, \"rgba(9, 9, 11, 0)\");\n\n                rightGrad.addColorStop(0, \"rgba(9, 9, 11, 0)\");\n                rightGrad.addColorStop(0.5, \"rgba(9, 9, 11, 0.78)\");\n                rightGrad.addColorStop(1, \"rgba(9, 9, 11, 1.0)\");\n            } else {\n                leftGrad.addColorStop(0, \"rgba(255, 255, 255, 1.0)\");\n                leftGrad.addColorStop(0.5, \"rgba(255, 255, 255, 0.82)\");\n                leftGrad.addColorStop(1, \"rgba(255, 255, 255, 0)\");\n\n                rightGrad.addColorStop(0, \"rgba(255, 255, 255, 0)\");\n                rightGrad.addColorStop(0.5, \"rgba(255, 255, 255, 0.82)\");\n                rightGrad.addColorStop(1, \"rgba(255, 255, 255, 1.0)\");\n            }\n\n            ctx.fillStyle = leftGrad;\n            ctx.fillRect(0, 0, fadeWidth, height);\n\n            ctx.fillStyle = rightGrad;\n            ctx.fillRect(width - fadeWidth, 0, fadeWidth, height);\n        }\n\n        ctx.restore();\n    }, [rows, gridGap, fadeWidthPercent, loadedImages, isDark]);\n\n    // Drag Physics Animation Loop\n    useEffect(() => {\n        let animId: number;\n\n        const updatePhysics = () => {\n            const drag = dragRef.current;\n            const container = containerRef.current;\n            if (container) {\n                const w = container.clientWidth;\n                const h = container.clientHeight;\n                if (canvasRef.current && (canvasRef.current.width !== w || canvasRef.current.height !== h)) {\n                    canvasRef.current.width = w;\n                    canvasRef.current.height = h;\n                }\n            }\n\n            if (!drag.isDown) {\n                drag.velocity *= 0.92;\n                drag.targetPos += drag.velocity;\n                drag.scrollPos += (drag.targetPos - drag.scrollPos) * 0.18;\n            } else {\n                drag.scrollPos += (drag.targetPos - drag.scrollPos) * 0.35;\n            }\n\n            renderCanvas();\n            animId = requestAnimationFrame(updatePhysics);\n        };\n\n        updatePhysics();\n\n        return () => {\n            cancelAnimationFrame(animId);\n        };\n    }, [renderCanvas]);\n\n    // Pointer Event Handlers for Mouse Drag\n    const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n        const drag = dragRef.current;\n        drag.isDown = true;\n        drag.startX = e.clientX;\n        drag.lastX = e.clientX;\n        drag.lastTime = performance.now();\n        drag.velocity = 0;\n        setIsDragging(true);\n        e.currentTarget.setPointerCapture(e.pointerId);\n    };\n\n    const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n        const drag = dragRef.current;\n        if (!drag.isDown) return;\n\n        const now = performance.now();\n        const dx = e.clientX - drag.lastX;\n        const dt = Math.max(1, now - drag.lastTime);\n\n        drag.targetPos -= dx * 1.4;\n        drag.velocity = (-dx / dt) * 14;\n\n        drag.lastX = e.clientX;\n        drag.lastTime = now;\n    };\n\n    const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n        const drag = dragRef.current;\n        drag.isDown = false;\n        setIsDragging(false);\n\n        try {\n            e.currentTarget.releasePointerCapture(e.pointerId);\n        } catch {\n            // Ignore pointer capture release error if already lost\n        }\n    };\n\n    const stageContent = (\n        <div\n            ref={containerRef}\n            onPointerDown={handlePointerDown}\n            onPointerMove={handlePointerMove}\n            onPointerUp={handlePointerUp}\n            onPointerCancel={handlePointerUp}\n            className={cn(\n                \"relative w-full h-[500px] rounded-[24px] overflow-hidden border border-border bg-background text-foreground select-none transition-all\",\n                isDragging ? \"cursor-grabbing\" : \"cursor-grab\",\n                className\n            )}\n        >\n            <canvas\n                ref={canvasRef}\n                className=\"absolute inset-0 w-full h-full object-cover pointer-events-none\"\n            />\n        </div>\n    );\n\n    if (!showHeaderShell) {\n        return stageContent;\n    }\n\n    return (\n        <div className=\"w-full space-y-4 font-sans text-foreground select-none\">\n            {/* Header Metadata Shell */}\n            <div className=\"space-y-3\">\n                <div className=\"flex items-center gap-3\">\n                    <h2 className=\"text-2xl sm:text-3xl font-extrabold tracking-tight text-foreground\">\n                        {title}\n                    </h2>\n                    {categoryTag && (\n                        <span className=\"px-2.5 py-0.5 rounded-full text-[10px] font-extrabold tracking-wider uppercase bg-primary/10 text-primary border border-primary/20\">\n                            {categoryTag}\n                        </span>\n                    )}\n                </div>\n\n                {description && (\n                    <p className=\"text-xs sm:text-sm font-normal text-muted-foreground max-w-2xl leading-relaxed\">\n                        {description}\n                    </p>\n                )}\n\n                {/* Badge tags */}\n                {tags && tags.length > 0 && (\n                    <div className=\"flex items-center gap-1.5 flex-wrap pt-0.5\">\n                        {tags.map((tag, idx) => (\n                            <span\n                                key={idx}\n                                className=\"px-2.5 py-0.5 rounded-full text-[11px] font-mono font-medium border bg-muted/60 text-muted-foreground border-border\"\n                            >\n                                {tag}\n                            </span>\n                        ))}\n                    </div>\n                )}\n\n                {/* Action Buttons */}\n                <div className=\"flex items-center gap-2.5 pt-2\">\n                    {previewText && (\n                        <button\n                            type=\"button\"\n                            className=\"px-4 py-1.5 rounded-lg text-xs font-semibold tracking-wider text-primary bg-primary/10 hover:bg-primary/20 transition-colors uppercase cursor-pointer\"\n                        >\n                            {previewText}\n                        </button>\n                    )}\n\n                    {ctaText && (\n                        <button\n                            type=\"button\"\n                            className=\"px-4 py-1.5 rounded-lg text-xs font-semibold tracking-wider text-primary-foreground bg-primary hover:bg-primary/90 shadow-sm transition-all cursor-pointer flex items-center gap-1.5\"\n                        >\n                            <span>🔒</span>\n                            <span>{ctaText}</span>\n                        </button>\n                    )}\n                </div>\n            </div>\n\n            {/* Main Stage */}\n            {stageContent}\n\n            {/* Footer Caption */}\n            {footerCaption && (\n                <p className=\"text-xs font-mono tracking-wide text-muted-foreground pt-1\">\n                    {footerCaption}\n                </p>\n            )}\n        </div>\n    );\n}\n\nexport default SquircleCarousel;\n",
      "type": "registry:component",
      "target": "components/custom/squircle-carousel.tsx"
    }
  ],
  "docs": "Named export: SquircleCarousel. Interactive drag with grab cursor, momentum physics, barrel fisheye distortion, and squircle grid masking.",
  "categories": [
    "media",
    "scroll"
  ],
  "type": "registry:component"
}
