# Gallery Section v4

> A responsive CSS-grid masonry gallery with varied tile heights and a quick blur-in reveal as each tile scrolls into view, no title or copy — just the grid.

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-v4).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/uiception/gallery-section-v4.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-v4/page.tsx`

```tsx
import GallerySectionV4 from "./components/gallery-section-v4"

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

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

```tsx
import { GalleryMasonryItem } from "./gallery-masonry-item"
import { createBlockImage } from "@/lib/block-media"
import { ROW_UNIT, imageFiles, type GalleryItem } from "../lib/config"

const blockImage = createBlockImage("gallery-section-v4")
const galleryItems: GalleryItem[] = imageFiles.map(
  ({ file, alt, heightPx }, index) => ({
    id: `image-${index + 1}`,
    alt,
    span: Math.round(heightPx / ROW_UNIT),
    imageSrc: blockImage(file),
  }),
)

export type GallerySectionV4Props = {
  items?: GalleryItem[]
}

export default function GallerySectionV4({
  items = galleryItems,
}: GallerySectionV4Props = {}) {
  return (
    <section className="py-4 md:py-6 lg:py-8">
      <div className="mx-auto max-w-7xl px-4">
        <div className="grid grid-flow-dense auto-rows-[4px] grid-cols-2 gap-x-4 md:grid-cols-3 lg:grid-cols-4">
          {items.map((item, index) => (
            <GalleryMasonryItem
              key={item.id}
              item={item}
              priority={index < 4}
            />
          ))}
        </div>
      </div>
    </section>
  )
}
```

### `registry/new-york/blocks/gallery-section-v4/components/gallery-masonry-item.tsx`

```tsx
"use client"

import { useState } from "react"
import Image from "next/image"

import { useInView } from "../hooks/use-in-view"
import type { GalleryItem } from "../lib/config"

type GalleryMasonryItemProps = {
  item: GalleryItem
  priority?: boolean
}

export function GalleryMasonryItem({
  item,
  priority = false,
}: GalleryMasonryItemProps) {
  const [loaded, setLoaded] = useState(false)
  const [ref, inView] = useInView<HTMLDivElement>()
  const revealed = loaded && inView

  return (
    <div style={{ gridRowEnd: `span ${item.span}` }}>
      <div
        ref={ref}
        className="relative h-[calc(100%-1rem)] overflow-hidden rounded-lg bg-muted"
      >
        <Image
          src={item.imageSrc}
          alt={item.alt}
          fill
          unoptimized
          priority={priority}
          sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw"
          className={`object-cover transition-[opacity,filter,translate] duration-500 ease-out${revealed ? " translate-y-0 opacity-100 blur-none" : " translate-y-6 opacity-0 blur-sm"}`}
          onLoad={() => setLoaded(true)}
          onError={() => setLoaded(true)}
        />
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/gallery-section-v4/hooks/use-in-view.ts`

```ts
"use client"

import { useEffect, useRef, useState } from "react"

/** Flips to `true` once the node crosses into the viewport, then stops observing. */
export function useInView<T extends HTMLElement>(threshold = 0.15) {
  const ref = useRef<T>(null)
  const [inView, setInView] = useState(false)

  useEffect(() => {
    const node = ref.current
    if (!node) return

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) return
        setInView(true)
        observer.disconnect()
      },
      { threshold, rootMargin: "0px 0px -10% 0px" },
    )

    observer.observe(node)
    return () => observer.disconnect()
  }, [threshold])

  return [ref, inView] as const
}
```

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

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

/** Grid row height in px — keep in sync with the `auto-rows-[…]` class on the grid. */
export const ROW_UNIT = 4

export const imageFiles = [
  { file: "image-1.jpg", alt: "Mountain landscape at golden hour", heightPx: 320 },
  { file: "image-2.jpg", alt: "Coastal cliffs and ocean waves", heightPx: 220 },
  { file: "image-3.jpg", alt: "Forest trail through tall trees", heightPx: 380 },
  { file: "image-4.jpg", alt: "City skyline at dusk", heightPx: 260 },
  { file: "image-5.jpg", alt: "Desert dunes under a clear sky", heightPx: 300 },
  { file: "image-6.jpg", alt: "Snow-capped peaks above a valley", heightPx: 420 },
  { file: "image-7.jpg", alt: "Lush green hills and farmland", heightPx: 240 },
  { file: "image-8.jpg", alt: "Rocky shoreline at sunset", heightPx: 340 },
  { file: "image-9.jpg", alt: "Misty lake surrounded by mountains", heightPx: 280 },
  { file: "image-10.jpg", alt: "Cherry blossoms in spring light", heightPx: 360 },
  { file: "image-1.jpg", alt: "Mountain landscape at golden hour", heightPx: 260 },
  { file: "image-2.jpg", alt: "Coastal cliffs and ocean waves", heightPx: 360 },
  { file: "image-3.jpg", alt: "Forest trail through tall trees", heightPx: 300 },
  { file: "image-4.jpg", alt: "City skyline at dusk", heightPx: 220 },
  { file: "image-5.jpg", alt: "Desert dunes under a clear sky", heightPx: 400 },
  { file: "image-6.jpg", alt: "Snow-capped peaks above a valley", heightPx: 280 },
  { file: "image-7.jpg", alt: "Lush green hills and farmland", heightPx: 340 },
  { file: "image-8.jpg", alt: "Rocky shoreline at sunset", heightPx: 240 },
  { file: "image-9.jpg", alt: "Misty lake surrounded by mountains", heightPx: 380 },
  { file: "image-10.jpg", alt: "Cherry blossoms in spring light", heightPx: 320 },
]
```

### `public/images/blocks/gallery-section-v4/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-v4/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-v4/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-v4/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-v4/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.
```

### `public/images/blocks/gallery-section-v4/image-6.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-v4/image-7.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-v4/image-8.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-v4/image-9.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-v4/image-10.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.
```
