Random UI

CClock

Clock made of clocks, with tailwindcss and dark mode support

Tailwind CSS
Tailwind CSS

This component is based on its original codepen.

Demo

Installation

This component uses the use-hydration hook.

Copy and paste the following code into your project.

cclock.tsx
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useHydration } from '@/hooks/use-hydration';
import { cn } from '@/lib/utils';

const H  = { h: 0,   m: 180 },
      V  = { h: 270, m: 90 },
      TL = { h: 180, m: 270 },
      TR = { h: 0,   m: 270 },
      BL = { h: 180, m: 90 },
      BR = { h: 0,   m: 90 },
      E  = { h: 135, m: 135 };

const digits = [
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  V,  V,  V,
    V,  V,  V,  V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  BL, E,
    TR, BL, V,  E,
    E,  V,  V,  E,
    E,  V,  V,  E,
    BR, TL, TR, BL,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    BR, H,  TL, V,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    E,  BR, TL, V,
    E,  TR, BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, BL, BR, BL,
    V,  V,  V,  V,
    V,  TR, TL, V,
    TR, H,  BL, V,
    E,  E,  V,  V,
    E,  E,  TR, TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    TR, H,  BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    E,  E,  V,  V,
    E,  E,  V,  V,
    E,  E,  V,  V,
    E,  E,  TR, TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
];

const normalizeAngle = (next: number, prev: number) => {
  const delta = (((next - prev) % 360) + 360) % 360;
  return prev + delta;
};

const getTimeDigits = () => {
  const now = new Date();
  return [now.getHours(), now.getMinutes(), now.getSeconds()].flatMap((val) =>
    String(val).padStart(2, "0").split("").map(Number)
  );
};


interface ClockProps {
  h: number;
  m: number;
}

const Clock: React.FC<ClockProps> = ({ h, m }) => {
  const prev = useRef({ h: 0, m: 0 });
  const hourAngle = normalizeAngle(h, prev.current.h);
  const minuteAngle = normalizeAngle(m, prev.current.m);
  prev.current = { h: hourAngle, m: minuteAngle };

  const handStyle = {
    width: "47%",
    height: "3px",
  };

  return (
    <div
      className={cn(
        "size-[var(--clock-size)] relative rounded-full flex-shrink-0 border-2 max-[700px]:border max-[500px]:border",
        "border-white bg-gradient-to-br from-[#d0d0d0] via-white to-white",
        "dark:border-black dark:from-[#111111] dark:via-[#222222] dark:to-[#222222]",
        "shadow-[-2px_2px_6px_#d0d0d0,_2px_-2px_6px_#ffffff] dark:shadow-[-2px_2px_6px_#111111,_2px_-2px_6px_#222222]",
      )}
    >
      {/* Hour hand */}
      <div
        className="absolute bg-black dark:bg-white rounded-full transition-transform ease-in-out max-[500px]:w-1/2"
        style={{
          ...handStyle,
          top: "calc(50% - 1.5px)",
          left: "50%",
          transformOrigin: "0% 50%",
          transform: `rotate(${hourAngle}deg)`,
          transitionDuration: `0.4s`,
        }}
      />
      {/* Minute hand */}
      <div
        className="absolute bg-black dark:bg-white rounded-full transition-transform ease-in-out max-[500px]:w-1/2"
        style={{
          ...handStyle,
          top: "calc(50% - 1.5px)",
          left: "50%",
          transformOrigin: "0% 50%",
          transform: `rotate(${minuteAngle}deg)`,
          transitionDuration: `0.4s`,
        }}
      />
    </div>
  );
};


interface CClockProps {
  className?: string;
}

const CClock: React.FC<CClockProps> = ({ className }) => {
  const [time, setTime] = useState(Array(6).fill(0));

  useEffect(() => {
    let updateTimerId: NodeJS.Timeout;
    const updateTime = () => {
      setTime(getTimeDigits());
      const now = Date.now();
      const delay = 1000 - (now % 1000);
      updateTimerId = setTimeout(updateTime, delay);
    };

    const initialTimerId = setTimeout(() => {
      updateTime();
    }, 600);

    return () => {
      clearTimeout(updateTimerId);
      clearTimeout(initialTimerId);
    };
  }, []);

  const isMounted = useHydration();

  if (!isMounted) return null;

  return (
    <div
      className={cn("flex items-center justify-center h-screen font-sans text-center", className)}
      style={
        {
          "--clock-size": "3vw",
          "--gap": "calc(var(--clock-size) * 0.05)",
          "--clock-segment-w": "calc(var(--clock-size) * 4 + var(--gap) * 5)",
          "--clock-segment-h": "calc(var(--clock-size) * 6 + var(--gap) * 5)",
          gap: "var(--gap)",
          paddingLeft: "calc(var(--clock-size) + var(--gap) * 2)",
        } as React.CSSProperties
      }
    >
      {time.map((t, i) => (
        <div
          key={i}
          className="flex flex-wrap"
          style={
            {
              gap: "var(--gap)",
              width: "var(--clock-segment-w)",
              height: "var(--clock-segment-h)",
              marginRight: i % 2 === 1 ? "var(--clock-size)" : undefined,
            } as React.CSSProperties
          }
        >
          {digits[t].map(({ h, m }, j) => (
            <Clock key={j} h={h} m={m} />
          ))}
        </div>
      ))}
    </div>
  );
};

