The Clock
Interactive tastefull clock widget.
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>
);
}