{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"clock","type":"registry:ui","title":"Clock","description":"An elegant analog clock with a smooth sweeping (or bouncy ticking) second hand, minute ticks, a digital readout and any IANA time zone.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/clock.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useId, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ClockProps {\n  /** IANA time zone, e.g. \"Asia/Tokyo\". Defaults to the viewer's zone. */\n  timeZone?: string;\n  /** Caption on the face. Defaults to the city from `timeZone`. */\n  label?: string;\n  /** Diameter in px. */\n  size?: number;\n  /** Continuous sweeping second hand; off = ticking with a small bounce. */\n  sweep?: boolean;\n  /** Show the second hand. */\n  showSeconds?: boolean;\n  /** Digital readout on the face. */\n  showDigital?: boolean;\n  /** 12h readout with AM/PM. */\n  hour12?: boolean;\n  /** Draw 12, 3, 6, 9 numerals. */\n  showNumbers?: boolean;\n  /** Second hand color. */\n  accent?: string;\n  className?: string;\n}\n\n/** Offset (ms) between the zone's wall clock and UTC at `now`. */\nfunction zoneOffset(timeZone: string | undefined, now: number) {\n  if (!timeZone) return -new Date(now).getTimezoneOffset() * 60000;\n  try {\n    const p = Object.fromEntries(\n      new Intl.DateTimeFormat(\"en-US\", {\n        timeZone,\n        hourCycle: \"h23\",\n        year: \"numeric\",\n        month: \"numeric\",\n        day: \"numeric\",\n        hour: \"numeric\",\n        minute: \"numeric\",\n        second: \"numeric\",\n      })\n        .formatToParts(new Date(now))\n        .map((x) => [x.type, +x.value]),\n    );\n    return Date.UTC(p.year, p.month - 1, p.day, p.hour % 24, p.minute, p.second) - (now - (now % 1000));\n  } catch {\n    return -new Date(now).getTimezoneOffset() * 60000;\n  }\n}\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst turn = (el: SVGGElement | null, deg: number) => {\n  if (el) el.style.transform = `rotate(${deg}deg)`;\n};\n/** Hands pivot on the face centre (100,100 in the 200 viewBox). */\nconst PIVOT = { transformBox: \"view-box\", transformOrigin: \"100px 100px\" } as const;\nconst TICKS = Array.from({ length: 60 }, (_, i) => i);\n\nexport function Clock({\n  timeZone,\n  label,\n  size = 280,\n  sweep = true,\n  showSeconds = true,\n  showDigital = true,\n  hour12 = false,\n  showNumbers = true,\n  accent = \"#ff5a36\",\n  className,\n}: ClockProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const hourRef = useRef<SVGGElement>(null);\n  const minRef = useRef<SVGGElement>(null);\n  const secRef = useRef<SVGGElement>(null);\n  const digitalRef = useRef<HTMLSpanElement>(null);\n  const handsRef = useRef<SVGGElement>(null);\n  const uid = useId().replace(/:/g, \"\");\n  const caption = label ?? (timeZone ? timeZone.split(\"/\").pop()!.replace(/_/g, \" \") : \"Local\");\n\n  useEffect(() => {\n    const el = root.current!;\n    const reduce = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const smooth = sweep && !reduce;\n    let offset = zoneOffset(timeZone, Date.now());\n    let offsetAt = Date.now();\n    let raf = 0;\n    let timer = 0;\n    let lastSec = -1;\n    let lastMin = -1;\n    let turns = 0; // full minutes elapsed, so the ticking hand never spins backwards at :00\n\n    const frame = () => {\n      const now = Date.now();\n      if (now - offsetAt > 60000) {\n        offset = zoneOffset(timeZone, now); // DST changes\n        offsetAt = now;\n      }\n      const t = now + offset;\n      const ms = ((t % 86400000) + 86400000) % 86400000;\n      const h = ms / 3600000;\n      const m = (ms % 3600000) / 60000;\n      const s = (ms % 60000) / 1000;\n      const whole = Math.floor(s);\n\n      if (whole !== lastSec) {\n        if (whole < lastSec) turns++;\n        lastSec = whole;\n        const H = Math.floor(h);\n        const M = Math.floor(m);\n        if (digitalRef.current) {\n          const hh = hour12 ? (H % 12 || 12) : H;\n          digitalRef.current.textContent = `${hour12 ? hh : pad(hh)}:${pad(M)}${showSeconds ? `:${pad(whole)}` : \"\"}${hour12 ? (H < 12 ? \" AM\" : \" PM\") : \"\"}`;\n        }\n        if (M !== lastMin) {\n          lastMin = M;\n          el.setAttribute(\"aria-label\", `${caption || timeZone || \"Local time\"}: ${hour12 ? H % 12 || 12 : pad(H)}:${pad(M)}${hour12 ? (H < 12 ? \" AM\" : \" PM\") : \"\"}`);\n        }\n        if (!smooth) turn(secRef.current, (whole + turns * 60) * 6);\n      }\n      turn(hourRef.current, (h % 12) * 30);\n      turn(minRef.current, m * 6);\n      if (smooth) turn(secRef.current, s * 6);\n    };\n\n    const loop = () => {\n      frame();\n      raf = requestAnimationFrame(loop);\n    };\n    const start = () => {\n      stop();\n      frame();\n      if (smooth) raf = requestAnimationFrame(loop);\n      else timer = window.setInterval(frame, 250);\n    };\n    const stop = () => {\n      cancelAnimationFrame(raf);\n      clearInterval(timer);\n    };\n\n    frame();\n    handsRef.current!.style.opacity = \"1\"; // hands stay hidden until they show the real time\n    const io = new IntersectionObserver(([e]) => (e.isIntersecting ? start() : stop()));\n    io.observe(el);\n    return () => {\n      stop();\n      io.disconnect();\n    };\n  }, [timeZone, sweep, hour12, showSeconds, caption]);\n\n  const tickHand = !sweep;\n\n  return (\n    <div\n      ref={root}\n      role=\"img\"\n      aria-label={caption || timeZone || \"Clock\"}\n      className={cn(\"relative shrink-0 rounded-full\", className)}\n      style={{ width: size, height: size }}\n    >\n      <svg viewBox=\"0 0 200 200\" className=\"size-full overflow-visible\" aria-hidden>\n        <defs>\n          <radialGradient id={`${uid}f`} cx=\"50%\" cy=\"38%\" r=\"70%\">\n            <stop offset=\"0%\" stopColor=\"var(--card)\" />\n            <stop offset=\"100%\" stopColor=\"var(--background)\" />\n          </radialGradient>\n          <filter id={`${uid}s`} filterUnits=\"userSpaceOnUse\" x=\"-20\" y=\"-20\" width=\"240\" height=\"240\">\n            <feDropShadow dx=\"0\" dy=\"1.6\" stdDeviation=\"1.4\" floodOpacity=\"0.35\" />\n          </filter>\n        </defs>\n\n        {/* bezel + face */}\n        <circle cx=\"100\" cy=\"100\" r=\"99\" fill=\"var(--border)\" />\n        <circle cx=\"100\" cy=\"100\" r=\"96.5\" fill={`url(#${uid}f)`} />\n        <circle cx=\"100\" cy=\"100\" r=\"96.5\" fill=\"none\" stroke=\"#000\" strokeOpacity=\"0.25\" strokeWidth=\"3\" style={{ filter: \"blur(2px)\" }} />\n\n        {TICKS.map((i) => {\n          const hour = i % 5 === 0;\n          return (\n            <line\n              key={i}\n              x1=\"100\"\n              y1={hour ? 9 : 10}\n              x2=\"100\"\n              y2={hour ? 21 : 15}\n              stroke=\"currentColor\"\n              strokeWidth={hour ? 2.6 : 1}\n              strokeLinecap=\"round\"\n              className={hour ? \"text-foreground\" : \"text-muted-foreground/50\"}\n              transform={`rotate(${i * 6} 100 100)`}\n            />\n          );\n        })}\n\n        {showNumbers &&\n          [12, 3, 6, 9].map((n, i) => {\n            const a = (i * Math.PI) / 2;\n            return (\n              <text\n                key={n}\n                x={100 + Math.sin(a) * 66}\n                y={100 - Math.cos(a) * 66}\n                textAnchor=\"middle\"\n                dominantBaseline=\"central\"\n                className=\"fill-foreground text-[17px] font-semibold tabular-nums\"\n              >\n                {n}\n              </text>\n            );\n          })}\n\n        <text x=\"100\" y=\"64\" textAnchor=\"middle\" className=\"fill-muted-foreground text-[8.5px] font-medium tracking-[0.2em] uppercase\">\n          {caption}\n        </text>\n\n        <g ref={handsRef} className=\"transition-opacity duration-500\" style={{ opacity: 0 }}>\n          {/* hour */}\n          <g ref={hourRef} filter={`url(#${uid}s)`} style={PIVOT}>\n            <line x1=\"100\" y1=\"112\" x2=\"100\" y2=\"52\" stroke=\"currentColor\" strokeWidth=\"6.5\" strokeLinecap=\"round\" className=\"text-foreground\" />\n          </g>\n          {/* minute */}\n          <g ref={minRef} filter={`url(#${uid}s)`} style={PIVOT}>\n            <line x1=\"100\" y1=\"114\" x2=\"100\" y2=\"24\" stroke=\"currentColor\" strokeWidth=\"4\" strokeLinecap=\"round\" className=\"text-foreground\" />\n          </g>\n          {/* second */}\n          {showSeconds && (\n            <g\n              ref={secRef}\n              filter={`url(#${uid}s)`}\n              style={tickHand ? { ...PIVOT, transition: \"transform .32s cubic-bezier(.4,2.2,.5,1)\" } : PIVOT}\n            >\n              <line x1=\"100\" y1=\"124\" x2=\"100\" y2=\"16\" stroke={accent} strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n              <circle cx=\"100\" cy=\"124\" r=\"3.4\" fill={accent} />\n            </g>\n          )}\n          <circle cx=\"100\" cy=\"100\" r=\"5\" fill={showSeconds ? accent : \"currentColor\"} className=\"text-foreground\" />\n          <circle cx=\"100\" cy=\"100\" r=\"1.8\" className=\"fill-card\" />\n        </g>\n      </svg>\n\n      {showDigital && (\n        <span\n          ref={digitalRef}\n          aria-hidden\n          className=\"absolute left-1/2 -translate-x-1/2 rounded-md bg-muted/80 px-2 py-0.5 font-mono font-medium text-foreground tabular-nums\"\n          style={{ top: \"66%\", fontSize: Math.max(10, size * 0.05) }}\n        >\n          --:--\n        </span>\n      )}\n    </div>\n  );\n}\n"}]}