export default CClock;
cclock-counter.tsx
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { useHydration } from '@/hooks/use-hydration';
import { cn } from '@/lib/utils';

const H  = { a: 0,   b: 180 },  
      V  = { a: 270, b: 90 },   
      TL = { a: 180, b: 270 },
      TR = { a: 0,   b: 270 },
      BL = { a: 180, b: 90 },
      BR = { a: 0,   b: 90 },   
      E  = { a: 135, b: 135 }; 

const digits = [
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  V,  V,  V,
    V,  V,  V,  V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  BL, E,
    TR, BL, V,  E,
    E,  V,  V,  E,
    E,  V,  V,  E,
    BR, TL, TR, BL,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    BR, H,  TL, V,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    E,  BR, TL, V,
    E,  TR, BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, BL, BR, BL,
    V,  V,  V,  V,
    V,  TR, TL, V,
    TR, H,  BL, V,
    E,  E,  V,  V,
    E,  E,  TR, TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    TR, H,  BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, H,  TL,
    V,  TR, H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    TR, H,  BL, V,
    E,  E,  V,  V,
    E,  E,  V,  V,
    E,  E,  V,  V,
    E,  E,  TR, TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  H,  TL,
  ],
  [
    BR, H,  H,  BL,
    V,  BR, BL, V,
    V,  TR, TL, V,
    TR, H,  BL, V,
    BR, H,  TL, V,
    TR, H,  H,  TL,
  ],
];

const normalizeAngle = (next: number, prev: number) => {
  const delta = (((next - prev) % 360) + 360) % 360;
  return prev + delta;
};

interface DialProps {
  angle1: number;
  angle2: number;
}

const Dial: React.FC<DialProps> = ({ angle1, angle2 }) => {
  const prev = useRef({ angle1: 0, angle2: 0 });
  const normalizedAngle1 = normalizeAngle(angle1, prev.current.angle1);
  const normalizedAngle2 = normalizeAngle(angle2, prev.current.angle2);
  prev.current = { angle1: normalizedAngle1, angle2: normalizedAngle2 };

  const handStyle = {
    width: "47%",
    height: "3px",
  };

  return (
    <div
      className={cn(
        "size-[var(--dial-size)] relative rounded-full flex-shrink-0 border-2 max-[700px]:border max-[500px]:border",
        "border-white bg-gradient-to-br from-[#d0d0d0] via-white to-white",
        "dark:border-black dark:from-[#111111] dark:via-[#222222] dark:to-[#222222]",
        "shadow-[-2px_2px_6px_#d0d0d0,_2px_-2px_6px_#ffffff] dark:shadow-[-2px_2px_6px_#111111,_2px_-2px_6px_#222222]",
      )}
    >
      {/* First hand */}
      <div
        className="absolute bg-black dark:bg-white rounded-full transition-transform ease-in-out max-[500px]:w-1/2"
        style={{
          ...handStyle,
          top: "calc(50% - 1.5px)",
          left: "50%",
          transformOrigin: "0% 50%",
          transform: `rotate(${normalizedAngle1}deg)`,
          transitionDuration: `0.4s`,
        }}
      />
      {/* Second hand */}
      <div
        className="absolute bg-black dark:bg-white rounded-full transition-transform ease-in-out max-[500px]:w-1/2"
        style={{
          ...handStyle,
          top: "calc(50% - 1.5px)",
          left: "50%",
          transformOrigin: "0% 50%",
          transform: `rotate(${normalizedAngle2}deg)`,
          transitionDuration: `0.4s`,
        }}
      />
    </div>
  );
};

interface ClockCounterProps {
  value: number;
  className?: string;
}

const ClockCounter: React.FC<ClockCounterProps> = ({
  value,
  className,
}) => {
  const convertToDigits = (num: number) => {
    const absValue = Math.abs(Math.floor(num));
    const str = String(absValue);
    const targetLength = Math.max(1, str.length);
    return str.padStart(targetLength, "0").split("").map(Number);
  };

  const [displayValue, setDisplayValue] = useState(() =>
    convertToDigits(value)
  );

  useEffect(() => {
    setDisplayValue(convertToDigits(value));
  }, [value]);

  const isMounted = useHydration();

  if (!isMounted) return null;

  return (
    <div
      className={cn(`flex items-center justify-center font-sans text-center`, className)}
      style={
        {
          "--dial-size": "3vw",
          "--gap": "calc(var(--dial-size) * 0.05)",
          "--segment-w": "calc(var(--dial-size) * 4 + var(--gap) * 5)",
          "--segment-h": "calc(var(--dial-size) * 6 + var(--gap) * 5)",
          gap: "var(--gap)",
        } as React.CSSProperties
      }
    >
      {displayValue.map((digit, i) => (
        <div
          key={i}
          className="flex flex-wrap"
          style={
            {
              gap: "var(--gap)",
              width: "var(--segment-w)",
              height: "var(--segment-h)",
            } as React.CSSProperties
          }
        >
          {digits[digit].map(({ a, b }, j) => (
            <Dial key={j} angle1={a} angle2={b} />
          ))}
        </div>
      ))}
    </div>
  );
};

export default ClockCounter;

Usage

import CClock from "@/components/cclock";
<CClock />

Counter example:

import ClockCounter from "@/components/cclock-counter";
<ClockCounter value={value} />