Velocity UI
Loading…
Menu

Magic Calendar

An interactive calendar component with confetti effects and magic date triggers.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add magic-calendar

Source Code

magic-calendar.tsx
'use client'

import React, { useMemo, useState, useRef } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { ChevronLeft, ChevronRight, MapPin, Sparkles } from 'lucide-react'
import { cn } from '@/lib/utils'
import confetti from 'canvas-confetti'
import { MONTHS, YEARS, daysMatrix } from './magic-calendar-utils'

export default function MagicCalendar() {
  const [viewDate, setViewDate] = useState(new Date(2002, 4, 1))
  const [selectedDate, setSelectedDate] = useState<Date | null>(null)
  const [isPopupOpen, setIsPopupOpen] = useState(false)
  const [showDial, setShowDial] = useState<'month' | 'year' | null>(null)

  const y = viewDate.getFullYear()
  const m = viewDate.getMonth()
  const cells = useMemo(() => daysMatrix(y, m), [y, m])

  const fireConfetti = () => {
    confetti({
      particleCount: 150,
      spread: 70,
      origin: { y: 0.6 },
      colors: ['#38bdf8', '#818cf8', '#ffffff']
    });
  };

  const handleDateClick = (d: number) => {
    const newDate = new Date(y, m, d)
    setSelectedDate(newDate)
    setIsPopupOpen(true)
    if (d === 11 && m === 4 && y === 2002) fireConfetti();
  }

  const triggerMagic = () => {
    setViewDate(new Date(2002, 4, 1));
    setSelectedDate(new Date(2002, 4, 11));
    setIsPopupOpen(true);
    setTimeout(fireConfetti, 50);
  }

  const USER_IMAGE = "https://i.postimg.cc/3xgQH76g/Whats-App-Image-2026-02-19-at-8-23-43-PM.jpg";

  const isVikasBirthday = selectedDate?.getDate() === 11 && 
                          selectedDate?.getMonth() === 4 && 
                          selectedDate?.getFullYear() === 2002;

  return (
    <div className="relative flex flex-col items-center justify-center p-8 min-h-[700px] select-none font-sans">
      <div className="relative p-[3px] rounded-[34px] bg-gradient-to-b from-neutral-200 to-neutral-100 dark:from-neutral-800 dark:to-neutral-900 shadow-2xl">
        <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/50 to-transparent opacity-50 blur-sm" />
        <motion.div 
          className="relative w-full max-w-sm overflow-hidden rounded-[32px] bg-card/90 backdrop-blur-xl border border-white/20 shadow-inner"
        >
          {/* Inner Double Border Effect */}
          <div className="absolute inset-[3px] rounded-[29px] border border-neutral-100/50 dark:border-white/5 pointer-events-none z-0" />
        <div className="relative flex items-center justify-between p-6 z-30">
          <div className="relative space-y-1">
            <div className="flex gap-1 items-baseline">
                <button 
                  onClick={() => setShowDial(showDial === 'month' ? null : 'month')}
                  className={cn("text-xl font-semibold tracking-tight transition-all hover:text-sky-400 active:scale-95", showDial === 'month' && "text-sky-500")}
                >
                  {MONTHS[m]}
                </button>
                <button 
                  onClick={() => setShowDial(showDial === 'year' ? null : 'year')}
                  className={cn("text-sm font-bold opacity-30 transition-all hover:opacity-100 hover:text-sky-400 active:scale-95", showDial === 'year' && "opacity-100 text-sky-500")}
                >
                  {y}
                </button>
            </div>
            <p className="text-[10px] font-bold text-muted-foreground/50 uppercase tracking-[0.2em]">
                {showDial ? "Pick and close" : "Select a date"}
            </p>

            <AnimatePresence>
              {showDial && (
                <>
                  <motion.div 
                    initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
                    onClick={() => setShowDial(null)}
                    className="fixed inset-0 z-40" 
                  />
                  
                  <motion.div
                    initial={{ opacity: 0, y: 10, scale: 0.95 }}
                    animate={{ opacity: 1, y: 0, scale: 1 }}
                    exit={{ opacity: 0, y: 10, scale: 0.95 }}
                    className="absolute top-full left-0 mt-2 w-48 z-50 overflow-hidden rounded-2xl border border-white/10 bg-background/80 backdrop-blur-2xl shadow-2xl"
                  >
                    <div className="h-48 overflow-y-auto scrollbar-hide py-2 px-1">
                      {(showDial === 'month' ? MONTHS : YEARS).map((item, idx) => (
                        <button
                          key={item}
                          onClick={() => {
                            if (showDial === 'month') setViewDate(new Date(y, idx, 1));
                            else setViewDate(new Date(Number(item), m, 1));
                            setShowDial(null);
                          }}
                          className={cn(
                            "w-full px-4 py-2 text-left text-xs font-bold uppercase tracking-wider transition-colors rounded-lg mb-1",
                            (showDial === 'month' ? m === idx : y === Number(item)) 
                              ? "bg-sky-500 text-white" 
                              : "text-foreground/60 hover:bg-foreground/5 hover:text-foreground"
                          )}
                        >
                          {item}
                        </button>
                      ))}
                    </div>
                  </motion.div>
                </>
              )}
            </AnimatePresence>
          </div>

          <div className="flex gap-1 bg-foreground/5 p-1 rounded-full relative z-20">
            <button onClick={() => setViewDate(new Date(y, m - 1, 1))} className="p-2 hover:bg-background rounded-full transition-colors">
              <ChevronLeft className="w-4 h-4" />
            </button>
            <button onClick={() => setViewDate(new Date(y, m + 1, 1))} className="p-2 hover:bg-background rounded-full transition-colors">
              <ChevronRight className="w-4 h-4" />
            </button>
          </div>
        </div>

        <div className="px-6 pb-8 pt-2">
          <div className="grid grid-cols-7 mb-4">
            {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, idx) => (
              <span
                key={day + '-' + idx}
                className="text-center text-[10px] font-bold text-muted-foreground/40"
              >
                {day}
              </span>
            ))}
          </div>
          <div className="grid grid-cols-7 gap-y-2">
            {cells.map((cell) => {
              const isSelected = selectedDate?.getDate() === cell.day && selectedDate?.getMonth() === m && selectedDate?.getFullYear() === y;
              return (
                <div key={cell.key} className="relative flex items-center justify-center aspect-square">
                  {cell.day && (
                    <motion.button
                      whileHover={{ scale: 1.1 }}
                      whileTap={{ scale: 0.95 }}
                      onClick={() => handleDateClick(cell.day!)}
                      className={cn(
                        "relative z-10 w-10 h-10 rounded-2xl text-sm font-medium transition-colors flex items-center justify-center",
                        isSelected ? "text-background" : "text-foreground/70 hover:text-foreground"
                      )}
                    >
                      {cell.day}
                      {isSelected && (
                        <motion.div
                          layoutId="active-pill"
                          className="absolute inset-0 bg-foreground rounded-2xl -z-10 shadow-lg"
                        />
                      )}
                    </motion.button>
                  )}
                </div>
              )
            })}
          </div>
        </div>
      </motion.div>
      </div>

      <button 
        onClick={triggerMagic}
        className="mt-8 flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.3em] opacity-40 hover:opacity-100 hover:text-sky-400 transition-all group"
      >
        <Sparkles size={12} className="group-hover:scale-125 transition-transform text-sky-400" />
        Redirect to <span className="underline underline-offset-4 decoration-sky-500/30">11 May 2002</span>
      </button>

      <AnimatePresence>
        {isPopupOpen && selectedDate && (
          <div className="absolute inset-0 z-50 flex items-center justify-center p-4">
            <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={() => setIsPopupOpen(false)} className="absolute inset-0 bg-background/40 backdrop-blur-md" />
            <motion.div
              initial={{ scale: 0.9, opacity: 0, y: 10 }}
              animate={{ scale: 1, opacity: 1, y: 0 }}
              exit={{ scale: 0.9, opacity: 0, y: 10 }}
              transition={{ type: "spring", stiffness: 400, damping: 25 }}
              className={cn(
                "relative w-full max-w-[280px] overflow-hidden rounded-[44px] p-1 shadow-2xl",
                isVikasBirthday ? "bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 shadow-purple-500/40" : "bg-card border border-white/20"
              )}
            >
              {isVikasBirthday && (
                 <div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent skew-x-12 translate-x-[-150%] animate-shimmer pointer-events-none z-10" />
              )}
              <div className="absolute inset-0 rounded-[44px] border border-white/40 pointer-events-none z-20" />
              <div className="absolute inset-3 rounded-[34px] border border-dashed border-white/20 pointer-events-none z-20" />

              <div className="relative z-10">
                <div className={cn(
                  "p-8 text-center space-y-2 rounded-t-[40px] flex flex-col items-center",
                  isVikasBirthday ? "bg-white/10 text-white" : "bg-foreground text-background"
                )}>
                  {isVikasBirthday && (
                     <div className="w-20 h-20 rounded-full border-4 border-white/20 shadow-xl overflow-hidden mb-2 relative group">
                        <div className="absolute inset-0 bg-black/10 group-hover:bg-transparent transition-colors" />
                        <img src={USER_IMAGE} alt="Profile" className="w-full h-full object-cover scale-110" />
                     </div>
                  )}
                  <h3 className="text-6xl font-black tracking-tighter tabular-nums leading-none">{selectedDate.getDate()}</h3>
                  <p className="text-[10px] font-black uppercase tracking-[0.2em] opacity-60">
                    {MONTHS[selectedDate.getMonth()]} {selectedDate.getFullYear()}
                  </p>
                </div>
                
                <div className="p-8 space-y-4 text-center">
                  <p className={cn("text-base font-bold", isVikasBirthday ? "text-white" : "text-foreground")}>
                    {isVikasBirthday ? "Vikas Birthday 🎂" : "Date Selected"}
                  </p>
                  {isVikasBirthday && (
                    <div className="flex items-center justify-center gap-2 opacity-70 text-white">
                      <MapPin size={12} />
                      <span className="text-[10px] font-bold uppercase tracking-tight">Dehradun, India</span>
                    </div>
                  )}
                  <button
                    onClick={() => setIsPopupOpen(false)}
                    className={cn(
                      "w-full py-4 rounded-2xl text-[10px] font-black uppercase tracking-[0.2em] transition-all",
                      isVikasBirthday ? "bg-white text-sky-600 shadow-xl active:scale-95" : "bg-foreground/5 text-foreground hover:bg-foreground/10"
                    )}
                  >
                    Close
                  </button>
                </div>
              </div>
            </motion.div>
          </div>
        )}
      </AnimatePresence>
    </div>
  )
}

magic-calendar-utils.ts

magic-calendar-utils.ts
export const MONTHS = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December'
]

export const YEARS = Array.from({ length: 101 }, (_, i) => 1950 + i)

export const daysMatrix = (y: number, m: number) => {
  const first = new Date(y, m, 1).getDay()
  const last = new Date(y, m + 1, 0).getDate()
  const cells: { day: number | null; key: string }[] = []
  for (let i = 0; i < first; i++) cells.push({ day: null, key: `p-${i}` })
  for (let d = 1; d <= last; d++) cells.push({ day: d, key: `${y}-${m}-${d}` })
  return cells
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest
  • canvas-confetti: latest
  • date-fns: latest