Velocity UI
Loading…
Menu

Morphing Dialog

A card that smoothly expands into a full-screen dialog/modal.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add morphing-dialog

Source Code

morphing-dialog.tsx
'use client'

import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, Maximize2, ArrowRight } from 'lucide-react'

// Professional easing for a "smooth" feel
const transition = {
  type: 'spring',
  stiffness: 150,
  damping: 24,
  mass: 0.8,
}

export default function MorphingDialog() {
  const [isOpen, setIsOpen] = useState(false)

  // Close on Escape key
  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') setIsOpen(false)
    }
    window.addEventListener('keydown', onKeyDown)
    return () => window.removeEventListener('keydown', onKeyDown)
  }, [])

  return (
    <div className="w-full min-h-[600px] flex items-center justify-center relative overflow-hidden p-8 font-sans">
      <div className="absolute inset-0 bg-neutral-50/50 dark:bg-neutral-950/50" />
      <div className="absolute inset-0 bg-grid-black/[0.02] dark:bg-grid-white/[0.02] bg-[size:32px_32px]" />

      <AnimatePresence>
        {isOpen && (
          <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
            {/* Backdrop with a smoother blur fade */}
            <motion.div
              initial={{ opacity: 0, backdropFilter: 'blur(0px)' }}
              animate={{ opacity: 1, backdropFilter: 'blur(12px)' }}
              exit={{ opacity: 0, backdropFilter: 'blur(0px)' }}
              onClick={() => setIsOpen(false)}
              className="absolute inset-0 bg-white/40 dark:bg-black/60"
            />

            <motion.div
              layoutId="card-container"
              transition={transition}
              className="w-full max-w-2xl bg-white dark:bg-neutral-900 overflow-hidden shadow-[0_32px_64px_-16px_rgba(0,0,0,0.2)] dark:shadow-[0_32px_64px_-16px_rgba(0,0,0,0.6)] relative z-10 border border-neutral-200 dark:border-white/10"
              style={{ borderRadius: 32 }}
            >
              <motion.div layoutId="image-container" className="relative h-72 sm:h-80 overflow-hidden">
                <motion.img
                  layout
                  src="https://images.unsplash.com/photo-1617788138017-80ad40651399?q=80&w=2070&auto=format&fit=crop"
                  alt="Porsche 911"
                  className="w-full h-full object-cover"
                />
                
                <motion.button
                  initial={{ opacity: 0, scale: 0.8 }}
                  animate={{ opacity: 1, scale: 1 }}
                  exit={{ opacity: 0, scale: 0.8 }}
                  onClick={() => setIsOpen(false)}
                  className="absolute top-5 right-5 p-2.5 bg-white/20 hover:bg-white/30 text-white rounded-full backdrop-blur-xl transition-colors border border-white/20 z-20"
                >
                  <X size={20} />
                </motion.button>

                <div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-black/80 via-black/20 to-transparent">
                  <motion.h2 layoutId="title" className="text-3xl font-bold text-white">Porsche 911 GT3</motion.h2>
                  <motion.p layoutId="subtitle" className="text-neutral-200/90 text-sm mt-1">Precision. Power. Performance.</motion.p>
                </div>
              </motion.div>

              <motion.div 
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: 10 }}
                transition={{ delay: 0.1 }}
                className="p-8 space-y-6"
              >
                <div className="flex gap-3">
                   {['502 HP', '0-60 3.2s', '198 MPH'].map((spec) => (
                      <span key={spec} className="px-3 py-1.5 rounded-full bg-neutral-100 dark:bg-neutral-800 text-[11px] font-bold tracking-wider uppercase text-neutral-500 dark:text-neutral-400 border border-neutral-200 dark:border-white/5">
                        {spec}
                      </span>
                   ))}
                </div>
                
                <p className="text-neutral-600 dark:text-neutral-400 leading-relaxed font-light">
                  The 911 GT3 with Touring Package. It hides its performance potential. But not its ambition. A high-performance athlete that does not show off its talent, but loves to demonstrate it.
                </p>

                <div className="pt-2 flex gap-3">
                  <button className="flex-[2] px-6 py-4 bg-neutral-950 dark:bg-white text-white dark:text-black rounded-2xl font-bold hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-2">
                    Configure Now <ArrowRight size={18} />
                  </button>
                  <button className="flex-1 px-6 py-4 border border-neutral-200 dark:border-neutral-800 text-neutral-900 dark:text-white rounded-2xl font-bold hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors">
                    Specs
                  </button>
                </div>
              </motion.div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>

      {!isOpen && (
        <motion.div
          layoutId="card-container"
          transition={transition}
          onClick={() => setIsOpen(true)}
          whileHover={{ y: -4 }}
          whileTap={{ scale: 0.98 }}
          className="w-[380px] bg-white dark:bg-neutral-900 cursor-pointer group shadow-xl hover:shadow-2xl border border-neutral-200 dark:border-white/10 overflow-hidden"
          style={{ borderRadius: 32 }}
        >
          <motion.div layoutId="image-container" className="relative h-64 overflow-hidden">
            <motion.img
              layout
              src="https://images.unsplash.com/photo-1617788138017-80ad40651399?q=80&w=2070&auto=format&fit=crop"
              alt="Porsche 911"
              className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
            />
            <div className="absolute top-4 right-4 p-2 bg-black/40 text-white rounded-full backdrop-blur-md opacity-0 group-hover:opacity-100 transition-opacity">
              <Maximize2 size={16} />
            </div>
          </motion.div>
          
          <div className="p-6">
            <motion.h2 layoutId="title" className="text-xl font-bold text-neutral-900 dark:text-white">Porsche 911 GT3</motion.h2>
            <motion.p layoutId="subtitle" className="text-neutral-500 dark:text-neutral-400 text-sm">Precision. Power. Performance.</motion.p>
          </div>
        </motion.div>
      )}
    </div>
  )
}

Dependencies

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

Props

Component property reference.

NameTypeDefaultDescription
triggerReactNodenullThe element that opens the dialog.
contentReactNodenullThe content displayed inside the dialog.
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.