{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "carousel-01",
  "title": "Carousel 01",
  "description": "Auto-scroll carousel that pauses on item hover and reveals title.",
  "dependencies": [
    "motion",
    "class-variance-authority"
  ],
  "devDependencies": [
    "embla-carousel-auto-scroll"
  ],
  "registryDependencies": [
    "carousel"
  ],
  "files": [
    {
      "path": "registry/blocks/carousel/carousel-01/carousel-01.tsx",
      "content": "'use client'\n\nimport { cva } from 'class-variance-authority'\nimport AutoScroll from 'embla-carousel-auto-scroll'\nimport { motion } from 'motion/react'\nimport { useEffect, useRef, useState } from 'react'\n\nimport {\n  Carousel,\n  type CarouselApi,\n  CarouselContent,\n  CarouselItem,\n} from '@/components/ui/carousel'\nimport { cn } from '@/lib/utils'\n\nexport type Carousel01Aspect = 'landscape' | 'portrait' | 'wide'\n\nexport interface Carousel01Media {\n  src: string\n  aspect: Carousel01Aspect\n}\n\nexport interface Carousel01Item {\n  title: string\n  media: Carousel01Media\n}\n\nexport type Carousel01TitlePlacement = 'inside' | 'outside'\n\nexport interface Carousel01Props {\n  titlePlacement: Carousel01TitlePlacement\n  items: Carousel01Item[]\n}\n\nconst aspectVariants = cva('relative w-full overflow-hidden rounded-lg', {\n  variants: {\n    aspect: {\n      landscape: 'aspect-[17/9]',\n      portrait: 'aspect-[3/2]',\n      wide: 'aspect-[21/8]',\n    },\n  },\n  defaultVariants: {\n    aspect: 'landscape',\n  },\n})\n\nexport function Carousel01({\n  titlePlacement,\n  items,\n}: Readonly<Carousel01Props>) {\n  const [api, setApi] = useState<CarouselApi>()\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)\n  const hoverCountRef = useRef(0)\n  const resumeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  useEffect(() => {\n    if (!api) return\n    const autoScroll = api.plugins()?.autoScroll\n    if (autoScroll) {\n      autoScroll.play()\n    }\n  }, [api])\n\n  const handleItemMouseEnter = (index: number) => {\n    setHoveredIndex(index)\n    hoverCountRef.current += 1\n    if (resumeTimeoutRef.current) {\n      clearTimeout(resumeTimeoutRef.current)\n      resumeTimeoutRef.current = null\n    }\n    api?.plugins()?.autoScroll?.stop()\n  }\n\n  const handleItemMouseLeave = () => {\n    setHoveredIndex(null)\n    hoverCountRef.current -= 1\n    if (hoverCountRef.current <= 0) {\n      hoverCountRef.current = 0\n      resumeTimeoutRef.current = setTimeout(() => {\n        api?.plugins()?.autoScroll?.play()\n        resumeTimeoutRef.current = null\n      }, 50)\n    }\n  }\n\n  const titleMotionProps = {\n    initial: { y: 6, opacity: 0 },\n    transition: { duration: 0.2, ease: 'easeOut' as const },\n  }\n\n  if (!items?.length) return null\n\n  return (\n    <Carousel\n      setApi={setApi}\n      opts={{\n        align: 'center',\n        loop: true,\n        watchDrag: false,\n      }}\n      plugins={[\n        AutoScroll({\n          speed: 1,\n          startDelay: 0,\n          stopOnMouseEnter: false,\n          stopOnInteraction: false,\n        }),\n      ]}\n      className=\"w-full\"\n      aria-label=\"Carousel de medians\"\n    >\n      <CarouselContent className=\"-ml-4\">\n        {items.map((item, index) => {\n          const isHovered = hoveredIndex === index\n          const isDimmed = hoveredIndex !== null && hoveredIndex !== index\n          if (titlePlacement === 'outside') {\n            return (\n              <CarouselItem\n                key={`${item.title}-${index}`}\n                className=\"basis-full pl-4 md:basis-[36%]\"\n              >\n                <button\n                  type=\"button\"\n                  aria-label={item.title}\n                  className={cn(\n                    'flex w-full cursor-default flex-col gap-1 border-0 bg-transparent p-0 text-left transition-opacity duration-300',\n                    isDimmed && 'opacity-50',\n                  )}\n                  onMouseEnter={() => handleItemMouseEnter(index)}\n                  onMouseLeave={handleItemMouseLeave}\n                  onFocus={() => handleItemMouseEnter(index)}\n                  onBlur={handleItemMouseLeave}\n                >\n                  <motion.span\n                    className=\"text-foreground min-h-[20px] text-sm font-medium\"\n                    {...titleMotionProps}\n                    animate={\n                      isHovered ? { y: 0, opacity: 1 } : { y: 6, opacity: 0 }\n                    }\n                    transition={{ duration: 0.2, ease: 'easeInOut' }}\n                  >\n                    {item.title}\n                  </motion.span>\n                  <span\n                    className={cn(\n                      'relative w-full',\n                      aspectVariants({ aspect: item.media.aspect }),\n                    )}\n                  >\n                    <img\n                      src={item.media.src}\n                      alt=\"\"\n                      loading=\"lazy\"\n                      decoding=\"async\"\n                      className=\"pointer-events-none absolute inset-0 size-full object-cover\"\n                    />\n                  </span>\n                </button>\n              </CarouselItem>\n            )\n          }\n\n          return (\n            <CarouselItem\n              key={`${item.title}-${index}`}\n              className=\"basis-full pl-4 md:basis-[36%]\"\n            >\n              <button\n                type=\"button\"\n                aria-label={item.title}\n                className={cn(\n                  aspectVariants({ aspect: item.media.aspect }),\n                  'block w-full cursor-default border-0 bg-transparent p-0 text-left transition-opacity duration-300',\n                  isDimmed && 'opacity-50',\n                )}\n                onMouseEnter={() => handleItemMouseEnter(index)}\n                onMouseLeave={handleItemMouseLeave}\n                onFocus={() => handleItemMouseEnter(index)}\n                onBlur={handleItemMouseLeave}\n              >\n                <img\n                  src={item.media.src}\n                  alt=\"\"\n                  loading=\"lazy\"\n                  decoding=\"async\"\n                  className=\"pointer-events-none absolute inset-0 size-full object-cover\"\n                />\n                <motion.span\n                  className=\"absolute right-0 bottom-0 left-0 bg-gradient-to-t from-black/50 to-transparent px-3 py-2.5 text-sm font-medium text-white\"\n                  {...titleMotionProps}\n                  transition={{ duration: 0.2, ease: 'easeInOut' }}\n                  initial={{ y: 8, opacity: 0 }}\n                  animate={\n                    isHovered ? { y: 0, opacity: 1 } : { y: 8, opacity: 0 }\n                  }\n                >\n                  {item.title}\n                </motion.span>\n              </button>\n            </CarouselItem>\n          )\n        })}\n      </CarouselContent>\n    </Carousel>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/flx/blocks/carousel/carousel-01/carousel-01.tsx"
    },
    {
      "path": "registry/blocks/carousel/carousel-01/carousel-01-example.tsx",
      "content": "import { Carousel01, type Carousel01Props } from './carousel-01'\n\nexport const values = {\n  titlePlacement: 'outside',\n  items: [\n    {\n      title: 'Ship faster',\n      media: {\n        src: 'https://images.unsplash.com/photo-1610611742876-97e4d834d077?q=80&w=1170&auto=format&fit=crop',\n        aspect: 'landscape',\n      },\n    },\n    {\n      title: 'Performance first',\n      media: {\n        src: 'https://images.unsplash.com/photo-1688327009265-3e47cdab9dc4?q=80&w=1169&auto=format&fit=crop',\n        aspect: 'portrait' as const,\n      },\n    },\n    {\n      title: 'Stay in control',\n      media: {\n        src: 'https://images.unsplash.com/photo-1610210162763-6c4d6da47c8f?q=80&w=1170&auto=format&fit=crop',\n        aspect: 'landscape' as const,\n      },\n    },\n    {\n      title: 'Built together',\n      media: {\n        src: 'https://images.unsplash.com/photo-1672917765736-c1c397a5d37f?q=80&w=1170&auto=format&fit=crop',\n        aspect: 'portrait' as const,\n      },\n    },\n  ],\n} satisfies Carousel01Props\n\nexport function Carousel01Example() {\n  return (\n    <Carousel01\n      titlePlacement={values.titlePlacement}\n      items={values.items}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/flx/blocks/carousel/carousel-01/carousel-01-example.tsx"
    }
  ],
  "meta": {
    "iframeHeight": 500,
    "containerClassName": "max-w-full overflow-hidden px-0"
  },
  "type": "registry:block"
}