Animated Clock

A real-time clock with spring-animated digit transitions and 3D tilt effects.

Overview

A live clock component that displays the current time with smooth, spring-based digit transitions. Features mouse-tracking 3D tilt effects and glassmorphic styling for a premium appearance.


Features

  • Real-time updates every second
  • Spring physics for digit changes
  • Mouse-tracking 3D perspective tilt
  • 12-hour or 24-hour format support
  • Blur and scale entrance animations

Preview

Live Preview

Installation

Install via the ZenBlocks CLI to automatically handle dependencies:

npx shadcn@latest add https://zenblocks-three.vercel.app/r/animated-clock.json

Method 2: Manual

  1. Install Dependencies

    npm install framer-motion
  2. Copy the Source Code

    Copy the code below into components/zenblocks/animated-clock.tsx.

    Click to expand source
    "use client";
    
    import React, { useEffect, useState, useRef } from "react";
    import { motion, AnimatePresence, useMotionValue, useSpring, useTransform } from "framer-motion";
    import { cn } from "@/lib/utils";
    
    interface DigitProps {
        value: string | number;
        className?: string;
    }
    
    const Digit = ({ value, className }: DigitProps) => {
        return (
            <div className={cn("relative h-[1.2em] w-[0.6em] overflow-hidden flex items-center justify-center font-mono text-4xl md:text-6xl font-black tracking-tighter", className)}>
                <AnimatePresence mode="popLayout">
                    <motion.span
                        key={value}
                        initial={{ y: "100%", opacity: 0, filter: "blur(10px)", scale: 0.8 }}
                        animate={{ y: "0%", opacity: 1, filter: "blur(0px)", scale: 1 }}
                        exit={{ y: "-100%", opacity: 0, filter: "blur(10px)", scale: 0.8 }}
                        transition={{
                            type: "spring",
                            stiffness: 400,
                            damping: 30,
                            mass: 0.5,
                        }}
                        className="absolute inset-0 flex items-center justify-center"
                    >
                        {value}
                    </motion.span>
                </AnimatePresence>
            </div>
        );
    };
    
    const Separator = ({ className }: { className?: string }) => (
        <motion.div
            animate={{ opacity: [0.3, 1, 0.3] }}
            transition={{ duration: 1, repeat: Infinity, ease: "easeInOut" }}
            className={cn("text-4xl md:text-6xl font-bold px-1", className)}
        >
            :
        </motion.div>
    );
    
    
    export interface AnimatedClockProps {
        className?: string;
        /**
         * If true, displays time in 24-hour format (HH:MM:SS).
         * If false, displays in 12-hour format.
         * @default true
         */
        use24HourFormat?: boolean;
    }
    
    export const AnimatedClock = ({ className, use24HourFormat = true }: AnimatedClockProps) => {
        const [time, setTime] = useState(new Date());
        const [mounted, setMounted] = useState(false);
        const containerRef = useRef<HTMLDivElement>(null);
    
        // 3D Motion Values
        const x = useMotionValue(0);
        const y = useMotionValue(0);
    
        // Smooth springs for tilt
        const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [15, -15]), { stiffness: 100, damping: 20 });
        const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-15, 15]), { stiffness: 100, damping: 20 });
    
        useEffect(() => {
            setMounted(true);
            const timer = setInterval(() => {
                setTime(new Date());
            }, 1000);
            return () => clearInterval(timer);
        }, []);
    
        const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
            if (!containerRef.current) return;
            const rect = containerRef.current.getBoundingClientRect();
    
            // Normalize mouse position for 3D tilt (-0.5 to 0.5)
            const relativeX = (event.clientX - rect.left) / rect.width;
            const relativeY = (event.clientY - rect.top) / rect.height;
            x.set(relativeX - 0.5);
            y.set(relativeY - 0.5);
        };
    
        const handleMouseLeave = () => {
            x.set(0);
            y.set(0);
        };
    
        if (!mounted) {
            return (
                <div className={cn(
                    "group relative flex items-center justify-center gap-1 p-12 rounded-[3.5rem] bg-white/40 dark:bg-zinc-950/20 backdrop-blur-2xl border border-white/50 dark:border-white/5 shadow-2xl transition-all duration-500 w-[350px] h-[150px]",
                    className
                )} />
            );
        }
    
        let hours = time.getHours();
        if (!use24HourFormat) {
            hours = hours % 12 || 12; // Convert to 12h format
        }
    
        const hoursStr = hours.toString().padStart(2, "0");
        const minutesStr = time.getMinutes().toString().padStart(2, "0");
        const secondsStr = time.getSeconds().toString().padStart(2, "0");
    
        return (
            <motion.div
                ref={containerRef}
                onMouseMove={handleMouseMove}
                onMouseLeave={handleMouseLeave}
                style={{
                    rotateX,
                    rotateY,
                    transformStyle: "preserve-3d",
                }}
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                className={cn(
                    "group relative flex items-center justify-center gap-1 p-12 rounded-[3.5rem] bg-white/40 dark:bg-zinc-950/20 backdrop-blur-2xl border border-white/50 dark:border-white/5 shadow-2xl transition-all duration-500 w-fit cursor-default",
                    className
                )}
            >
                <div className="flex items-center" style={{ transform: "translateZ(50px)" }}>
                    <Digit value={hoursStr[0]} className="text-zinc-950 dark:text-white" />
                    <Digit value={hoursStr[1]} className="text-zinc-950 dark:text-white" />
                </div>
    
                <Separator className="text-zinc-400 dark:text-zinc-600" />
    
                <div className="flex items-center" style={{ transform: "translateZ(50px)" }}>
                    <Digit value={minutesStr[0]} className="text-zinc-950 dark:text-white" />
                    <Digit value={minutesStr[1]} className="text-zinc-950 dark:text-white" />
                </div>
    
                <Separator className="text-zinc-400 dark:text-zinc-600" />
    
                <div className="flex items-center" style={{ transform: "translateZ(50px)" }}>
                    <Digit value={secondsStr[0]} className="text-zinc-500 opacity-50" />
                    <Digit value={secondsStr[1]} className="text-zinc-500 opacity-50" />
                </div>
            </motion.div>
        );
    };

Usage

import { AnimatedClock } from "@/components/zenblocks/animated-clock";

<AnimatedClock />

Props

PropTypeDefaultDescription
use24HourFormatbooleantrueDisplay time in 24-hour format
classNamestring-Additional CSS classes

Accessibility

  • Time updates are not announced to screen readers (would be too verbose)
  • Component uses semantic HTML structure
  • Respects prefers-reduced-motion for tilt effects
  • High contrast maintained in both light and dark modes

Customization

12-Hour Format

<AnimatedClock use24HourFormat={false} />

Custom Styling

<AnimatedClock className="scale-150" />

Motion Behavior

  • Digit transitions: Spring animation with stiffness: 400, damping: 30 for snappy changes
  • 3D tilt: Mouse position maps to rotateX and rotateY transforms (±15deg range)
  • Entrance: Fades in with upward motion on mount

The separator colons pulse with opacity changes for visual rhythm.


Performance Notes

  • Uses setInterval for time updates (cleaned up on unmount)
  • Tilt calculations use useMotionValue and useSpring for smooth interpolation
  • Server-side rendering safe with hydration guard

Examples

Centered Display

<div className="flex items-center justify-center min-h-screen">
  <AnimatedClock />
</div>

Notes

  • Clock automatically adjusts to user's local timezone
  • Seconds display at reduced opacity for visual hierarchy
  • Component is client-side only due to time updates