# Bento Grid 1

> A destination bento with a filter row and a top-rated tile

A shadcn/ui block from the [@shadcnstore](https://www.shadcnstore.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/shadcnstore/bento-grid-1).

- Page, with a live preview: https://shadcn.exchange/r/shadcnstore/bento-grid-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnstore/bento-grid-1-light-28cff048876678a3.png
- Registry item: https://shadcnstore.com/r/bento-grid-1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnstore.com/r/bento-grid-1.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`, `motion`
- Registry items: `@shadcnstore/motion-sliding-tabs`, `badge`, `button`, `card`

## Files

### `storage/app/registry-sources/marketing/bento-grids/bento-grid-1/page.tsx`

```tsx
import { BentoGrid1 } from '@/components/blocks/marketing/bento-grids/bento-grid-1/components/bento-grid-1'

export default function Page() {
  return (
    <div className="flex min-h-svh w-full items-center justify-center">
      <div className="w-full">
        <BentoGrid1 />
      </div>
    </div>
  )
}
```

### `storage/app/registry-sources/marketing/bento-grids/bento-grid-1/bento-grid-1.tsx`

```tsx
'use client'

import { useState } from 'react'
import { SlidersHorizontal } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { SlidingTabs } from '@/components/motion/sliding-tabs'

import { destinationsByRegion, regions, type Destination, type RegionId } from "@/components/data/bento-grid-1-data"
import { BentoGrid1DestinationCard } from './bento-grid-1-destination-card'
import { BentoGrid1FilterColumn, BentoGrid1FilterGrid, BentoGrid1FilterItem } from './bento-grid-1-filter-grid'

const columnHeights = [
  ['h-[300px]', 'h-[300px]'],
  ['h-[420px]', 'h-[180px]'],
  ['h-[300px]', 'h-[300px]'],
] as const

function buildColumns(items: Destination[]) {
  return columnHeights.map((heights, columnIndex) => {
    const start = columnIndex * 2
    return heights
      .map((heightClass, rowIndex) => {
        const destination = items[start + rowIndex]
        if (!destination) {
          return null
        }

        return { destination, heightClass }
      })
      .filter((entry): entry is { destination: Destination; heightClass: (typeof heights)[number] } => entry !== null)
  })
}

export function BentoGrid1() {
  const [activeRegion, setActiveRegion] = useState<RegionId>('top-rated')
  const [favorites, setFavorites] = useState<number[]>([])

  const activeDestinations = destinationsByRegion[activeRegion]
  const columns = buildColumns(activeDestinations)

  const toggleFavorite = (id: number) => {
    setFavorites(prev => (prev.includes(id) ? prev.filter(favId => favId !== id) : [...prev, id]))
  }

  return (
    <section className='w-full'>
      <div className='mx-auto w-full max-w-7xl px-4 py-12 sm:px-6 lg:px-8'>
        <div className='mb-8 flex flex-col gap-2 text-center sm:mb-12 lg:mb-16'>
          <h2 className='text-3xl tracking-tight sm:text-4xl lg:text-5xl'>
            <span className='text-foreground font-bold'>Top</span>{' '}
            <span className='text-foreground font-semibold'>Destinations</span>
          </h2>
          <p className='text-muted-foreground mx-auto max-w-2xl'>
            There are Many Variations of Passages Of Lorem Ipsum Available
          </p>
        </div>

        <div className='mb-8 flex flex-wrap items-center justify-between gap-4'>
          <SlidingTabs
            items={regions.map(region => ({ value: region.id, label: region.label }))}
            value={activeRegion}
            onValueChange={setActiveRegion}
            label="Destination regions"
            shape="pill"
          />
          <Button variant='outline' className='h-9 cursor-pointer px-4 py-2'>
            <SlidersHorizontal data-icon='inline-start' />
            Filter
          </Button>
        </div>

        <BentoGrid1FilterGrid
          filterKey={activeRegion}
          className='grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3'
        >
          {columns.map((column, columnIndex) => (
            <BentoGrid1FilterColumn
              key={columnIndex}
              className={columnIndex === 2 ? 'flex flex-col gap-6 sm:max-lg:col-span-2' : 'flex flex-col gap-6'}
            >
              {column.map(({ destination, heightClass }) => (
                <BentoGrid1FilterItem key={destination.id}>
                  <BentoGrid1DestinationCard
                    destination={destination}
                    heightClass={heightClass}
                    isFavorite={favorites.includes(destination.id)}
                    onFavorite={() => toggleFavorite(destination.id)}
                  />
                </BentoGrid1FilterItem>
              ))}
            </BentoGrid1FilterColumn>
          ))}
        </BentoGrid1FilterGrid>
      </div>
    </section>
  )
}

export default BentoGrid1
```

### `storage/app/registry-sources/marketing/bento-grids/bento-grid-1/bento-grid-1-destination-card.tsx`

```tsx
'use client'

import { Heart, Star } from 'lucide-react'

import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'

import type { Destination } from "@/components/data/bento-grid-1-data"

type BentoGrid1DestinationCardProps = {
  destination: Destination
  heightClass: string
  isFavorite: boolean
  onFavorite: () => void
}

export function BentoGrid1DestinationCard({
  destination,
  heightClass,
  isFavorite,
  onFavorite,
}: BentoGrid1DestinationCardProps) {
  return (
    <div className={cn('group', heightClass)}>
      <Card className='h-full overflow-hidden border-0 p-0 shadow-xs ring-0 transition-all hover:shadow-md'>
        <CardContent className='relative h-full p-0'>
          <div className='size-full overflow-hidden'>
            <img
              src={destination.image}
              alt={destination.title}
              className='size-full object-cover opacity-80 transition-all duration-300 group-hover:opacity-100 group-hover:transition-opacity'
            />
            <div className='absolute inset-0 bg-linear-to-t from-black/60 via-black/20 to-transparent' />

            <Badge
              className='absolute start-4 top-4 flex h-auto items-center gap-1 rounded-md px-3 py-1.5 backdrop-blur-xs'
              variant='secondary'
            >
              <Star className='fill-foreground text-foreground size-3.5' aria-hidden='true' />
              <span className='text-foreground'>{destination.rating}.0</span>
            </Badge>

            <Button
              variant='secondary'
              size='icon'
              onClick={event => {
                event.stopPropagation()
                onFavorite()
              }}
              aria-label={isFavorite ? `Remove ${destination.title} from favorites` : `Add ${destination.title} to favorites`}
              className='absolute end-4 top-4 size-8 cursor-pointer rounded-full p-0 backdrop-blur-xs'
            >
              <Heart
                className={cn(isFavorite ? 'fill-destructive text-destructive' : 'text-muted-foreground')}
                aria-hidden='true'
              />
            </Button>

            <div className='absolute start-4 bottom-4'>
              <Badge className='h-auto rounded-md px-3 py-1.5 text-sm font-medium backdrop-blur-xs' variant='secondary'>
                {destination.location}
              </Badge>
            </div>

            <div className='absolute end-4 bottom-4'>
              <Badge className='bg-primary/90 text-primary-foreground h-auto rounded-md px-4 py-1.5 text-sm font-semibold backdrop-blur-xs'>
                ${destination.price}
              </Badge>
            </div>
          </div>
        </CardContent>
      </Card>
    </div>
  )
}
```

### `storage/app/registry-sources/marketing/bento-grids/bento-grid-1/bento-grid-1-filter-grid.tsx`

```tsx
'use client'

import type { ReactNode } from 'react'
import { AnimatePresence, motion, useReducedMotion, type Variants } from 'motion/react'

/**
 * Destination filter reveal — Motion's gallery filter pattern
 * (AnimatePresence + staggered opacity/scale), timed with Transitions.dev tokens
 * (250ms-class easing from tabs-sliding / icon-swap).
 *
 * Transitions.dev has no dedicated image-filter transition; Motion / Framer Motion
 * filtered galleries are the widely used React pattern for this UX.
 */
const filterEase = [0.22, 1, 0.36, 1] as const

const gridVariants: Variants = {
  hidden: {},
  show: {
    transition: {
      staggerChildren: 0.06,
      delayChildren: 0.02,
    },
  },
  exit: {
    transition: {
      staggerChildren: 0.03,
      staggerDirection: -1,
    },
  },
}

const columnVariants: Variants = {
  hidden: {},
  show: {
    transition: {
      staggerChildren: 0.045,
    },
  },
  exit: {
    transition: {
      staggerChildren: 0.025,
      staggerDirection: -1,
    },
  },
}

const itemVariants: Variants = {
  hidden: {
    opacity: 0,
    y: 12,
    scale: 0.96,
  },
  show: {
    opacity: 1,
    y: 0,
    scale: 1,
    transition: {
      duration: 0.28,
      ease: filterEase,
    },
  },
  exit: {
    opacity: 0,
    y: -8,
    scale: 0.98,
    transition: {
      duration: 0.18,
      ease: 'easeIn',
    },
  },
}

type WrapperProps = {
  className?: string
  children: ReactNode
}

type FilterGridProps = WrapperProps & {
  filterKey: string
}

export function BentoGrid1FilterGrid({ filterKey, className, children }: FilterGridProps) {
  const reduceMotion = useReducedMotion()

  if (reduceMotion) {
    return (
      <div key={filterKey} className={className}>
        {children}
      </div>
    )
  }

  return (
    <AnimatePresence mode='wait'>
      <motion.div
        key={filterKey}
        className={className}
        variants={gridVariants}
        initial='hidden'
        animate='show'
        exit='exit'
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

export function BentoGrid1FilterColumn({ className, children }: WrapperProps) {
  const reduceMotion = useReducedMotion()

  if (reduceMotion) {
    return <div className={className}>{children}</div>
  }

  return (
    <motion.div className={className} variants={columnVariants}>
      {children}
    </motion.div>
  )
}

export function BentoGrid1FilterItem({ className, children }: WrapperProps) {
  const reduceMotion = useReducedMotion()

  if (reduceMotion) {
    return <div className={className}>{children}</div>
  }

  return (
    <motion.div className={className} variants={itemVariants}>
      {children}
    </motion.div>
  )
}
```

### `storage/app/registry-sources/marketing/bento-grids/bento-grid-1/bento-grid-1-data.ts`

```ts
export const regions = [
  { id: 'top-rated', label: 'Top Rated' },
  { id: 'italy', label: 'Italy' },
  { id: 'dubai', label: 'Dubai' },
  { id: 'london', label: 'London' },
] as const

export type RegionId = (typeof regions)[number]['id']

export interface Destination {
  id: number
  title: string
  location: string
  image: string
  price: number
  rating: number
}

const topRated: Destination[] = [
  {
    id: 1,
    title: 'Italy',
    location: 'Italy',
    image:
      'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/professional-outdoor-adventure-gear-and-equipment.1200w.4ed779.avif',
    price: 504,
    rating: 5,
  },
  {
    id: 2,
    title: 'Dubai',
    location: 'Dubai',
    image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/venice-canals-with-gondolas-2.800w.387c6a.avif',
    price: 421,
    rating: 5,
  },
  {
    id: 3,
    title: 'Maldives',
    location: 'Maldives',
    image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/maldives.500w.ff09c7.avif',
    price: 254,
    rating: 5,
  },
  {
    id: 4,
    title: 'Sezaland',
    location: 'Sezaland',
    image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/historic-edinburgh-castle-on-hill.800w.209186.avif',
    price: 504,
    rating: 5,
  },
  {
    id: 5,
    title: 'London',
    location: 'London',
    image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/sheikh-zayed-grand-mosque.800w.fd1148.avif',
    price: 214,
    rating: 5,
  },
  {
    id: 6,
    title: 'America',
    location: 'America',
    image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/american-flag-patriotic.500w.31382f.avif',
    price: 898,
    rating: 5,
  },
]

export const destinationsByRegion: Record<RegionId, Destination[]> = {
  'top-rated': topRated,
  italy: [
    {
      id: 11,
      title: 'Rome',
      location: 'Italy',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/historic-rome-colosseum.800w.020806.avif',
      price: 389,
      rating: 5,
    },
    {
      id: 12,
      title: 'Venice',
      location: 'Italy',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/venice-canals-with-gondolas.800w.fb4288.avif',
      price: 452,
      rating: 5,
    },
    {
      id: 13,
      title: 'Tuscany',
      location: 'Italy',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/beautiful-tuscany-landscape.800w.512602.avif',
      price: 318,
      rating: 5,
    },
    {
      id: 14,
      title: 'Amalfi',
      location: 'Italy',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/scenic-amalfi-coast.800w.5b35d9.avif',
      price: 476,
      rating: 5,
    },
    {
      id: 15,
      title: 'Florence',
      location: 'Italy',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/venice-canals-with-gondolas-2.800w.387c6a.avif',
      price: 341,
      rating: 4,
    },
    {
      id: 16,
      title: 'Milan',
      location: 'Italy',
      image:
        'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/professional-outdoor-adventure-gear-and-equipment.1200w.4ed779.avif',
      price: 298,
      rating: 4,
    },
  ],
  dubai: [
    {
      id: 21,
      title: 'Marina',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/sheikh-zayed-grand-mosque.800w.fd1148.avif',
      price: 512,
      rating: 5,
    },
    {
      id: 22,
      title: 'Old Dubai',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/bangkok-grand-palace.800w.e8c0c4.avif',
      price: 278,
      rating: 4,
    },
    {
      id: 23,
      title: 'Desert Safari',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/scenic-scottish-highlands.800w.4f8b81.avif',
      price: 196,
      rating: 5,
    },
    {
      id: 24,
      title: 'Palm Jumeirah',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/maldives.500w.ff09c7.avif',
      price: 624,
      rating: 5,
    },
    {
      id: 25,
      title: 'Downtown',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/paris-with-eiffel-tower.800w.809f1f.avif',
      price: 448,
      rating: 5,
    },
    {
      id: 26,
      title: 'Creek',
      location: 'Dubai',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/dutch-windmills-kinderdijk.800w.c46210.avif',
      price: 312,
      rating: 4,
    },
  ],
  london: [
    {
      id: 31,
      title: 'Big Ben',
      location: 'London',
      image:
        'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/london-big-ben-and-westminster-2.800w.727b9f.avif',
      price: 286,
      rating: 5,
    },
    {
      id: 32,
      title: 'Tower Bridge',
      location: 'London',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/iconic-tower-bridge.800w.5679c9.avif',
      price: 264,
      rating: 5,
    },
    {
      id: 33,
      title: 'Buckingham',
      location: 'London',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/buckingham-palace-front-view.800w.35ecc6.avif',
      price: 312,
      rating: 5,
    },
    {
      id: 34,
      title: 'London Eye',
      location: 'London',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/london-eye-at-sunset.800w.c53f42.avif',
      price: 198,
      rating: 4,
    },
    {
      id: 35,
      title: 'Westminster',
      location: 'London',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/london-big-ben-and-westminster.800w.0f9ffe.avif',
      price: 245,
      rating: 5,
    },
    {
      id: 36,
      title: 'Thames Walk',
      location: 'London',
      image: 'https://assets.shadcnstore.com/shadcnstore.com/stock/marketing/historic-edinburgh-castle-on-hill.800w.209186.avif',
      price: 176,
      rating: 4,
    },
  ],
}

/** Flat list kept for consumers that still expect a single destinations array. */
export const destinations = destinationsByRegion['top-rated']
```
