The Clock

Interactive tastefull clock widget.

Live Preview
Open in

29M 59S

SALE ENDS IN

Motion

Code

"use client";

import { useState, useEffect } from "react";
import { motion, animate, useMotionValue, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";

// Orange Indicator
function OrangeIndicator() {
  return (
    <svg
      viewBox="0 0 300 100"
      xmlns="http://www.w3.org/2000/svg"
      className="absolute -right-2 top-1/2 -translate-y-1/2 w-20 text-orange-500 z-20 drop-shadow-xl"
    >
      <defs>
        <linearGradient id="orangeGradient" x1="0%" y1="0%" x2="100%" y2="0%">
          <stop offset="0%" stopColor="#ffc066" />
          <stop offset="100%" stopColor="#ff6600" />
        </linearGradient>
      </defs>

      <path
        d="M10,50 
             a10,10 0 0 1 10,-10 
             L290,30 
             L290,70 
             L20,60 
             a10,10 0 0 1 -10,-10 
             Z"
        fill="url(#orangeGradient)"
      />
    </svg>
  );
}

// Animated Lines for Clock
function AnimatedLines({ count }: { count: number }) {
  const circleAngle = 360;
  const totalLines = 40;
  const lineAngle = circleAngle / totalLines;

  const lines = Array.from({ length: totalLines }, (_, i) => ({
    id: i,
    angle: i * lineAngle,
  }));

  const rotation = useMotionValue(0);

  useEffect(() => {
    const currentRotation = rotation.get();
    animate(rotation, currentRotation - lineAngle, {
      type: "spring",
      duration: 0.75,
      bounce: 0.5,
      onComplete: () => {
        if (rotation.get() <= -circleAngle) {
          rotation.set(0);
        }
      },
    });
  }, [count]);

  return (
    <motion.div
      className="absolute inset-0 origin-center"
      style={{ rotate: rotation }}
    >
      {lines.map((line) => (
        <div
          key={line.id}
          className={cn(
            "absolute top-1/2 left-1/2 w-[70px] h-[1px] origin-left opacity-50 bg-neutral-600",
            line.id % 5 === 0 && "bg-white"
          )}
          style={{
            transform: `rotate(${line.angle}deg)`,
          }}
        />
      ))}
    </motion.div>
  );
}

// Clock Component
function Clock() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount((c) => (c + 1) % 40);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="relative w-24 h-full isolate overflow-visible flex items-center">
      <div className="absolute w-full h-full left-[60%]">
        <AnimatedLines count={count} />
      </div>

      <OrangeIndicator />

      <div className="absolute -right-[30%] top-1/2 -translate-y-1/2 size-8 rounded-full bg-gradient-to-br from-neutral-950 to-neutral-700 blur-sm z-0" />
      <div className="absolute -right-2 top-1/2 -translate-y-1/2 size-6 rounded-full bg-gradient-to-br from-white to-neutral-500 z-10 shadow-md" />
      <div className="absolute inset-y-0 right-0 w-20 bg-gradient-to-l from-neutral-900 from-5% to-transparent z-5" />
    </div>
  );
}

// Countdown Component
function Countdown({ isHovered }: { isHovered: boolean }) {
  const [count, setCount] = useState(1799);

  useEffect(() => {
    const id = setInterval(() => {
      setCount((c) => {
        if (c === 0) {
          return 60;
        }
        return c - 1;
      });
    }, 1000);

    return () => {
      clearInterval(id);
    };
  }, []);

  const minutes = Math.floor(count / 60);
  const seconds = count % 60;

  return (
    <div className="flex items-center gap-x-4 text-3xl">
      <p className="font-black text-neutral-500">
        <motion.span layout>
          <AnimatePresence mode="popLayout" initial={false}>
            {minutes
              .toString()
              .padStart(2, "0")
              .split("")
              .map((digit, index) => (
                <motion.span
                  className="inline-block tabular-nums"
                  key={`${digit}-${index}`}
                  initial={{ y: "8px", filter: "blur(2px)", opacity: 0 }}
                  animate={{ y: "0", filter: "blur(0px)", opacity: 1 }}
                  exit={{ y: "-8px", filter: "blur(2px)", opacity: 0 }}
                  transition={{ type: "spring", bounce: 0.35 }}
                >
                  {digit}
                </motion.span>
              ))}
          </AnimatePresence>
        </motion.span>
        <motion.span
          className="text-white"
          animate={{
            textShadow: isHovered
              ? "0 0 8px rgba(255, 255, 255, 0.5)"
              : "0 0 0px rgba(255, 255, 255, 0)",
          }}
          transition={{ duration: 0.3 }}
        >
          M
        </motion.span>{" "}
        <motion.span layout>
          <AnimatePresence mode="popLayout" initial={false}>
            {seconds
              .toString()
              .padStart(2, "0")
              .split("")
              .map((digit, index) => (
                <motion.span
                  className="inline-block tabular-nums"
                  key={`${digit}-${index}`}
                  initial={{ y: "8px", filter: "blur(2px)", opacity: 0 }}
                  animate={{ y: "0", filter: "blur(0px)", opacity: 1 }}
                  exit={{ y: "-8px", filter: "blur(2px)", opacity: 0 }}
                  transition={{ type: "spring", bounce: 0.35 }}
                >
                  {digit}
                </motion.span>
              ))}
          </AnimatePresence>
        </motion.span>
        <motion.span
          className="text-white pl-px"
          animate={{
            textShadow: isHovered
              ? "0 0 8px rgba(255, 255, 255, 0.5)"
              : "0 0 0px rgba(255, 255, 255, 0)",
          }}
          transition={{ duration: 0.3 }}
        >
          S
        </motion.span>
      </p>
    </div>
  );
}

// Main Component
export default function TheClock() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <motion.div
      className="flex items-center justify-between rounded-2xl w-[320px] h-[100px] relative overflow-hidden bg-[#171717]"
      onHoverStart={() => setIsHovered(true)}
      onHoverEnd={() => setIsHovered(false)}
    >
      {/* Cool minimal blurry background effect */}
      <motion.div
        className="absolute inset-0"
        style={{
          background:
            "radial-gradient(circle at 20% 50%, rgba(255, 102, 0, 0.15) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.08) 0%, transparent 40%)",
          backdropFilter: "blur(0px)",
        }}
        animate={{
          backdropFilter: isHovered ? "blur(1px)" : "blur(0px)",
          opacity: isHovered ? 0.6 : 0.3,
        }}
        transition={{ duration: 0.4 }}
      />

      {/* Left Side: Countdown */}
      <div className="flex flex-col justify-center relative z-10 ml-4">
        <Countdown isHovered={isHovered} />
        <p className="text-neutral-500 text-sm font-medium mt-1">SALE ENDS IN</p>
      </div>

      {/* Right Side: Clock */}
      {/* <div className="flex items-center justify-end h-full flex-shrink-0 overflow-hidden"> */}
        <Clock />
      {/* </div> */}
    </motion.div>
  );
}