Logo SmoothUI
ComponentsApple Invites

Apple Invites

Inspired by Apple's design, this component showcases a collection of event invites with smooth animations and transitions.

Movie Night
Interested
Participant 1

Movie Night

Fri, June 20, 8:00 PM

Cine Town

Yoga
Hosting
Participant 1

Yoga

Sat, June 14, 6:00 AM

Central Park

Tyler Turns 3!
Going
Participant 1

Tyler Turns 3!

Sat, June 14, 3:00 PM

Central Park

Code

Install with shadcn Beta

Terminal

npx shadcn@latest add "https://smoothui.dev/r/apple-invites.json"

Manual install

Terminal

npm install motion lucide-react popmotion

global.css

//Progressive Blur
.gradient-mask-t-0 {
    -webkit-mask-image: linear-gradient(#0000, #000);
    mask-image: linear-gradient(#0000, #000);
}

AppleInvites.tsx

"use client"

import { useEffect, useState } from "react"
import Image from "next/image"
import { Crown } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
import { wrap } from "popmotion"

interface Participant {
  avatar: string
}

interface Event {
  id: number
  title: string
  subtitle: string
  location: string
  image: string
  badge?: string
  participants: Participant[]
}

const events: Event[] = [
  {
    id: 1,
    title: "Yoga",
    subtitle: "Sat, June 14, 6:00 AM",
    location: "Central Park",
    image:
      "https://images.unsplash.com/photo-1593164842264-854604db2260?=jpg&fit=crop&w=640&q=80&fit=max",
    badge: "Hosting",
    participants: [{ avatar: "https://github.com/educlopez.png" }],
  },
  {
    id: 2,
    title: "Tyler Turns 3!",
    subtitle: "Sat, June 14, 3:00 PM",
    location: "Central Park",
    image:
      "https://images.unsplash.com/photo-1524293568345-75d62c3664f7?=jpg&fit=crop&w=640&q=80&fit=max",
    badge: "Going",
    participants: [{ avatar: "https://github.com/educlopez.png" }],
  },
  {
    id: 3,
    title: "Golf party",
    subtitle: "Sun, April 15, 9:00 AM",
    location: "Golf Park",
    image:
      "https://images.unsplash.com/photo-1592919505780-303950717480?=jpg&fit=crop&w=640&q=80&fit=max",
    badge: "Going",
    participants: [{ avatar: "https://github.com/educlopez.png" }],
  },
  {
    id: 4,
    title: "Movie Night",
    subtitle: "Fri, June 20, 8:00 PM",
    location: "Cine Town",
    image:
      "https://images.unsplash.com/photo-1571947453052-1e72ce0d19b9?=jpg&fit=crop&w=640&q=80&fit=max",
    badge: "Interested",
    participants: [{ avatar: "https://github.com/educlopez.png" }],
  },
]

const variants = {
  center: {
    x: "-50%",
    rotate: 0,
    scale: 1,
    opacity: 1,
    zIndex: 3,
    transition: { type: "spring", stiffness: 300, damping: 30 },
  },
  left: {
    x: "-130%",
    rotate: -12,
    scale: 0.9,
    opacity: 0.8,
    zIndex: 2,
    transition: { type: "spring", stiffness: 300, damping: 30 },
  },
  right: {
    x: "30%",
    rotate: 12,
    scale: 0.9,
    opacity: 0.8,
    zIndex: 2,
    transition: { type: "spring", stiffness: 300, damping: 30 },
  },
  hidden: {
    opacity: 0,
    zIndex: 1,
    transition: { duration: 0.3 },
  },
}

export default function AppleInvites() {
  const [[page, direction], setPage] = useState([0, 0])

  const activeIndex = wrap(0, events.length, page)

  useEffect(() => {
    const timer = setInterval(() => {
      setPage(([prevPage, prevDirection]) => [prevPage + 1, 1])
    }, 3000)

    return () => clearInterval(timer)
  }, [])

  const visibleEvents = [-1, 0, 1].map(
    (offset) => events[wrap(0, events.length, activeIndex + offset)]
  )

  return (
    <div className="relative flex h-full w-[1200px] items-center justify-center">
      <AnimatePresence initial={false} custom={direction}>
        {visibleEvents.map((event, index) => (
          <motion.div
            key={event.id}
            custom={direction}
            variants={variants}
            initial="hidden"
            animate={index === 1 ? "center" : index === 0 ? "left" : "right"}
            exit="hidden"
            className="absolute top-1/2 left-1/2 h-[250] w-[160px] origin-center -translate-y-1/2 md:h-[500px] md:w-[320px]"
          >
            <div className="relative h-full w-full overflow-hidden rounded-3xl">
              <Image
                src={event.image}
                alt={event.title}
                className="h-full w-full object-cover"
                fill
                sizes="(min-width: 640px) 320px, 100vw"
                priority
              />

              {/* Badge */}
              <div className="absolute top-4 left-4 z-3">
                <span className="flex flex-row items-center gap-2 rounded-full bg-black/30 px-3 py-1 text-xs font-medium text-white backdrop-blur-xl md:text-sm">
                  <Crown size={14} />
                  {event.badge}
                </span>
              </div>

              {/* Content */}
              <div className="absolute bottom-0 z-3 w-full overflow-hidden rounded-b-3xl p-6 text-white">
                {/* Participant Avatars */}
                <div className="mx-auto mb-2 flex items-center justify-center gap-2">
                  {event.participants.map((participant, idx) => (
                    <Image
                      key={idx}
                      src={participant.avatar}
                      alt={`Participant ${idx + 1}`}
                      width={36}
                      height={36}
                      className="w-6 rounded-full md:h-9 md:w-9"
                    />
                  ))}
                </div>
                <p className="text-md mb-1 text-center font-bold md:text-2xl">
                  {event.title}
                </p>
                <p className="text-center text-xs opacity-90 md:text-sm">
                  {event.subtitle}
                </p>
                <p className="text-center text-xs opacity-90 md:text-sm">
                  {event.location}
                </p>
              </div>

              <div className="fixed inset-x-0 bottom-0 isolate z-2 h-1/2">
                <div className="gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[1px]"></div>
                <div className="gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[2px]"></div>
                <div className="gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[3px]"></div>
                <div className="gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[6px]"></div>
                <div className="gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[12px]"></div>
              </div>
            </div>
          </motion.div>
        ))}
      </AnimatePresence>
    </div>
  )
}