# V carousel 13

> Media gallery carousel with caption badge and progress dots

A shadcn/ui block from the [@cnippet](https://ui.cnippet.dev/) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet/v-carousel-13).

- Page, with a live preview: https://shadcn.exchange/r/cnippet/v-carousel-13
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/cnippet/v-carousel-13-light-6ab3b413ec18ac8e.png
- Registry item: https://ui.cnippet.dev/r/v-carousel-13.json
- Categories: carousel
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.cnippet.dev/r/v-carousel-13.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@cnippet/carousel`

## Files

### `registry/default/variants/v-carousel-13.tsx`

```tsx
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import {
  Carousel,
  type CarouselApi,
  CarouselContent,
  CarouselItem,
  CarouselNext,
  CarouselPrevious,
} from "@/registry/default/ui/carousel";

const MEDIA = [
  {
    bg: "from-violet-400 to-indigo-600",
    caption: "Brand identity refresh",
    type: "Image",
  },
  {
    bg: "from-sky-400 to-cyan-600",
    caption: "Product launch 2026",
    type: "Video",
  },
  {
    bg: "from-rose-400 to-pink-600",
    caption: "Social media kit",
    type: "Image",
  },
  {
    bg: "from-amber-400 to-orange-500",
    caption: "Campaign assets",
    type: "Image",
  },
  { bg: "from-emerald-400 to-teal-600", caption: "Demo reel", type: "Video" },
];

export default function Particle() {
  const [api, setApi] = React.useState<CarouselApi>();
  const [current, setCurrent] = React.useState(0);
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    if (!api) return;
    setCount(api.scrollSnapList().length);
    setCurrent(api.selectedScrollSnap());
    api.on("select", () => setCurrent(api.selectedScrollSnap()));
  }, [api]);

  return (
    <div className="w-full max-w-sm space-y-3">
      <Carousel className="w-full" setApi={setApi}>
        <CarouselContent>
          {MEDIA.map((item, i) => (
            <CarouselItem key={i}>
              <div
                className={cn(
                  "relative flex h-48 items-end rounded-xl bg-linear-to-br p-4",
                  item.bg,
                )}
              >
                <div className="flex flex-col gap-0.5">
                  <span className="w-fit rounded-md bg-black/30 px-2 py-0.5 font-medium text-[10px] text-white">
                    {item.type}
                  </span>
                  <span className="font-semibold text-sm text-white drop-shadow">
                    {item.caption}
                  </span>
                </div>
              </div>
            </CarouselItem>
          ))}
        </CarouselContent>
        <CarouselPrevious />
        <CarouselNext />
      </Carousel>
      <div className="flex items-center justify-between px-1 text-muted-foreground text-xs">
        <span>
          {current + 1} of {count}
        </span>
        <div className="flex gap-1">
          {MEDIA.map((_, i) => (
            <span
              className={cn(
                "block h-1 rounded-full transition-all",
                i === current
                  ? "w-4 bg-primary"
                  : "w-1.5 bg-muted-foreground/30",
              )}
              key={i}
            />
          ))}
        </div>
      </div>
    </div>
  );
}
```
