Retro Clock.

A functional analog clock with a technical, industrial aesthetic using Framer Motion.

Interactive Preview

Installation Guide

** 01. Dependencies **
pnpm add motion motion
** 02. Source Code **

Create components/retro-clock.tsx and paste the code below:

"use client";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
export default function RetroClock() {
  const [time, setTime] = useState<Date | null>(null);
  const [initialTime, setInitialTime] = useState<Date | null>(null);
  const [location, setLocation] = useState({
    city: "...",
    region: "...",
    offset: "...",
  });
  useEffect(() => {
    const now = new Date();
    setInitialTime(now);
    setTime(now);
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);
    fetch("https://ipwho.is/")
      .then((res) => res.json())
      .then((data) => {
        if (data.success !== false) {
          const tz =
            data.timezone.id ||
            Intl.DateTimeFormat().resolvedOptions().timeZone;
          const [geoRegion] = tz.includes("/") ? tz.split("/") : ["UTC"];
          setLocation({
            city: data.city.toUpperCase(),
            region: geoRegion.toUpperCase(),
            offset: `UTC ${data.timezone.utc || ""}`,
          });
        } else {
          throw new Error("API Limit or Error");
        }
      })
      .catch(() => {
        const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
        const [reg, cit] = tz.includes("/")
          ? tz.split("/")
          : ["UTC", "Universal"];
        setLocation((prev) => ({
          ...prev,
          city: cit.toUpperCase().replace("_", " "),
          region: reg.toUpperCase().replace("_", " "),
        }));
      });
    return () => clearInterval(timer);
  }, []);
  if (!time || !initialTime) return null;
  const timeDifference = time.getTime() - initialTime.getTime();
  const secondsElapsed = timeDifference / 1000;
  const initialSeconds = initialTime.getSeconds();
  const initialMinutes = initialTime.getMinutes();
  const initialHours = initialTime.getHours();
  const secondDegrees = initialSeconds * 6 + secondsElapsed * 6;
  const minuteDegrees =
    initialMinutes * 6 + initialSeconds * 0.1 + secondsElapsed * 0.1;
  const hourDegrees =
    (initialHours % 12) * 30 +
    initialMinutes * 0.5 +
    secondsElapsed * (30 / 3600);
  const dateStr = time
    .toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
      year: "numeric",
    })
    .toUpperCase();
  const dayStr = time
    .toLocaleDateString("en-US", { weekday: "short" })
    .toUpperCase();
  const timeZoneAbbr = time
    .toLocaleTimeString("en-us", { timeZoneName: "short" })
    .split(" ")
    .pop();
  return (
    <div className="font-tech text-muted-foreground flex aspect-square h-full w-full items-center justify-center overflow-hidden tracking-[0.2em] uppercase">
      <div className="relative flex h-75 w-75 items-center justify-center text-[10px] font-medium">
        <div className="absolute top-[10%] left-1/2 -translate-x-1/2 text-center leading-tight">
          <span className="text-foreground block text-[1.2em] font-bold">
            {dayStr}
          </span>
          <span>{dateStr}</span>
        </div>
        <div className="absolute top-1/2 right-[5%] -translate-y-1/2 text-right">
          <span className="block text-[0.8em] opacity-70">REGION</span>
          <span className="text-foreground font-bold">{location.region}</span>
        </div>
        <div className="absolute top-1/2 left-[5%] -translate-y-1/2 text-left">
          <span className="block text-[0.8em] opacity-70">OFFSET</span>
          <span className="text-foreground font-bold">
            {location.offset || "..."}
          </span>
        </div>
        <div className="absolute bottom-[10%] left-1/2 -translate-x-1/2 text-center leading-tight">
          <span className="block text-[0.8em] opacity-70">LOCATION</span>
          <span className="text-foreground mb-1 block font-bold">
            {location.city}
          </span>
          <span className="text-[0.9em]">{timeZoneAbbr}</span>
        </div>
        <motion.div
          className="absolute w-0.5 origin-bottom bg-blue-600 dark:bg-blue-500"
          style={{ height: "36%", bottom: "50%", left: "calc(50% - 1px)" }}
          animate={{ rotate: minuteDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <motion.div
          className="absolute w-0.5 origin-top bg-blue-600 dark:bg-blue-500"
          style={{ height: "8%", top: "50%", left: "calc(50% - 1px)" }}
          animate={{ rotate: minuteDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <motion.div
          className="absolute w-0.5 origin-bottom bg-red-600 dark:bg-red-500"
          style={{ height: "30%", bottom: "50%", left: "calc(50% - 1px)" }}
          animate={{ rotate: secondDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <motion.div
          className="absolute w-0.5 origin-top bg-red-600 dark:bg-red-500"
          style={{ height: "6%", top: "50%", left: "calc(50% - 1px)" }}
          animate={{ rotate: secondDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <motion.div
          className="absolute w-1 origin-bottom rounded-full bg-neutral-900 dark:bg-white"
          style={{ height: "20%", bottom: "50%", left: "calc(50% - 2px)" }}
          animate={{ rotate: hourDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <motion.div
          className="absolute w-1 origin-top rounded-full bg-neutral-900 dark:bg-white"
          style={{ height: "5%", top: "50%", left: "calc(50% - 2px)" }}
          animate={{ rotate: hourDegrees }}
          transition={{ type: "spring", stiffness: 300, damping: 30 }}
        />
        <div
          className="absolute h-px w-[24%] bg-neutral-900 opacity-40 dark:bg-white"
          style={{ right: "50%" }}
        />
        <div
          className="pointer-events-none absolute h-full w-[0.5px] bg-neutral-900 opacity-10 dark:bg-white"
          style={{ left: "50%" }}
        />
        <div
          className="pointer-events-none absolute h-[0.5px] w-full bg-neutral-900 opacity-10 dark:bg-white"
          style={{ top: "50%" }}
        />
        <div className="absolute z-20 aspect-square w-[3.2%] rounded-full border-2 border-black bg-yellow-400 shadow-[0_0_10px_rgba(250,204,21,0.5)]" />
      </div>
    </div>
  );
}
** 03. Usage Example **
import RetroClock from "@/components/retro-clock";

export default function MyComponent() {
  return (
    <div className="h-[300px] w-full flex items-center justify-center">
        <RetroClock />
    </div>
  );
}