Velocity UI
Loading…
Menu

Lens Reveal Hero

A mysterious hero section where content is revealed by a flashlight-like lens following the cursor.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add lens-reveal-hero

Source Code

lens-reveal-hero.tsx

"use client"

import React, { useRef, useState } from "react"
import { motion, useMotionValue, useSpring } from "framer-motion"
import { Search } from "lucide-react"

export default function LensRevealHero() {
  const containerRef = useRef<HTMLDivElement>(null)
  const [hovered, setHovered] = useState(false)
  
  const x = useMotionValue(0)
  const y = useMotionValue(0)
  
  // Smooth spring physics for the lens movement
  const springX = useSpring(x, { stiffness: 150, damping: 15, mass: 0.1 })
  const springY = useSpring(y, { stiffness: 150, damping: 15, mass: 0.1 })

  function onMouseMove(e: React.MouseEvent<HTMLDivElement>) {
    const rect = containerRef.current?.getBoundingClientRect()
    if (!rect) return
    x.set(e.clientX - rect.left)
    y.set(e.clientY - rect.top)
    setHovered(true)
  }

  function onMouseLeave() {
    setHovered(false)
  }

  return (
    <div 
        ref={containerRef}
        className="relative w-full h-[600px] overflow-hidden bg-neutral-950 cursor-none group rounded-2xl shadow-2xl border border-neutral-800"
        onMouseMove={onMouseMove}
        onMouseLeave={onMouseLeave}
    >
        {/* Base Layer: Monochrome / Blurred */}
        <div className="absolute inset-0 z-0">
             <img 
                src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?q=80&w=1966&auto=format&fit=crop"
                alt="Supercar"
                className="w-full h-full object-cover filter grayscale blur-[2px] brightness-[0.4] scale-105"
             />
             <div className="absolute inset-0 bg-neutral-950/50" />
             
             {/* Text Content Base */}
             <div className="absolute inset-0 flex flex-col items-center justify-center text-center z-10 pointer-events-none p-4">
                <h1 className="text-6xl md:text-9xl font-black text-white/10 tracking-tighter uppercase select-none transition-colors duration-700">
                    VELOCITY
                </h1>
                <p className="mt-6 text-white/20 font-mono tracking-[0.5em] text-sm md:text-base uppercase select-none">
                    Experience the speed
                </p>
             </div>
        </div>

        {/* Reveal Layer: Full Color / Sharp */}
        <motion.div 
            className="absolute inset-0 z-20 pointer-events-none"
            style={{
                maskImage: 'radial-gradient(circle 200px at var(--x) var(--y), black 0%, transparent 100%)',
                WebkitMaskImage: 'radial-gradient(circle 200px at var(--x) var(--y), black 0%, transparent 100%)',
                // @ts-ignore
                '--x': springX,
                // @ts-ignore
                '--y': springY,
            }}
        >
             <img 
                src="https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?q=80&w=1966&auto=format&fit=crop"
                alt="Supercar"
                className="w-full h-full object-cover scale-105"
             />
             <div className="absolute inset-0 bg-gradient-to-t from-neutral-900/40 to-transparent mix-blend-overlay" />

             {/* Text Content Reveal */}
             <div className="absolute inset-0 flex flex-col items-center justify-center text-center z-10">
                <h1 className="text-6xl md:text-9xl font-black text-white tracking-tighter uppercase drop-shadow-[0_0_30px_rgba(255,255,255,0.3)] select-none">
                    VELOCITY
                </h1>
                <p className="mt-6 text-white font-mono tracking-[0.5em] text-sm md:text-base uppercase select-none drop-shadow-md">
                    Experience the speed
                </p>
             </div>
        </motion.div>

        {/* Custom Cursor Lens UI */}
        <motion.div
            className="absolute top-0 left-0 w-[400px] h-[400px] rounded-full z-30 pointer-events-none flex items-center justify-center"
            style={{ 
                x: springX, 
                y: springY,
                translateX: '-50%',
                translateY: '-50%',
                opacity: hovered ? 1 : 0,
                scale: hovered ? 1 : 0.5
            }}
        >
            {/* Glass Ring */}
            <div className="absolute inset-0 rounded-full border border-white/20 bg-white/5 backdrop-blur-[2px] shadow-[0_0_40px_rgba(255,255,255,0.1)]" />
            
            {/* Inner Ring */}
            <div className="absolute inset-[20px] rounded-full border border-white/10" />
            
            {/* Decorative Marks */}
            <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[2px] h-6 bg-white/50" />
            <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-[2px] h-6 bg-white/50" />
            <div className="absolute left-0 top-1/2 -translate-y-1/2 w-6 h-[2px] bg-white/50" />
            <div className="absolute right-0 top-1/2 -translate-y-1/2 w-6 h-[2px] bg-white/50" />
            
            {/* Center Crosshair */}
            <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
                <Search className="w-8 h-8 text-white/50" strokeWidth={1} />
            </div>
        </motion.div>

    </div>
  )
}

Dependencies

  • framer-motion: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
titlestring'VELOCITY'Main heading text.
subtitlestring'Experience the speed'Subheading text.
imageUrlstringundefinedBackground image URL.
lensSizenumber200Radius of the reveal lens in pixels.
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.