Velocity UI
Loading…
Menu

Magic Calendar

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

Preview

Live interactive preview.

Installation

Add this component to your project using the CLI:

terminal
npx -y vui-registry-cli-v1@latest 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