{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "diagonal-carousel",
  "type": "registry:ui",
  "dependencies": [
    "framer-motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/ui/diagonal-carousel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, type Transition } from \"framer-motion\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DiagonalCarouselItem {\n  src: string;\n  title: string;\n  alt?: string;\n}\n\nexport interface DiagonalCarouselProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  items: DiagonalCarouselItem[];\n  activeIndex?: number;\n  defaultActiveIndex?: number;\n  onActiveIndexChange?: (index: number) => void;\n  loop?: boolean;\n  slideSize?: number;\n  rotationStep?: number;\n  verticalStep?: number;\n  inactiveScale?: number;\n  transition?: Transition;\n  showControls?: boolean;\n  showDots?: boolean;\n  viewportClassName?: string;\n  slideClassName?: string;\n  imageClassName?: string;\n  labelClassName?: string;\n  controlsClassName?: string;\n}\n\nconst DEFAULT_TRANSITION: Transition = {\n  type: \"spring\",\n  bounce: 0.16,\n  duration: 0.85,\n};\n\nconst clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);\n\nexport function DiagonalCarousel({\n  items,\n  activeIndex,\n  defaultActiveIndex = 0,\n  onActiveIndexChange,\n  loop = false,\n  slideSize = 260,\n  rotationStep = 30,\n  verticalStep = 120,\n  inactiveScale = 0.6,\n  transition = DEFAULT_TRANSITION,\n  showControls = true,\n  showDots = true,\n  viewportClassName,\n  slideClassName,\n  imageClassName,\n  labelClassName,\n  controlsClassName,\n  className,\n  onKeyDown,\n  tabIndex,\n  ...props\n}: DiagonalCarouselProps) {\n  const maxIndex = Math.max(0, items.length - 1);\n  const [uncontrolledIndex, setUncontrolledIndex] = React.useState(() =>\n    clamp(defaultActiveIndex, 0, maxIndex)\n  );\n  const currentIndex = clamp(activeIndex ?? uncontrolledIndex, 0, maxIndex);\n  const safeSlideSize = Math.max(120, slideSize);\n  const safeInactiveScale = clamp(inactiveScale, 0.35, 1);\n\n  const selectSlide = React.useCallback(\n    (nextIndex: number) => {\n      if (!items.length) {\n        return;\n      }\n\n      const resolvedIndex = loop\n        ? (nextIndex + items.length) % items.length\n        : clamp(nextIndex, 0, maxIndex);\n\n      if (activeIndex === undefined) {\n        setUncontrolledIndex(resolvedIndex);\n      }\n\n      onActiveIndexChange?.(resolvedIndex);\n    },\n    [activeIndex, items.length, loop, maxIndex, onActiveIndexChange]\n  );\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n\n    if (event.defaultPrevented) {\n      return;\n    }\n\n    if (event.key === \"ArrowLeft\") {\n      event.preventDefault();\n      selectSlide(currentIndex - 1);\n    }\n\n    if (event.key === \"ArrowRight\") {\n      event.preventDefault();\n      selectSlide(currentIndex + 1);\n    }\n  };\n\n  if (!items.length) {\n    return null;\n  }\n\n  const isPreviousDisabled = !loop && currentIndex === 0;\n  const isNextDisabled = !loop && currentIndex === maxIndex;\n\n  return (\n    <div\n      role=\"region\"\n      aria-roledescription=\"carousel\"\n      aria-label=\"Diagonal image carousel\"\n      tabIndex={tabIndex ?? 0}\n      onKeyDown={handleKeyDown}\n      className={cn(\"relative isolate h-full w-full overflow-hidden\", className)}\n      {...props}\n    >\n      <div className={cn(\"absolute inset-0 overflow-hidden\", viewportClassName)}>\n        <motion.div\n          className=\"absolute left-1/2 top-[30%] flex w-fit\"\n          animate={{ x: -(currentIndex * safeSlideSize + safeSlideSize / 2) }}\n          transition={transition}\n        >\n          {items.map((item, index) => {\n            const isActive = currentIndex === index;\n            const distance = index - currentIndex;\n\n            return (\n              <motion.div\n                key={`${item.src}-${index}`}\n                className={cn(\n                  \"flex shrink-0 flex-col items-center gap-2 will-change-transform\",\n                  slideClassName\n                )}\n                style={{ width: safeSlideSize }}\n                animate={{\n                  rotate: distance * rotationStep,\n                  scale: isActive ? 1 : safeInactiveScale,\n                  y: distance * verticalStep,\n                }}\n                transition={transition}\n              >\n                <motion.p\n                  className={cn(\"whitespace-nowrap text-sm\", labelClassName)}\n                  animate={{\n                    opacity: isActive ? 1 : 0,\n                    scale: isActive ? 1 : 0.7,\n                  }}\n                  transition={{ duration: 0.3 }}\n                >\n                  {item.title}\n                </motion.p>\n\n                <button\n                  type=\"button\"\n                  aria-label={`Show ${item.title}`}\n                  aria-current={isActive ? \"true\" : undefined}\n                  className=\"aspect-square w-full cursor-pointer\"\n                  onClick={() => selectSlide(index)}\n                >\n                  {/* eslint-disable-next-line @next/next/no-img-element */}\n                  <img\n                    src={item.src}\n                    alt={item.alt ?? item.title}\n                    draggable={false}\n                    className={cn(\n                      \"h-full w-full select-none rounded-2xl object-cover shadow-xl\",\n                      imageClassName\n                    )}\n                  />\n                </button>\n              </motion.div>\n            );\n          })}\n        </motion.div>\n      </div>\n\n      {showControls && (\n        <div\n          className={cn(\n            \"absolute inset-x-4 bottom-5 z-10 mx-auto flex w-fit items-center justify-center gap-3 rounded-full border border-neutral-300/80 bg-neutral-200/70 px-2 text-neutral-700 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-neutral-900/70 dark:text-neutral-100\",\n            controlsClassName\n          )}\n        >\n          <button\n            type=\"button\"\n            aria-label=\"Show previous slide\"\n            disabled={isPreviousDisabled}\n            className=\"inline-flex size-9 items-center justify-center rounded-full transition-colors hover:bg-white/70 disabled:cursor-not-allowed disabled:opacity-35 dark:hover:bg-white/10\"\n            onClick={() => selectSlide(currentIndex - 1)}\n          >\n            <ChevronLeft className=\"size-5\" />\n          </button>\n\n          {showDots && (\n            <div className=\"flex items-center justify-center gap-2\">\n              {items.map((item, index) => (\n                <button\n                  key={`${item.title}-${index}`}\n                  type=\"button\"\n                  aria-label={`Show slide ${index + 1}: ${item.title}`}\n                  aria-current={currentIndex === index ? \"true\" : undefined}\n                  className={cn(\n                    \"h-2 rounded-full bg-current transition-[width,opacity] duration-300\",\n                    currentIndex === index ? \"w-7 opacity-100\" : \"w-2 opacity-30\"\n                  )}\n                  onClick={() => selectSlide(index)}\n                />\n              ))}\n            </div>\n          )}\n\n          <button\n            type=\"button\"\n            aria-label=\"Show next slide\"\n            disabled={isNextDisabled}\n            className=\"inline-flex size-9 items-center justify-center rounded-full transition-colors hover:bg-white/70 disabled:cursor-not-allowed disabled:opacity-35 dark:hover:bg-white/10\"\n            onClick={() => selectSlide(currentIndex + 1)}\n          >\n            <ChevronRight className=\"size-5\" />\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default DiagonalCarousel;\n",
      "type": "registry:ui",
      "target": "components/ui/diagonal-carousel.tsx"
    }
  ]
}
