Goose UI
uianimated-timersource: ok · refreshed
Animated Timer
A beautiful animated clock with sliding digits and gradient border.
npx shadcn add "https://gooseui.pro/r/animated-timer.json"free
Visual style
gradient
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files1 · source cached 10/7/2026, 4:34:17 PM
registry/new-york/ui/animated-timer.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface TimerCharProps {
char: string
}
function TimerChar({ char }: TimerCharProps) {
const ref = React.useRef<HTMLDivElement>(null)
const [height, setHeight] = React.useState(0)
React.useEffect(() => {
if (ref.current) {
setHeight(ref.current.offsetHeight)
}
}, [])
if (char === ":") {
return (
<div className="flex items-center justify-center h-[60px] md:h-[100px] lg:h-[150px] w-5 md:w-10 lg:w-12">
<span className="text-white text-4xl md:text-6xl lg:text-8xl">:</span>
</div>
)
}
const number = parseInt(char, 10)
const top = height > 0 ? number * height * -1 : 0
return (
<div
ref={ref}
className="relative h-[60px] md:h-[100px] lg:h-[150px] w-[30px] md:w-[50px] lg:w-[80px] overflow-hidden"
>
<div
className="absolute left-0 flex flex-col w-full transition-[top] duration-200 ease-out"
style={{ top }}
>
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((digit) => (
<div
key={digit}
className="flex items-center justify-center h-[60px] md:h-[100px] lg:h-[150px] w-full"
>
<span
className={cn(
"text-white transition-all duration-300",
number === digit
? "text-4xl md:text-6xl lg:text-8xl opacity-100"
: "text-2xl md:text-3xl lg:text-4xl opacity-5",
)}
>
{digit}
</span>
</div>
))}
</div>
</div>
)
}
interface AnimatedTimerProps {
className?: string
showSeconds?: boolean
use24Hour?: boolean
}
export function AnimatedTimer({
className,
showSeconds = true,
use24Hour = true,
}: AnimatedTimerProps) {
const [time, setTime] = React.useState<Date | null>(null)
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
setTime(new Date())
}, [])
React.useEffect(() => {
if (!mounted) return
const interval = setInterval(() => {
setTime(new Date())
}, 1000)
return () => clearInterval(interval)
}, [mounted])
const formatSegment = (segment: number): string => {
return segment < 10 ? `0${segment}` : String(segment)
}
const getHours = (hours: number): number => {
if (use24Hour) return hours
return hours % 12 === 0 ? 12 : hours % 12
}
const getTimeString = (): string => {
if (!time) return showSeconds ? "00:00:00" : "00:00"
const hours = formatSegment(getHours(time.getHours()))
const minutes = formatSegment(time.getMinutes())
const seconds = formatSegment(time.getSeconds())
return showSeconds
? `${hours}:${minutes}:${seconds}`
: `${hours}:${minutes}`
}
const chars = getTimeString().split("")
// Show skeleton during SSR
if (!mounted) {
return (
<div
className={cn(
"rounded-xl md:rounded-2xl lg:rounded-[22px] bg-gradient-to-br from-blue-600 to-red-500 p-1",
className,
)}
>
<div className="flex items-center bg-zinc-900 rounded-lg md:rounded-xl lg:rounded-[20px] px-3 md:px-4 lg:px-5">
{chars.map((char, index) => (
<div
key={index}
className={cn(
"flex items-center justify-center",
char === ":"
? "h-[60px] md:h-[100px] lg:h-[150px] w-5 md:w-10 lg:w-12"
: "h-[60px] md:h-[100px] lg:h-[150px] w-[30px] md:w-[50px] lg:w-[80px]",
)}
>
<span className="text-white text-4xl md:text-6xl lg:text-8xl opacity-20">
{char}
</span>
</div>
))}
</div>
</div>
)
}
return (
<div
className={cn(
"rounded-xl md:rounded-2xl lg:rounded-[22px] bg-gradient-to-br from-blue-600 to-red-500 p-1",
className,
)}
>
<div className="flex items-center bg-zinc-900 rounded-lg md:rounded-xl lg:rounded-[20px] px-3 md:px-4 lg:px-5">
{chars.map((char, index) => (
<TimerChar key={index} char={char} />
))}
</div>
</div>
)
}