{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gummy-carousel",
  "type": "registry:ui",
  "title": "Gummy Carousel",
  "description": "Controlled or uncontrolled accessible Carousel with labelled slides, previous/next controls, indicators, keyboard navigation, RTL, and reduced motion.",
  "registryDependencies": [
    "https://gummyui.dev/r/gummy-primitives-styles.json"
  ],
  "files": [
    {
      "path": "components/ui/GummyCarousel.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nfunction joinClassNames(...values: Array<string | false | null | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\ntype CarouselContextValue = {\n  index: number;\n  itemCount: number;\n  loop: boolean;\n  orientation: \"horizontal\" | \"vertical\";\n  direction: \"ltr\" | \"rtl\";\n  moveTo: (index: number) => void;\n};\n\nconst CarouselContext = React.createContext<CarouselContextValue | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n  if (!context) throw new Error(\"Gummy Carousel parts must be used inside GummyCarousel.\");\n  return context;\n}\n\nexport type GummyCarouselProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"dir\"\n> & {\n  itemCount: number;\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  loop?: boolean;\n  orientation?: \"horizontal\" | \"vertical\";\n  direction?: \"ltr\" | \"rtl\";\n  label?: string;\n};\n\nexport const GummyCarousel = React.forwardRef<HTMLDivElement, GummyCarouselProps>(\n  function GummyCarousel(\n    {\n      itemCount,\n      index: controlledIndex,\n      defaultIndex = 0,\n      onIndexChange,\n      loop = false,\n      orientation = \"horizontal\",\n      direction = \"ltr\",\n      label = \"Carousel\",\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) {\n    const count = Math.max(1, Math.floor(itemCount));\n    const clamp = React.useCallback(\n      (next: number) => Math.max(0, Math.min(count - 1, next)),\n      [count],\n    );\n    const [internalIndex, setInternalIndex] = React.useState(() => clamp(defaultIndex));\n    const currentIndex = clamp(controlledIndex ?? internalIndex);\n    const moveTo = React.useCallback(\n      (next: number) => {\n        const resolved = loop ? ((next % count) + count) % count : clamp(next);\n        if (controlledIndex === undefined) setInternalIndex(resolved);\n        if (resolved !== currentIndex) onIndexChange?.(resolved);\n      },\n      [clamp, controlledIndex, count, currentIndex, loop, onIndexChange],\n    );\n    const context = React.useMemo(\n      () => ({\n        index: currentIndex,\n        itemCount: count,\n        loop,\n        orientation,\n        direction,\n        moveTo,\n      }),\n      [count, currentIndex, direction, loop, moveTo, orientation],\n    );\n    return (\n      <CarouselContext.Provider value={context}>\n        <div\n          {...props}\n          ref={ref}\n          dir={direction}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          aria-label={label}\n          className={joinClassNames(\"gummy-carousel\", className)}\n          data-orientation={orientation}\n        >\n          {children}\n          <p className=\"gummy-visually-hidden\" aria-live=\"polite\" aria-atomic=\"true\">\n            Slide {currentIndex + 1} of {count}\n          </p>\n        </div>\n      </CarouselContext.Provider>\n    );\n  },\n);\n\nexport const GummyCarouselContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function GummyCarouselContent({ className, style, onKeyDown, ...props }, ref) {\n  const carousel = useCarousel();\n  return (\n    <div\n      {...props}\n      ref={ref}\n      tabIndex={0}\n      className={joinClassNames(\"gummy-carousel__viewport\", className)}\n      data-orientation={carousel.orientation}\n      style={{ ...style, \"--gummy-carousel-index\": carousel.index } as React.CSSProperties}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (event.defaultPrevented) return;\n        let delta = 0;\n        if (carousel.orientation === \"horizontal\") {\n          if (event.key === \"ArrowLeft\") delta = carousel.direction === \"rtl\" ? 1 : -1;\n          if (event.key === \"ArrowRight\") delta = carousel.direction === \"rtl\" ? -1 : 1;\n        } else {\n          if (event.key === \"ArrowUp\") delta = -1;\n          if (event.key === \"ArrowDown\") delta = 1;\n        }\n        if (event.key === \"Home\") {\n          event.preventDefault();\n          carousel.moveTo(0);\n        } else if (event.key === \"End\") {\n          event.preventDefault();\n          carousel.moveTo(carousel.itemCount - 1);\n        } else if (delta) {\n          event.preventDefault();\n          carousel.moveTo(carousel.index + delta);\n        }\n      }}\n    />\n  );\n});\n\nexport type GummyCarouselItemProps = React.HTMLAttributes<HTMLDivElement> & {\n  index: number;\n  label?: string;\n};\n\nexport const GummyCarouselItem = React.forwardRef<HTMLDivElement, GummyCarouselItemProps>(\n  function GummyCarouselItem({ index, label, className, ...props }, ref) {\n    const carousel = useCarousel();\n    const active = index === carousel.index;\n    return (\n      <div\n        {...props}\n        ref={ref}\n        role=\"group\"\n        aria-roledescription=\"slide\"\n        aria-label={label ?? `${index + 1} of ${carousel.itemCount}`}\n        aria-hidden={!active}\n        inert={!active}\n        className={joinClassNames(\"gummy-carousel__item\", className)}\n        data-active={active || undefined}\n      />\n    );\n  },\n);\n\ntype CarouselButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;\n\nexport const GummyCarouselPrevious = React.forwardRef<HTMLButtonElement, CarouselButtonProps>(\n  function GummyCarouselPrevious({ className, children, onClick, disabled, ...props }, ref) {\n    const carousel = useCarousel();\n    const unavailable = !carousel.loop && carousel.index === 0;\n    return (\n      <button\n        {...props}\n        ref={ref}\n        type=\"button\"\n        className={joinClassNames(\"gummy-carousel__control\", className)}\n        aria-label={props[\"aria-label\"] ?? \"Previous slide\"}\n        disabled={disabled || unavailable}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) carousel.moveTo(carousel.index - 1);\n        }}\n      >\n        {children ?? <span aria-hidden=\"true\">{carousel.direction === \"rtl\" ? \"→\" : \"←\"}</span>}\n      </button>\n    );\n  },\n);\n\nexport const GummyCarouselNext = React.forwardRef<HTMLButtonElement, CarouselButtonProps>(\n  function GummyCarouselNext({ className, children, onClick, disabled, ...props }, ref) {\n    const carousel = useCarousel();\n    const unavailable = !carousel.loop && carousel.index === carousel.itemCount - 1;\n    return (\n      <button\n        {...props}\n        ref={ref}\n        type=\"button\"\n        className={joinClassNames(\"gummy-carousel__control\", className)}\n        aria-label={props[\"aria-label\"] ?? \"Next slide\"}\n        disabled={disabled || unavailable}\n        onClick={(event) => {\n          onClick?.(event);\n          if (!event.defaultPrevented) carousel.moveTo(carousel.index + 1);\n        }}\n      >\n        {children ?? <span aria-hidden=\"true\">{carousel.direction === \"rtl\" ? \"←\" : \"→\"}</span>}\n      </button>\n    );\n  },\n);\n\nexport const GummyCarouselIndicators = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(function GummyCarouselIndicators({ className, ...props }, ref) {\n  const carousel = useCarousel();\n  return (\n    <div\n      {...props}\n      ref={ref}\n      role=\"group\"\n      aria-label={props[\"aria-label\"] ?? \"Choose slide\"}\n      className={joinClassNames(\"gummy-carousel__indicators\", className)}\n    >\n      {Array.from({ length: carousel.itemCount }, (_, index) => (\n        <button\n          key={index}\n          type=\"button\"\n          aria-label={`Go to slide ${index + 1}`}\n          aria-current={index === carousel.index ? \"true\" : undefined}\n          onClick={() => carousel.moveTo(index)}\n        />\n      ))}\n    </div>\n  );\n});\n\nGummyCarousel.displayName = \"GummyCarousel\";\nGummyCarouselContent.displayName = \"GummyCarouselContent\";\nGummyCarouselItem.displayName = \"GummyCarouselItem\";\nGummyCarouselPrevious.displayName = \"GummyCarouselPrevious\";\nGummyCarouselNext.displayName = \"GummyCarouselNext\";\nGummyCarouselIndicators.displayName = \"GummyCarouselIndicators\";\n"
    }
  ]
}
