# Gallery Section v5

> An arc carousel gallery: square photo cards ride a slow, endlessly rotating ring over a dashed orbit line, framing a short centered title and description. Pauses on card hover.

A shadcn/ui block from the [@uiception](https://uiception.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/gallery-section-v5).

- Page, with a live preview: https://shadcn.exchange/r/uiception/gallery-section-v5
- Screenshot: https://shadcn.exchange/shot/uiception/gallery-section-v5/light.png
- Registry item: https://uiception.com/r/gallery-section-v5.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uiception/gallery-section-v5.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: `https://uiception.com/r/block-media.json`

## Files

### `registry/new-york/blocks/gallery-section-v5/page.tsx`

```tsx
import GallerySectionV5 from "./components/gallery-section-v5"

export default function Page() {
  return <GallerySectionV5 />
}
```

### `registry/new-york/blocks/gallery-section-v5/components/gallery-section-v5.tsx`

```tsx
import { GalleryArcCard } from "./gallery-arc-card"
import { createBlockImage } from "@/lib/block-media"
import {
  RING_SLOTS,
  galleryFiles,
  sectionMeta,
  type GalleryItem,
} from "../lib/config"
import "../styles/gallery-section-v5.css"

const blockImage = createBlockImage("gallery-section-v5")
const galleryItems: GalleryItem[] = galleryFiles.map(({ file, alt }, index) => ({
  id: String(index + 1),
  alt,
  imageSrc: blockImage(file),
}))

export type GallerySectionV5Props = {
  header?: { title: string; description?: string } | false
  items?: GalleryItem[]
}

export default function GallerySectionV5({
  header = sectionMeta,
  items = galleryItems,
}: GallerySectionV5Props = {}) {
  const slots = Array.from({ length: RING_SLOTS }, (_, slot) => ({
    slot,
    item: items[slot % items.length],
  }))

  return (
    <section className="gsv5-stage relative isolate overflow-hidden py-4 md:py-6 lg:py-8">
      <div aria-hidden className="gsv5-fade pointer-events-none absolute inset-0 -z-10">
        <div className="gsv5-orbit absolute left-1/2 rounded-full border border-dashed border-border" />
      </div>

      <div className="gsv5-fade absolute inset-0 -z-10">
        <ul className="gsv5-ring absolute left-1/2" aria-label="Gallery">
          {slots.map(({ slot, item }) => (
            <GalleryArcCard
              key={slot}
              item={item}
              angle={(360 / RING_SLOTS) * slot}
              decorative={slot >= items.length}
              priority={slot < 3 || slot > RING_SLOTS - 3}
            />
          ))}
        </ul>
      </div>

      {header !== false ? (
        <div className="gsv5-header pointer-events-none mx-auto max-w-md px-4 text-center *:pointer-events-auto">
          <h2 className="text-4xl font-medium leading-[1.1] tracking-[-0.03em] lg:text-5xl">
            {header.title}
          </h2>
          {header.description ? (
            <p className="mx-auto mt-4 max-w-sm text-[0.9375rem] leading-relaxed text-muted-foreground">
              {header.description}
            </p>
          ) : null}
        </div>
      ) : null}
    </section>
  )
}
```

### `registry/new-york/blocks/gallery-section-v5/components/gallery-arc-card.tsx`

```tsx
import type { CSSProperties } from "react"
import Image from "next/image"

import type { GalleryItem } from "../lib/config"

type GalleryArcCardProps = {
  item: GalleryItem
  angle: number
  /** Repeated copies are decorative — only the first pass is announced. */
  decorative?: boolean
  priority?: boolean
}

export function GalleryArcCard({
  item,
  angle,
  decorative = false,
  priority = false,
}: GalleryArcCardProps) {
  return (
    <li
      className="gsv5-card"
      style={{ "--gsv5-angle": `${angle}deg` } as CSSProperties}
      aria-hidden={decorative || undefined}
    >
      <div className="relative size-full overflow-hidden rounded-2xl bg-muted shadow-xl shadow-foreground/10 ring-1 ring-border transition-transform duration-500 ease-out hover:scale-105">
        <Image
          src={item.imageSrc}
          alt={decorative ? "" : item.alt}
          fill
          unoptimized
          priority={priority}
          sizes="13rem"
          className="object-cover"
        />
      </div>
    </li>
  )
}
```

### `registry/new-york/blocks/gallery-section-v5/lib/config.ts`

```ts
export type GalleryItem = {
  id: string
  imageSrc: string
  alt: string
}

export const sectionMeta = {
  title: "Always something new on the easel",
  description:
    "Recent canvases and paper studies drift past one by one.",
} as const

/** Cards spaced evenly around the full ring — items repeat to fill every slot. */
export const RING_SLOTS = 20

export const galleryFiles = [
  { file: "image-1.jpg", alt: "Oil painting of a morning harbor" },
  { file: "image-2.jpg", alt: "Acrylic field study painting" },
  { file: "image-3.jpg", alt: "Oil painting of a quiet interior" },
  { file: "image-4.jpg", alt: "Watercolor of coastal light" },
  { file: "image-5.jpg", alt: "Oil painting of an autumn orchard" },
] as const
```

### `registry/new-york/blocks/gallery-section-v5/styles/gallery-section-v5.css`

```css
/* Ring geometry — the ring is a 0×0 point at the circle's center; each card
   is rotated to its slot then pushed out by the radius, so rotating the point
   spins the whole arc. One full turn loops seamlessly. */
.gsv5-stage {
  --gsv5-card: 7rem;
  --gsv5-radius: 28rem;
  --gsv5-top: 2.5rem;
  --gsv5-center: calc(var(--gsv5-top) + var(--gsv5-card) / 2 + var(--gsv5-radius));
  min-height: calc(var(--gsv5-top) + var(--gsv5-card) / 2 + var(--gsv5-radius) * 0.7);
}

@media (min-width: 768px) {
  .gsv5-stage {
    --gsv5-card: 11rem;
    --gsv5-radius: 40rem;
    --gsv5-top: 3rem;
  }
}

@media (min-width: 1024px) {
  .gsv5-stage {
    --gsv5-card: 13rem;
    --gsv5-radius: 48rem;
  }
}

.gsv5-fade {
  mask-image: linear-gradient(to bottom, #000 50%, transparent 92%);
}

.gsv5-orbit {
  top: calc(var(--gsv5-center) - var(--gsv5-radius));
  width: calc(var(--gsv5-radius) * 2);
  height: calc(var(--gsv5-radius) * 2);
  translate: -50% 0;
}

.gsv5-ring {
  top: var(--gsv5-center);
  width: 0;
  height: 0;
  animation: gsv5-spin 90s linear infinite;
}

.gsv5-ring:has(.gsv5-card:hover) {
  animation-play-state: paused;
}

.gsv5-card {
  position: absolute;
  top: calc(var(--gsv5-card) / -2);
  left: calc(var(--gsv5-card) / -2);
  width: var(--gsv5-card);
  height: var(--gsv5-card);
  transform: rotate(var(--gsv5-angle)) translateY(calc(var(--gsv5-radius) * -1));
}

.gsv5-header {
  padding-top: calc(var(--gsv5-top) + var(--gsv5-card) + 2.5rem);
  padding-bottom: 4rem;
}

@keyframes gsv5-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gsv5-ring {
    animation-play-state: paused;
  }
}
```

### `public/images/blocks/gallery-section-v5/image-1.jpg.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```

### `public/images/blocks/gallery-section-v5/image-2.jpg.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```

### `public/images/blocks/gallery-section-v5/image-3.jpg.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```

### `public/images/blocks/gallery-section-v5/image-4.jpg.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```

### `public/images/blocks/gallery-section-v5/image-5.jpg.gitkeep`

```gitkeep
Place your asset here. Same base name, any supported format: jpg, jpeg, png, webp, avif, gif, or svg. Remove the .gitkeep extension from the filename.
```
