Velocity UI
Loading…
Menu

Focus Cards

A grid of cards where hovering one card blurs and grayscales the others, creating a cinematic spotlight effect.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add focus-cards

Source Code

focus-cards.tsx
'use client'

import React, { useState } from 'react'
import { cn } from '@/lib/utils'

interface Card {
  id: string
  title: string
  subtitle: string
  image: string
}

const DEFAULT_CARDS: Card[] = [
  { id: '1', title: 'Kyoto', subtitle: 'The Spirit of Japan', image: 'https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?auto=format&fit=crop&q=80&w=800' },
  { id: '2', title: 'New York', subtitle: 'Metropolitan Pulse', image: 'https://images.unsplash.com/photo-1514565131-fce0801e5785?auto=format&fit=crop&q=80&w=800' }, // Clean, architectural NY
  { id: '3', title: 'Iceland', subtitle: 'Eternal Silence', image: 'https://images.unsplash.com/photo-1476610182048-b716b8518aae?auto=format&fit=crop&q=80&w=800' },
  { id: '4', title: 'Paris', subtitle: 'The Golden Hour', image: 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?auto=format&fit=crop&q=80&w=800' },
  { id: '5', title: 'Swiss Alps', subtitle: 'Peak Serenity', image: 'https://images.unsplash.com/photo-1531366936337-7c912a4589a7?auto=format&fit=crop&q=80&w=800' },
  { id: '6', title: 'Tokyo', subtitle: 'Futurist Dream', image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?auto=format&fit=crop&q=80&w=800' },
]

export default function AtelierFocusCards({ cards = DEFAULT_CARDS }: { cards?: Card[] }) {
  const [hovered, setHovered] = useState<string | null>(null)

  return (
    <div className="w-full min-h-screen bg-white dark:bg-[#0a0a0a] flex items-center justify-center p-4">
      <div className="grid grid-cols-1 md:grid-cols-3 gap-3 max-w-6xl w-full">
        {cards.map((card) => (
          <div
            key={card.id}
            onMouseEnter={() => setHovered(card.id)}
            onMouseLeave={() => setHovered(null)}
            className={cn(
              "relative aspect-[2/3] overflow-hidden bg-neutral-200 dark:bg-neutral-900",
              "transition-all duration-700 ease-[cubic-bezier(0.19,1,0.22,1)]",
              hovered !== null && hovered !== card.id && "opacity-30 grayscale-[0.5]"
            )}
          >
            {/* Image with slow-motion zoom */}
            <img 
              src={card.image} 
              alt={card.title}
              className={cn(
                "absolute inset-0 w-full h-full object-cover transition-transform duration-[1.5s] ease-out",
                hovered === card.id ? "scale-110" : "scale-100"
              )}
            />

            {/* Ultra-subtle overlay */}
            <div className={cn(
              "absolute inset-0 bg-black/20 transition-opacity duration-500",
              hovered === card.id ? "opacity-100" : "opacity-0"
            )} />

            {/* Minimalist Text Content */}
            <div className="absolute inset-x-0 bottom-0 p-8 flex flex-col justify-end">
               {/* Vertical line accent */}
               <div className={cn(
                "w-[1px] bg-white transition-all duration-700 mb-4 origin-bottom",
                hovered === card.id ? "h-12 opacity-100" : "h-0 opacity-0"
              )} />
              
              <h3 className={cn(
                "text-2xl font-light text-white tracking-tighter transition-all duration-500 delay-75",
                hovered === card.id ? "translate-x-0 opacity-100" : "-translate-x-4 opacity-0"
              )}>
                {card.title}
              </h3>
              
              <p className={cn(
                "text-[10px] uppercase tracking-[0.3em] text-neutral-300 mt-1 transition-all duration-500 delay-150",
                hovered === card.id ? "translate-x-0 opacity-100" : "-translate-x-2 opacity-0"
              )}>
                {card.subtitle}
              </p>
            </div>

            {/* Thin hairline border for light mode contrast */}
            <div className="absolute inset-0 border border-black/5 pointer-events-none" />
          </div>
        ))}
      </div>
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest

Props

Component property reference.

NameTypeDefaultDescription
cardsCard[]Demo CardsArray of card objects with title, subtitle, and image.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.