# Polaroid Gallery

> A scattered set of polaroid style cards with captions and slight tilts. Great for adding a casual, personal feel to an about page or event recap.

A shadcn/ui block from the [@shadcn-ui-blocks](https://www.shadcn-ui-blocks.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcn-ui-blocks/marketing-gallery-polaroid-gallery).

- Page, with a live preview: https://shadcn.exchange/r/shadcn-ui-blocks/marketing-gallery-polaroid-gallery
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcn-ui-blocks/marketing-gallery-polaroid-gallery-light-535241509c686889.png
- Registry item: https://www.shadcn-ui-blocks.com/r/marketing-gallery-polaroid-gallery.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.shadcn-ui-blocks.com/r/marketing-gallery-polaroid-gallery.json
```

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

## Dependencies

- npm packages: `lucide-react`

## Files

### `components/blocks/marketing/gallery/polaroid-gallery.tsx`

```tsx
import * as React from 'react';
import { Calendar, MapPin } from 'lucide-react';

interface PolaroidImage {
  src: string;
  alt: string;
  width: number;
  height: number;
  location?: string;
  date?: string;
  rotation?: number;
}

export default function PolaroidGallery() {
  // Static images array wrapped in useMemo to maintain stable reference
  const images = React.useMemo<PolaroidImage[]>(
    () => [
      {
        src: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?q=80&w=1473&auto=format&fit=crop',
        alt: 'Paris with Eiffel Tower view',
        width: 1473,
        height: 982,
        location: 'Paris, France',
        date: 'June 2023',
        rotation: -3,
      },
      {
        src: 'https://images.unsplash.com/photo-1473496169904-658ba7c44d8a?q=80&w=1470&auto=format&fit=crop',
        alt: 'Beach sunset with palm tree silhouettes',
        width: 1470,
        height: 980,
        location: 'Bali, Indonesia',
        date: 'August 2023',
        rotation: 2,
      },
      {
        src: 'https://images.unsplash.com/photo-1517231925375-bf2cb42917a5?q=80&w=1471&auto=format&fit=crop',
        alt: 'Espresso and pastry at a sidewalk cafe',
        width: 1471,
        height: 981,
        location: 'Rome, Italy',
        date: 'September 2023',
        rotation: -2,
      },
      {
        src: 'https://images.unsplash.com/photo-1551524559-8af4e6624178?q=80&w=1526&auto=format&fit=crop',
        alt: 'Vintage car parked on colorful street',
        width: 1526,
        height: 1017,
        location: 'Havana, Cuba',
        date: 'May 2023',
        rotation: 3,
      },
      {
        src: 'https://images.unsplash.com/photo-1526498460520-4c246339dccb?q=80&w=1470&auto=format&fit=crop',
        alt: 'Person hiking on mountain trail at sunrise',
        width: 1470,
        height: 980,
        location: 'Swiss Alps',
        date: 'July 2023',
        rotation: -1,
      },
      {
        src: 'https://images.unsplash.com/photo-1543158266-0066955047b1?q=80&w=1470&auto=format&fit=crop',
        alt: 'Friends laughing together at outdoor party',
        width: 1470,
        height: 980,
        location: 'Barcelona, Spain',
        date: 'April 2023',
        rotation: 1,
      },
      {
        src: 'https://images.unsplash.com/photo-1551918120-9739cb430c6d?q=80&w=1374&auto=format&fit=crop',
        alt: 'Desert landscape with red sand dunes',
        width: 1374,
        height: 916,
        location: 'Marrakech, Morocco',
        date: 'October 2023',
        rotation: -2,
      },
      {
        src: 'https://images.unsplash.com/photo-1519681393784-d120267933ba?q=80&w=1470&auto=format&fit=crop',
        alt: 'Starry night sky over mountain peaks',
        width: 1470,
        height: 980,
        location: 'Dolomites, Italy',
        date: 'January 2023',
        rotation: 2,
      },
      {
        src: 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop',
        alt: 'Sunset over calm lake with mountain reflections',
        width: 1470,
        height: 980,
        location: 'Lake Louise, Canada',
        date: 'June 2023',
        rotation: -1,
      },
    ],
    []
  );

  // Calculate random stagger for first render only
  const staggerOffsets = React.useMemo(
    () => images.map(() => Math.floor(Math.random() * 20) - 10),
    [images]
  );

  return (
    <div className="w-full p-4 md:p-6">
      <div className="flex flex-wrap justify-center gap-6 md:gap-8">
        {images.map((image, index) => {
          // Calculate polaroid dimensions - maintain image aspect ratio plus frame
          const aspectRatio = image.width / image.height;
          const polaroidWidth = 220; // fixed width for all polaroids
          const imageHeight = polaroidWidth / aspectRatio;

          return (
            <div
              key={index}
              className="group relative"
              style={{
                marginTop: `${staggerOffsets[index]}px`, // Random vertical stagger
                transform: `rotate(${image.rotation || 0}deg)`,
                transformOrigin: 'center',
                transition: 'transform 0.3s ease, z-index 0.3s ease',
                zIndex: 1,
              }}
              onMouseEnter={(e) => {
                e.currentTarget.style.zIndex = '10';
                e.currentTarget.style.transform = `rotate(${image.rotation || 0}deg) scale(1.05)`;
              }}
              onMouseLeave={(e) => {
                e.currentTarget.style.zIndex = '1';
                e.currentTarget.style.transform = `rotate(${image.rotation || 0}deg)`;
              }}
            >
              {/* Polaroid frame */}
              <div className="flex h-full w-full flex-col overflow-hidden rounded-md bg-white p-3 shadow-md">
                {/* Image */}
                <div
                  className="relative w-full grow overflow-hidden"
                  style={{
                    height: `${imageHeight}px`,
                  }}
                >
                  <img
                    src={image.src}
                    alt={image.alt}
                    sizes={`${polaroidWidth}px`}
                    className="w-full object-cover"
                  />
                </div>

                {/* Caption */}
                <div className="mt-3 py-2">
                  <p className="font-handwriting mb-2 text-center text-sm font-medium">
                    {image.alt}
                  </p>

                  <div className="flex items-center justify-between text-xs text-gray-500">
                    {image.location && (
                      <div className="flex items-center">
                        <MapPin className="mr-1 h-3 w-3" />
                        <span>{image.location}</span>
                      </div>
                    )}

                    {image.date && (
                      <div className="flex items-center">
                        <Calendar className="mr-1 h-3 w-3" />
                        <span>{image.date}</span>
                      </div>
                    )}
                  </div>
                </div>
              </div>

              {/* Tape effect */}
              <div className="pointer-events-none absolute -top-3 left-1/2 h-6 w-16 -translate-x-1/2 rotate-3 bg-gray-100/80 shadow-sm" />
            </div>
          );
        })}
      </div>
    </div>
  );
}
```
