{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "slug": "flip-clock",
  "name": "Flip Clock",
  "title": "Flip Clock",
  "type": "registry:ui",
  "description": "A realistic hinged flip clock and countdown with smooth 3D motion, moving shadows, and configurable time displays.",
  "summary": "Split panels fold through a two-stage GSAP timeline with gravity-like acceleration, a soft landing, and synchronized shadows. Switch between live time and countdowns, customize 12/24-hour formatting, seconds and AM/PM, and tune the panel geometry and flip motion.",
  "category": "clocks-and-timers",
  "tags": [
    "clock",
    "countdown",
    "timer",
    "flip",
    "time",
    "3d",
    "gsap",
    "animation"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-02",
  "updatedDate": "2026-10-03",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Two-stage hinged flip with accelerating departure and a damped landing",
    "Synchronized moving shadows and optional metal hinges",
    "Live clocks, fixed reference times and relative or absolute countdowns",
    "Seconds, days, 12/24-hour display and correct AM/PM formatting",
    "Pause and resume without resetting relative countdown time",
    "Responsive panels with stable tabular alignment",
    "Reduced-motion support and a single accessible time description"
  ],
  "anatomy": [
    "<FlipClock> time source and responsive panel layout",
    "<FlipUnit> static upper and lower faces",
    "Outgoing and incoming hinged flaps with animated shading",
    "AM/PM indicator and optional unit labels",
    "Optional center seams, hinges and colon separators"
  ],
  "physics": {
    "engine": "GSAP 3D timelines",
    "description": "The outgoing half accelerates through a 90-degree rotation around the central hinge. At the midpoint, the incoming half takes over and settles flat with configurable overshoot. Shading follows the rotation. Only changed panels animate.",
    "parameters": [
      {
        "label": "Flip duration",
        "value": "0.65s"
      },
      {
        "label": "Departure / landing",
        "value": "46% / 54%"
      },
      {
        "label": "Settling bounce",
        "value": "0.12"
      },
      {
        "label": "Perspective",
        "value": "900px"
      },
      {
        "label": "Moving shadow",
        "value": "0.28"
      }
    ]
  },
  "accessibility": {
    "role": "timer",
    "aria": "The timer exposes a single formatted time label. Duplicate visual faces are hidden from assistive technology. Time ticks do not trigger live announcements; completion has a separate polite status.",
    "keyboard": [
      {
        "key": "None",
        "description": "The clock is not interactive and has no focusable elements."
      }
    ],
    "reducedMotion": "Respects prefers-reduced-motion and updates the time immediately without rotations or moving shadows."
  },
  "guidelines": {
    "recommended": [
      "Launch countdowns and event timers",
      "Desk clocks and time-zone displays"
    ],
    "bestPractices": [
      "Use targetDate with an explicit timezone offset for a shared deadline.",
      "Use duration for a pauseable countdown; change duration or remount with a new key to restart.",
      "Provide onComplete for completion actions.",
      "Keep flipDuration below one second so the animation settles between ticks.",
      "The display resynchronizes when the tab becomes visible; elapsed time comes from timestamps rather than counted interval ticks."
    ]
  },
  "props": [
    {
      "name": "mode",
      "type": "\"clock\" | \"countdown\"",
      "description": "Choose a live or controlled clock, or a countdown.",
      "defaultValue": "\"clock\""
    },
    {
      "name": "duration",
      "type": "number",
      "description": "Relative countdown duration in seconds, clamped to 0–863999999. Changing it starts a new countdown.",
      "defaultValue": "3600"
    },
    {
      "name": "targetDate",
      "type": "Date | string | number",
      "description": "Optional absolute countdown deadline. ISO strings with a timezone offset are recommended. Resuming recomputes the deadline; it continues to elapse while paused."
    },
    {
      "name": "date",
      "type": "Date | string | number",
      "description": "Optional controlled clock time. Omit for a live clock. Invalid dates show dashes."
    },
    {
      "name": "running",
      "type": "boolean",
      "description": "Pause or resume. Relative countdowns preserve their exact remaining time; live clocks resynchronize on resume.",
      "defaultValue": "true"
    },
    {
      "name": "hourFormat",
      "type": "\"12\" | \"24\"",
      "description": "Clock hour format. Midnight and noon display 12 AM and 12 PM in 12-hour mode.",
      "defaultValue": "\"12\""
    },
    {
      "name": "timeZone",
      "type": "string",
      "description": "IANA time zone for clock mode. Defaults to local time; invalid zones fall back to local time."
    },
    {
      "name": "showSeconds",
      "type": "boolean",
      "description": "Display the seconds panel.",
      "defaultValue": "false (clock), true (countdown)"
    },
    {
      "name": "showPeriod",
      "type": "boolean",
      "description": "Display AM or PM in the hour panel when using a 12-hour clock.",
      "defaultValue": "true"
    },
    {
      "name": "showDays",
      "type": "boolean",
      "description": "Include a days panel in countdown mode. Without days, the hours panel shows total hours.",
      "defaultValue": "false"
    },
    {
      "name": "showLabels",
      "type": "boolean",
      "description": "Display labels below the panels.",
      "defaultValue": "false"
    },
    {
      "name": "padHours",
      "type": "boolean",
      "description": "Pad 12-hour clock hours with a leading zero. 24-hour clocks and countdowns always pad to two digits.",
      "defaultValue": "false"
    },
    {
      "name": "flipping",
      "type": "boolean",
      "description": "Enable the hinged flip animation. Disabled and reduced-motion displays update immediately.",
      "defaultValue": "true"
    },
    {
      "name": "flipDirection",
      "type": "\"down\" | \"up\"",
      "description": "Choose the direction the flap rotates around its center hinge.",
      "defaultValue": "\"down\""
    },
    {
      "name": "flipDuration",
      "type": "number",
      "description": "Total flip duration in seconds, clamped to 0.15–0.9 so each flip settles before the next second.",
      "defaultValue": "0.65"
    },
    {
      "name": "bounce",
      "type": "number",
      "description": "Small settling overshoot, clamped to 0–0.4.",
      "defaultValue": "0.12"
    },
    {
      "name": "shadowIntensity",
      "type": "number",
      "description": "Maximum moving flap shadow opacity, clamped to 0–0.7.",
      "defaultValue": "0.28"
    },
    {
      "name": "perspective",
      "type": "number",
      "description": "3D perspective distance in pixels, clamped to 250–2000.",
      "defaultValue": "900"
    },
    {
      "name": "cardSize",
      "type": "number",
      "description": "Maximum panel width in pixels, clamped to 64–600. Panels scale together to fit the container.",
      "defaultValue": "280"
    },
    {
      "name": "gap",
      "type": "number",
      "description": "Maximum spacing between panels in pixels, clamped to 0–48. Scales with the layout.",
      "defaultValue": "16"
    },
    {
      "name": "borderRadius",
      "type": "number",
      "description": "Maximum corner radius in pixels, clamped to 0–40 and scaled with smaller panels.",
      "defaultValue": "14"
    },
    {
      "name": "showSeparator",
      "type": "boolean",
      "description": "Display a colon centered between panels.",
      "defaultValue": "false"
    },
    {
      "name": "showHinges",
      "type": "boolean",
      "description": "Display subtle metal hinges at the center seam.",
      "defaultValue": "false"
    },
    {
      "name": "theme",
      "type": "\"dark\" | \"light\"",
      "description": "Default panel and number palette.",
      "defaultValue": "\"dark\""
    },
    {
      "name": "panelColor",
      "type": "string",
      "description": "Override the panel background colour."
    },
    {
      "name": "textColor",
      "type": "string",
      "description": "Override the number and label colour."
    },
    {
      "name": "fontFamily",
      "type": "string",
      "description": "Number font family. Tabular figures prevent horizontal jitter.",
      "defaultValue": "\"Arial, Helvetica, sans-serif\""
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional classes for the clock root."
    },
    {
      "name": "ariaLabel",
      "type": "string",
      "description": "Override the accessible time description."
    },
    {
      "name": "onComplete",
      "type": "() => void",
      "description": "Called once when a countdown reaches zero, including when an absolute deadline is already past. Remount with a new key to restart an unchanged duration."
    }
  ],
  "usage": "import { FlipClock } from \"@/components/ui/flip-clock\";\n\nexport function FlipClockDemo() {\n  return <FlipClock mode=\"countdown\" duration={300} showSeconds />;\n}",
  "files": [
    {
      "name": "flip-clock.tsx",
      "path": "registry/ui/flip-clock.tsx",
      "code": "\"use client\";\n\nimport * as React from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\nimport { useTimeSource } from \"./use-time-source\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type FlipClockMode = \"clock\" | \"countdown\";\nexport type FlipDirection = \"down\" | \"up\";\n\nexport interface FlipClockProps {\n  mode?: FlipClockMode;\n  duration?: number;\n  targetDate?: Date | string | number;\n  date?: Date | string | number;\n  running?: boolean;\n  hourFormat?: \"12\" | \"24\";\n  timeZone?: string;\n  showSeconds?: boolean;\n  showPeriod?: boolean;\n  showDays?: boolean;\n  showLabels?: boolean;\n  padHours?: boolean;\n  flipping?: boolean;\n  flipDirection?: FlipDirection;\n  flipDuration?: number;\n  bounce?: number;\n  shadowIntensity?: number;\n  perspective?: number;\n  cardSize?: number;\n  gap?: number;\n  borderRadius?: number;\n  showSeparator?: boolean;\n  showHinges?: boolean;\n  theme?: \"dark\" | \"light\";\n  panelColor?: string;\n  textColor?: string;\n  fontFamily?: string;\n  className?: string;\n  ariaLabel?: string;\n  onComplete?: () => void;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\ntype FlipUnitProps = {\n  value: string;\n  label: string;\n  period?: string;\n  separator: boolean;\n  options: { reducedMotion: boolean } & Required<\n    Pick<\n      FlipClockProps,\n      | \"flipping\"\n      | \"flipDirection\"\n      | \"flipDuration\"\n      | \"bounce\"\n      | \"shadowIntensity\"\n      | \"perspective\"\n      | \"borderRadius\"\n      | \"showHinges\"\n      | \"showLabels\"\n      | \"fontFamily\"\n      | \"cardSize\"\n    >\n  >;\n};\n\nfunction FlipUnit({ value, label, period, separator, options }: FlipUnitProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const previousRef = React.useRef<string | null>(null);\n  const {\n    flipping,\n    flipDirection,\n    flipDuration,\n    bounce,\n    shadowIntensity,\n    perspective,\n    borderRadius,\n    showHinges,\n    showLabels,\n    fontFamily,\n    cardSize,\n    reducedMotion,\n  } = options;\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root) return;\n      const top = root.querySelector<HTMLElement>(\"[data-flip-static-top]\");\n      const bottom = root.querySelector<HTMLElement>(\n        \"[data-flip-static-bottom]\",\n      );\n      const outgoing = root.querySelector<HTMLElement>(\"[data-flip-outgoing]\");\n      const incoming = root.querySelector<HTMLElement>(\"[data-flip-incoming]\");\n      const outgoingNumber =\n        outgoing?.querySelector<HTMLElement>(\"[data-flip-number]\");\n      const incomingNumber =\n        incoming?.querySelector<HTMLElement>(\"[data-flip-number]\");\n      const outgoingShade =\n        outgoing?.querySelector<HTMLElement>(\"[data-flip-shade]\");\n      const incomingShade =\n        incoming?.querySelector<HTMLElement>(\"[data-flip-shade]\");\n      if (\n        !top ||\n        !bottom ||\n        !outgoing ||\n        !incoming ||\n        !outgoingNumber ||\n        !incomingNumber ||\n        !outgoingShade ||\n        !incomingShade\n      )\n        return;\n      const oldValue = previousRef.current;\n      previousRef.current = value;\n      const reset = () => {\n        top.textContent = value;\n        bottom.textContent = value;\n        gsap.set([outgoing, incoming], { visibility: \"hidden\", rotationX: 0 });\n      };\n      reset();\n      if (\n        !flipping ||\n        reducedMotion ||\n        oldValue === null ||\n        oldValue === \"--\" ||\n        oldValue === value ||\n        value === \"--\"\n      )\n        return;\n      const down = flipDirection === \"down\";\n      const time = bounded(flipDuration, 0.65, 0.15, 0.9);\n      const shade = bounded(shadowIntensity, 0.28, 0, 0.7);\n      (down ? bottom : top).textContent = oldValue;\n      outgoingNumber.textContent = oldValue;\n      incomingNumber.textContent = value;\n      gsap.set(outgoing, { visibility: \"visible\", rotationX: 0 });\n      gsap.set(incoming, {\n        visibility: \"hidden\",\n        rotationX: down ? 90 : -90,\n      });\n      gsap.set(outgoingShade, { opacity: 0 });\n      gsap.set(incomingShade, { opacity: shade });\n      const settle = bounded(bounce, 0.12, 0, 0.4);\n      gsap\n        .timeline()\n        .to(\n          outgoing,\n          {\n            rotationX: down ? -90 : 90,\n            duration: time * 0.46,\n            ease: \"power2.in\",\n          },\n          0,\n        )\n        .to(\n          outgoingShade,\n          { opacity: shade, duration: time * 0.46, ease: \"power1.in\" },\n          0,\n        )\n        .set(outgoing, { visibility: \"hidden\" }, time * 0.46)\n        .set(incoming, { visibility: \"visible\" }, time * 0.46)\n        .to(\n          incoming,\n          {\n            rotationX: 0,\n            duration: time * 0.54,\n            ease: settle ? `back.out(${settle * 5})` : \"power3.out\",\n          },\n          time * 0.46,\n        )\n        .to(\n          incomingShade,\n          { opacity: 0, duration: time * 0.54, ease: \"power2.out\" },\n          time * 0.46,\n        )\n        .set([outgoing, incoming], { visibility: \"hidden\", rotationX: 0 }, time)\n        .call(\n          () => {\n            top.textContent = value;\n            bottom.textContent = value;\n          },\n          [],\n          time,\n        );\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        value,\n        reducedMotion,\n        flipping,\n        flipDirection,\n        flipDuration,\n        bounce,\n        shadowIntensity,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  const half = (bottom: boolean, part: \"static\" | \"outgoing\" | \"incoming\") => {\n    const moving = part !== \"static\";\n    const corner = `min(${borderRadius}px, ${(borderRadius / cardSize) * 100}cqw)`;\n    return (\n      <div\n        data-flip-outgoing={part === \"outgoing\" ? \"\" : undefined}\n        data-flip-incoming={part === \"incoming\" ? \"\" : undefined}\n        className={cn(\n          \"absolute inset-x-0 h-1/2 overflow-hidden bg-(--flip-panel)\",\n          bottom ? \"bottom-0\" : \"top-0\",\n          moving ? \"z-20 will-change-transform\" : \"z-0\",\n        )}\n        style={{\n          borderRadius: bottom\n            ? `0 0 ${corner} ${corner}`\n            : `${corner} ${corner} 0 0`,\n          transformOrigin: bottom ? \"50% 0%\" : \"50% 100%\",\n          backfaceVisibility: \"hidden\",\n          visibility: moving ? \"hidden\" : undefined,\n        }}\n      >\n        <span\n          data-flip-static-top={part === \"static\" && !bottom ? \"\" : undefined}\n          data-flip-static-bottom={part === \"static\" && bottom ? \"\" : undefined}\n          data-flip-number\n          className=\"absolute inset-x-0 flex h-[200%] items-center justify-center font-bold leading-none tracking-[-0.065em] text-(--flip-text) tabular-nums\"\n          style={{\n            top: bottom ? \"-100%\" : 0,\n            fontSize: `${58 * Math.min(1, 2 / Math.max(2, value.length))}cqw`,\n            fontFamily,\n            opacity: bottom ? 1 : 0.86,\n          }}\n        >\n          {value}\n        </span>\n        <span className=\"pointer-events-none absolute inset-0 bg-linear-to-b from-black/3 to-transparent\" />\n        {moving && (\n          <span\n            data-flip-shade\n            className=\"pointer-events-none absolute inset-0 bg-black\"\n            style={{ opacity: 0 }}\n          />\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <div className=\"relative min-w-0 flex-1\" aria-hidden=\"true\">\n      <div\n        ref={rootRef}\n        data-slot=\"flip-clock-unit\"\n        data-value={value}\n        className=\"relative isolate aspect-[1.14] w-full @container\"\n        style={{ perspective: bounded(perspective, 900, 250, 2000) }}\n      >\n        {half(false, \"static\")}\n        {half(true, \"static\")}\n        {half(flipDirection === \"up\", \"outgoing\")}\n        {half(flipDirection === \"down\", \"incoming\")}\n        {period && (\n          <span\n            className=\"absolute left-[6%] top-[6%] z-30 font-bold leading-none text-(--flip-text) opacity-55\"\n            style={{ fontSize: \"7cqw\", fontFamily }}\n          >\n            {period}\n          </span>\n        )}\n        <span\n          data-slot=\"flip-clock-seam\"\n          className=\"pointer-events-none absolute inset-x-0 top-1/2 z-30 -translate-y-1/2 bg-black/90\"\n          style={{ height: `min(1.5px, ${(1.5 / cardSize) * 100}cqw)` }}\n        />\n        {showHinges && (\n          <>\n            <span\n              data-slot=\"flip-clock-hinge\"\n              className=\"pointer-events-none absolute left-0 top-1/2 z-40 h-[9%] w-[2.5%] -translate-x-1/2 -translate-y-1/2 rounded-sm bg-linear-to-b from-zinc-500 via-zinc-700 to-zinc-950 shadow-sm\"\n            />\n            <span\n              data-slot=\"flip-clock-hinge\"\n              className=\"pointer-events-none absolute right-0 top-1/2 z-40 h-[9%] w-[2.5%] translate-x-1/2 -translate-y-1/2 rounded-sm bg-linear-to-b from-zinc-500 via-zinc-700 to-zinc-950 shadow-sm\"\n            />\n          </>\n        )}\n      </div>\n      {separator && (\n        <span className=\"pointer-events-none absolute left-full top-0 z-40 aspect-[1.14] w-full text-(--flip-text) opacity-45\">\n          <span\n            data-slot=\"flip-clock-separator\"\n            className=\"flex h-full items-center justify-center\"\n            style={{ width: \"var(--flip-gap)\" }}\n          >\n            <svg\n              viewBox=\"0 0 16 40\"\n              className=\"aspect-2/5 w-full max-w-4 shrink-0 fill-current\"\n              aria-hidden=\"true\"\n            >\n              <circle cx=\"8\" cy=\"10\" r=\"3\" />\n              <circle cx=\"8\" cy=\"30\" r=\"3\" />\n            </svg>\n          </span>\n        </span>\n      )}\n      {showLabels && (\n        <span className=\"mt-3 block text-center text-[clamp(9px,1.5cqw,11px)] font-medium uppercase tracking-[0.16em] text-(--flip-text) opacity-45\">\n          {label}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport function FlipClock({\n  mode = \"clock\",\n  duration = 3600,\n  targetDate,\n  date,\n  running = true,\n  hourFormat = \"12\",\n  timeZone,\n  showSeconds = mode === \"countdown\",\n  showPeriod = true,\n  showDays = false,\n  showLabels = false,\n  padHours = false,\n  flipping = true,\n  flipDirection = \"down\",\n  flipDuration = 0.65,\n  bounce = 0.12,\n  shadowIntensity = 0.28,\n  perspective = 900,\n  cardSize = 280,\n  gap = 16,\n  borderRadius = 14,\n  showSeparator = false,\n  showHinges = false,\n  theme = \"dark\",\n  panelColor,\n  textColor,\n  fontFamily = \"Arial, Helvetica, sans-serif\",\n  className,\n  ariaLabel,\n  onComplete,\n}: FlipClockProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const [reducedMotion, setReducedMotion] = React.useState(false);\n  useGSAP(\n    () => {\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          standard: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          setReducedMotion(Boolean(context.conditions?.reduced));\n        },\n      );\n      return () => media.revert();\n    },\n    { scope: rootRef },\n  );\n  const { time, remaining } = useTimeSource({\n    mode,\n    duration,\n    targetDate,\n    date,\n    running,\n    onComplete,\n  });\n  const formatter = React.useMemo(() => {\n    try {\n      return new Intl.DateTimeFormat(\"en-GB\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n        timeZone: timeZone || undefined,\n      });\n    } catch {\n      return new Intl.DateTimeFormat(\"en-GB\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n      });\n    }\n  }, [timeZone]);\n  const units: { value: string; label: string }[] = [];\n  let period: string | undefined;\n  const pad = (value: number) => String(value).padStart(2, \"0\");\n  if (mode === \"countdown\") {\n    const days = remaining === null ? null : Math.floor(remaining / 86400);\n    const hours =\n      remaining === null\n        ? null\n        : Math.floor(remaining / 3600) % (showDays ? 24 : Infinity);\n    if (showDays)\n      units.push({ value: days === null ? \"--\" : pad(days), label: \"Days\" });\n    units.push({ value: hours === null ? \"--\" : pad(hours), label: \"Hours\" });\n    units.push({\n      value: remaining === null ? \"--\" : pad(Math.floor(remaining / 60) % 60),\n      label: \"Minutes\",\n    });\n    if (showSeconds)\n      units.push({\n        value: remaining === null ? \"--\" : pad(remaining % 60),\n        label: \"Seconds\",\n      });\n  } else {\n    const parts =\n      time === null\n        ? null\n        : Object.fromEntries(\n            formatter\n              .formatToParts(new Date(time))\n              .map((part) => [part.type, part.value]),\n          );\n    const hour = parts ? Number(parts.hour) : null;\n    const displayHour =\n      hour === null ? null : hourFormat === \"12\" ? hour % 12 || 12 : hour;\n    if (hour !== null && showPeriod && hourFormat === \"12\")\n      period = hour < 12 ? \"AM\" : \"PM\";\n    units.push({\n      value:\n        displayHour === null\n          ? \"--\"\n          : hourFormat === \"24\" || padHours\n            ? pad(displayHour)\n            : String(displayHour),\n      label: \"Hours\",\n    });\n    units.push({ value: parts?.minute ?? \"--\", label: \"Minutes\" });\n    if (showSeconds)\n      units.push({ value: parts?.second ?? \"--\", label: \"Seconds\" });\n  }\n  const size = bounded(cardSize, 280, 64, 600);\n  const spacing = bounded(gap, 16, 0, 48);\n  const width = size * units.length + spacing * (units.length - 1);\n  const label =\n    ariaLabel ??\n    (mode === \"clock\"\n      ? `Current time: ${units.map((unit) => unit.value).join(\":\")}${period ? ` ${period}` : \"\"}`\n      : `Countdown: ${units.map((unit) => `${unit.value} ${unit.label.toLowerCase()}`).join(\", \")}`);\n  const options = {\n    reducedMotion,\n    flipping,\n    flipDirection,\n    flipDuration,\n    bounce,\n    shadowIntensity,\n    perspective,\n    borderRadius: bounded(borderRadius, 14, 0, 40),\n    showHinges,\n    showLabels,\n    fontFamily,\n    cardSize: size,\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"timer\"\n      aria-live=\"off\"\n      aria-label={label}\n      data-slot=\"flip-clock\"\n      data-mode={mode}\n      className={cn(\"w-full @container\", className)}\n      style={\n        {\n          maxWidth: width,\n          \"--flip-gap\": `min(${spacing}px, max(3px, ${(spacing / width) * 100}cqw))`,\n          \"--flip-panel\":\n            panelColor ?? (theme === \"dark\" ? \"#111111\" : \"#e7e7e7\"),\n          \"--flip-text\":\n            textColor ?? (theme === \"dark\" ? \"#f8f8f8\" : \"#171717\"),\n        } as React.CSSProperties\n      }\n    >\n      <div className=\"flex items-start\" style={{ gap: \"var(--flip-gap)\" }}>\n        {units.map((unit, index) => (\n          <FlipUnit\n            key={unit.label}\n            {...unit}\n            period={unit.label === \"Hours\" ? period : undefined}\n            separator={showSeparator && index < units.length - 1}\n            options={options}\n          />\n        ))}\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {mode === \"countdown\" && remaining === 0 ? \"Countdown complete\" : \"\"}\n      </span>\n    </div>\n  );\n}\n\nexport default FlipClock;\n",
      "type": "registry:ui",
      "target": "@ui/flip-clock.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\nimport { useTimeSource } from \"./use-time-source\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type FlipClockMode = \"clock\" | \"countdown\";\nexport type FlipDirection = \"down\" | \"up\";\n\nexport interface FlipClockProps {\n  mode?: FlipClockMode;\n  duration?: number;\n  targetDate?: Date | string | number;\n  date?: Date | string | number;\n  running?: boolean;\n  hourFormat?: \"12\" | \"24\";\n  timeZone?: string;\n  showSeconds?: boolean;\n  showPeriod?: boolean;\n  showDays?: boolean;\n  showLabels?: boolean;\n  padHours?: boolean;\n  flipping?: boolean;\n  flipDirection?: FlipDirection;\n  flipDuration?: number;\n  bounce?: number;\n  shadowIntensity?: number;\n  perspective?: number;\n  cardSize?: number;\n  gap?: number;\n  borderRadius?: number;\n  showSeparator?: boolean;\n  showHinges?: boolean;\n  theme?: \"dark\" | \"light\";\n  panelColor?: string;\n  textColor?: string;\n  fontFamily?: string;\n  className?: string;\n  ariaLabel?: string;\n  onComplete?: () => void;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\ntype FlipUnitProps = {\n  value: string;\n  label: string;\n  period?: string;\n  separator: boolean;\n  options: { reducedMotion: boolean } & Required<\n    Pick<\n      FlipClockProps,\n      | \"flipping\"\n      | \"flipDirection\"\n      | \"flipDuration\"\n      | \"bounce\"\n      | \"shadowIntensity\"\n      | \"perspective\"\n      | \"borderRadius\"\n      | \"showHinges\"\n      | \"showLabels\"\n      | \"fontFamily\"\n      | \"cardSize\"\n    >\n  >;\n};\n\nfunction FlipUnit({ value, label, period, separator, options }: FlipUnitProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const previousRef = React.useRef<string | null>(null);\n  const {\n    flipping,\n    flipDirection,\n    flipDuration,\n    bounce,\n    shadowIntensity,\n    perspective,\n    borderRadius,\n    showHinges,\n    showLabels,\n    fontFamily,\n    cardSize,\n    reducedMotion,\n  } = options;\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root) return;\n      const top = root.querySelector<HTMLElement>(\"[data-flip-static-top]\");\n      const bottom = root.querySelector<HTMLElement>(\n        \"[data-flip-static-bottom]\",\n      );\n      const outgoing = root.querySelector<HTMLElement>(\"[data-flip-outgoing]\");\n      const incoming = root.querySelector<HTMLElement>(\"[data-flip-incoming]\");\n      const outgoingNumber =\n        outgoing?.querySelector<HTMLElement>(\"[data-flip-number]\");\n      const incomingNumber =\n        incoming?.querySelector<HTMLElement>(\"[data-flip-number]\");\n      const outgoingShade =\n        outgoing?.querySelector<HTMLElement>(\"[data-flip-shade]\");\n      const incomingShade =\n        incoming?.querySelector<HTMLElement>(\"[data-flip-shade]\");\n      if (\n        !top ||\n        !bottom ||\n        !outgoing ||\n        !incoming ||\n        !outgoingNumber ||\n        !incomingNumber ||\n        !outgoingShade ||\n        !incomingShade\n      )\n        return;\n      const oldValue = previousRef.current;\n      previousRef.current = value;\n      const reset = () => {\n        top.textContent = value;\n        bottom.textContent = value;\n        gsap.set([outgoing, incoming], { visibility: \"hidden\", rotationX: 0 });\n      };\n      reset();\n      if (\n        !flipping ||\n        reducedMotion ||\n        oldValue === null ||\n        oldValue === \"--\" ||\n        oldValue === value ||\n        value === \"--\"\n      )\n        return;\n      const down = flipDirection === \"down\";\n      const time = bounded(flipDuration, 0.65, 0.15, 0.9);\n      const shade = bounded(shadowIntensity, 0.28, 0, 0.7);\n      (down ? bottom : top).textContent = oldValue;\n      outgoingNumber.textContent = oldValue;\n      incomingNumber.textContent = value;\n      gsap.set(outgoing, { visibility: \"visible\", rotationX: 0 });\n      gsap.set(incoming, {\n        visibility: \"hidden\",\n        rotationX: down ? 90 : -90,\n      });\n      gsap.set(outgoingShade, { opacity: 0 });\n      gsap.set(incomingShade, { opacity: shade });\n      const settle = bounded(bounce, 0.12, 0, 0.4);\n      gsap\n        .timeline()\n        .to(\n          outgoing,\n          {\n            rotationX: down ? -90 : 90,\n            duration: time * 0.46,\n            ease: \"power2.in\",\n          },\n          0,\n        )\n        .to(\n          outgoingShade,\n          { opacity: shade, duration: time * 0.46, ease: \"power1.in\" },\n          0,\n        )\n        .set(outgoing, { visibility: \"hidden\" }, time * 0.46)\n        .set(incoming, { visibility: \"visible\" }, time * 0.46)\n        .to(\n          incoming,\n          {\n            rotationX: 0,\n            duration: time * 0.54,\n            ease: settle ? `back.out(${settle * 5})` : \"power3.out\",\n          },\n          time * 0.46,\n        )\n        .to(\n          incomingShade,\n          { opacity: 0, duration: time * 0.54, ease: \"power2.out\" },\n          time * 0.46,\n        )\n        .set([outgoing, incoming], { visibility: \"hidden\", rotationX: 0 }, time)\n        .call(\n          () => {\n            top.textContent = value;\n            bottom.textContent = value;\n          },\n          [],\n          time,\n        );\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        value,\n        reducedMotion,\n        flipping,\n        flipDirection,\n        flipDuration,\n        bounce,\n        shadowIntensity,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  const half = (bottom: boolean, part: \"static\" | \"outgoing\" | \"incoming\") => {\n    const moving = part !== \"static\";\n    const corner = `min(${borderRadius}px, ${(borderRadius / cardSize) * 100}cqw)`;\n    return (\n      <div\n        data-flip-outgoing={part === \"outgoing\" ? \"\" : undefined}\n        data-flip-incoming={part === \"incoming\" ? \"\" : undefined}\n        className={cn(\n          \"absolute inset-x-0 h-1/2 overflow-hidden bg-(--flip-panel)\",\n          bottom ? \"bottom-0\" : \"top-0\",\n          moving ? \"z-20 will-change-transform\" : \"z-0\",\n        )}\n        style={{\n          borderRadius: bottom\n            ? `0 0 ${corner} ${corner}`\n            : `${corner} ${corner} 0 0`,\n          transformOrigin: bottom ? \"50% 0%\" : \"50% 100%\",\n          backfaceVisibility: \"hidden\",\n          visibility: moving ? \"hidden\" : undefined,\n        }}\n      >\n        <span\n          data-flip-static-top={part === \"static\" && !bottom ? \"\" : undefined}\n          data-flip-static-bottom={part === \"static\" && bottom ? \"\" : undefined}\n          data-flip-number\n          className=\"absolute inset-x-0 flex h-[200%] items-center justify-center font-bold leading-none tracking-[-0.065em] text-(--flip-text) tabular-nums\"\n          style={{\n            top: bottom ? \"-100%\" : 0,\n            fontSize: `${58 * Math.min(1, 2 / Math.max(2, value.length))}cqw`,\n            fontFamily,\n            opacity: bottom ? 1 : 0.86,\n          }}\n        >\n          {value}\n        </span>\n        <span className=\"pointer-events-none absolute inset-0 bg-linear-to-b from-black/3 to-transparent\" />\n        {moving && (\n          <span\n            data-flip-shade\n            className=\"pointer-events-none absolute inset-0 bg-black\"\n            style={{ opacity: 0 }}\n          />\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <div className=\"relative min-w-0 flex-1\" aria-hidden=\"true\">\n      <div\n        ref={rootRef}\n        data-slot=\"flip-clock-unit\"\n        data-value={value}\n        className=\"relative isolate aspect-[1.14] w-full @container\"\n        style={{ perspective: bounded(perspective, 900, 250, 2000) }}\n      >\n        {half(false, \"static\")}\n        {half(true, \"static\")}\n        {half(flipDirection === \"up\", \"outgoing\")}\n        {half(flipDirection === \"down\", \"incoming\")}\n        {period && (\n          <span\n            className=\"absolute left-[6%] top-[6%] z-30 font-bold leading-none text-(--flip-text) opacity-55\"\n            style={{ fontSize: \"7cqw\", fontFamily }}\n          >\n            {period}\n          </span>\n        )}\n        <span\n          data-slot=\"flip-clock-seam\"\n          className=\"pointer-events-none absolute inset-x-0 top-1/2 z-30 -translate-y-1/2 bg-black/90\"\n          style={{ height: `min(1.5px, ${(1.5 / cardSize) * 100}cqw)` }}\n        />\n        {showHinges && (\n          <>\n            <span\n              data-slot=\"flip-clock-hinge\"\n              className=\"pointer-events-none absolute left-0 top-1/2 z-40 h-[9%] w-[2.5%] -translate-x-1/2 -translate-y-1/2 rounded-sm bg-linear-to-b from-zinc-500 via-zinc-700 to-zinc-950 shadow-sm\"\n            />\n            <span\n              data-slot=\"flip-clock-hinge\"\n              className=\"pointer-events-none absolute right-0 top-1/2 z-40 h-[9%] w-[2.5%] translate-x-1/2 -translate-y-1/2 rounded-sm bg-linear-to-b from-zinc-500 via-zinc-700 to-zinc-950 shadow-sm\"\n            />\n          </>\n        )}\n      </div>\n      {separator && (\n        <span className=\"pointer-events-none absolute left-full top-0 z-40 aspect-[1.14] w-full text-(--flip-text) opacity-45\">\n          <span\n            data-slot=\"flip-clock-separator\"\n            className=\"flex h-full items-center justify-center\"\n            style={{ width: \"var(--flip-gap)\" }}\n          >\n            <svg\n              viewBox=\"0 0 16 40\"\n              className=\"aspect-2/5 w-full max-w-4 shrink-0 fill-current\"\n              aria-hidden=\"true\"\n            >\n              <circle cx=\"8\" cy=\"10\" r=\"3\" />\n              <circle cx=\"8\" cy=\"30\" r=\"3\" />\n            </svg>\n          </span>\n        </span>\n      )}\n      {showLabels && (\n        <span className=\"mt-3 block text-center text-[clamp(9px,1.5cqw,11px)] font-medium uppercase tracking-[0.16em] text-(--flip-text) opacity-45\">\n          {label}\n        </span>\n      )}\n    </div>\n  );\n}\n\nexport function FlipClock({\n  mode = \"clock\",\n  duration = 3600,\n  targetDate,\n  date,\n  running = true,\n  hourFormat = \"12\",\n  timeZone,\n  showSeconds = mode === \"countdown\",\n  showPeriod = true,\n  showDays = false,\n  showLabels = false,\n  padHours = false,\n  flipping = true,\n  flipDirection = \"down\",\n  flipDuration = 0.65,\n  bounce = 0.12,\n  shadowIntensity = 0.28,\n  perspective = 900,\n  cardSize = 280,\n  gap = 16,\n  borderRadius = 14,\n  showSeparator = false,\n  showHinges = false,\n  theme = \"dark\",\n  panelColor,\n  textColor,\n  fontFamily = \"Arial, Helvetica, sans-serif\",\n  className,\n  ariaLabel,\n  onComplete,\n}: FlipClockProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const [reducedMotion, setReducedMotion] = React.useState(false);\n  useGSAP(\n    () => {\n      const media = gsap.matchMedia();\n      media.add(\n        {\n          reduced: \"(prefers-reduced-motion: reduce)\",\n          standard: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          setReducedMotion(Boolean(context.conditions?.reduced));\n        },\n      );\n      return () => media.revert();\n    },\n    { scope: rootRef },\n  );\n  const { time, remaining } = useTimeSource({\n    mode,\n    duration,\n    targetDate,\n    date,\n    running,\n    onComplete,\n  });\n  const formatter = React.useMemo(() => {\n    try {\n      return new Intl.DateTimeFormat(\"en-GB\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n        timeZone: timeZone || undefined,\n      });\n    } catch {\n      return new Intl.DateTimeFormat(\"en-GB\", {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        second: \"2-digit\",\n        hourCycle: \"h23\",\n      });\n    }\n  }, [timeZone]);\n  const units: { value: string; label: string }[] = [];\n  let period: string | undefined;\n  const pad = (value: number) => String(value).padStart(2, \"0\");\n  if (mode === \"countdown\") {\n    const days = remaining === null ? null : Math.floor(remaining / 86400);\n    const hours =\n      remaining === null\n        ? null\n        : Math.floor(remaining / 3600) % (showDays ? 24 : Infinity);\n    if (showDays)\n      units.push({ value: days === null ? \"--\" : pad(days), label: \"Days\" });\n    units.push({ value: hours === null ? \"--\" : pad(hours), label: \"Hours\" });\n    units.push({\n      value: remaining === null ? \"--\" : pad(Math.floor(remaining / 60) % 60),\n      label: \"Minutes\",\n    });\n    if (showSeconds)\n      units.push({\n        value: remaining === null ? \"--\" : pad(remaining % 60),\n        label: \"Seconds\",\n      });\n  } else {\n    const parts =\n      time === null\n        ? null\n        : Object.fromEntries(\n            formatter\n              .formatToParts(new Date(time))\n              .map((part) => [part.type, part.value]),\n          );\n    const hour = parts ? Number(parts.hour) : null;\n    const displayHour =\n      hour === null ? null : hourFormat === \"12\" ? hour % 12 || 12 : hour;\n    if (hour !== null && showPeriod && hourFormat === \"12\")\n      period = hour < 12 ? \"AM\" : \"PM\";\n    units.push({\n      value:\n        displayHour === null\n          ? \"--\"\n          : hourFormat === \"24\" || padHours\n            ? pad(displayHour)\n            : String(displayHour),\n      label: \"Hours\",\n    });\n    units.push({ value: parts?.minute ?? \"--\", label: \"Minutes\" });\n    if (showSeconds)\n      units.push({ value: parts?.second ?? \"--\", label: \"Seconds\" });\n  }\n  const size = bounded(cardSize, 280, 64, 600);\n  const spacing = bounded(gap, 16, 0, 48);\n  const width = size * units.length + spacing * (units.length - 1);\n  const label =\n    ariaLabel ??\n    (mode === \"clock\"\n      ? `Current time: ${units.map((unit) => unit.value).join(\":\")}${period ? ` ${period}` : \"\"}`\n      : `Countdown: ${units.map((unit) => `${unit.value} ${unit.label.toLowerCase()}`).join(\", \")}`);\n  const options = {\n    reducedMotion,\n    flipping,\n    flipDirection,\n    flipDuration,\n    bounce,\n    shadowIntensity,\n    perspective,\n    borderRadius: bounded(borderRadius, 14, 0, 40),\n    showHinges,\n    showLabels,\n    fontFamily,\n    cardSize: size,\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"timer\"\n      aria-live=\"off\"\n      aria-label={label}\n      data-slot=\"flip-clock\"\n      data-mode={mode}\n      className={cn(\"w-full @container\", className)}\n      style={\n        {\n          maxWidth: width,\n          \"--flip-gap\": `min(${spacing}px, max(3px, ${(spacing / width) * 100}cqw))`,\n          \"--flip-panel\":\n            panelColor ?? (theme === \"dark\" ? \"#111111\" : \"#e7e7e7\"),\n          \"--flip-text\":\n            textColor ?? (theme === \"dark\" ? \"#f8f8f8\" : \"#171717\"),\n        } as React.CSSProperties\n      }\n    >\n      <div className=\"flex items-start\" style={{ gap: \"var(--flip-gap)\" }}>\n        {units.map((unit, index) => (\n          <FlipUnit\n            key={unit.label}\n            {...unit}\n            period={unit.label === \"Hours\" ? period : undefined}\n            separator={showSeparator && index < units.length - 1}\n            options={options}\n          />\n        ))}\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {mode === \"countdown\" && remaining === 0 ? \"Countdown complete\" : \"\"}\n      </span>\n    </div>\n  );\n}\n\nexport default FlipClock;\n"
    },
    {
      "name": "use-time-source.ts",
      "path": "registry/ui/use-time-source.ts",
      "code": "\"use client\";\n\nimport * as React from \"react\";\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nconst timestamp = (value: Date | string | number | undefined) =>\n  value === undefined ? undefined : new Date(value).getTime();\n\nexport function useTimeSource({\n  mode,\n  duration,\n  targetDate,\n  date,\n  running,\n  onComplete,\n}: {\n  mode: \"clock\" | \"countdown\";\n  duration: number;\n  targetDate?: Date | string | number;\n  date?: Date | string | number;\n  running: boolean;\n  onComplete?: () => void;\n}) {\n  const [snapshot, setSnapshot] = React.useState<{\n    time: number | null;\n    remaining: number | null;\n  }>({ time: null, remaining: null });\n  const runRef = React.useRef({\n    signature: \"\",\n    remaining: 0,\n    deadline: 0,\n    completed: false,\n    clockTime: 0,\n  });\n  const completeRef = React.useRef(onComplete);\n  const seconds = bounded(duration, 3600, 0, 863999999);\n  const target = timestamp(targetDate);\n  const fixedDate = timestamp(date);\n\n  React.useEffect(() => {\n    completeRef.current = onComplete;\n  }, [onComplete]);\n\n  React.useEffect(() => {\n    const run = runRef.current;\n    const signature =\n      mode === \"countdown\"\n        ? `${mode}:${seconds}:${target}`\n        : `${mode}:${fixedDate}`;\n    if (run.signature !== signature) {\n      run.signature = signature;\n      run.remaining =\n        mode === \"countdown\" && target !== undefined && Number.isFinite(target)\n          ? Math.max(0, target - Date.now())\n          : seconds * 1000;\n      run.completed = false;\n      run.clockTime = 0;\n    }\n    const relative = mode === \"countdown\" && target === undefined;\n    if (relative && running) run.deadline = performance.now() + run.remaining;\n    let timer: ReturnType<typeof setTimeout> | undefined;\n    let disposed = false;\n\n    const refresh = () => {\n      if (disposed) return;\n      clearTimeout(timer);\n      const wallTime = Date.now();\n      let time: number | null = null;\n      let remaining: number | null = null;\n      if (mode === \"clock\") {\n        if (fixedDate !== undefined)\n          time = Number.isFinite(fixedDate) ? fixedDate : null;\n        else {\n          if (running || !run.clockTime)\n            run.clockTime = Math.floor(wallTime / 1000) * 1000;\n          time = run.clockTime;\n        }\n      } else if (relative) {\n        if (running)\n          run.remaining = Math.max(0, run.deadline - performance.now());\n        remaining = Math.ceil(run.remaining / 1000);\n      } else if (Number.isFinite(target)) {\n        if (running) run.remaining = Math.max(0, target! - wallTime);\n        remaining = Math.ceil(run.remaining / 1000);\n      }\n      setSnapshot((previous) =>\n        previous.time === time && previous.remaining === remaining\n          ? previous\n          : { time, remaining },\n      );\n      if (mode === \"countdown\" && remaining === 0 && !run.completed) {\n        run.completed = true;\n        completeRef.current?.();\n      }\n      const needsTick =\n        running &&\n        (mode === \"clock\"\n          ? fixedDate === undefined\n          : remaining !== null && remaining > 0);\n      if (needsTick && !document.hidden) {\n        const delay = relative\n          ? (run.remaining % 1000 || 1000) + 10\n          : 1010 - (Date.now() % 1000);\n        timer = setTimeout(refresh, delay);\n      }\n    };\n    const visibility = () => {\n      if (document.hidden) clearTimeout(timer);\n      else refresh();\n    };\n    refresh();\n    document.addEventListener(\"visibilitychange\", visibility);\n    return () => {\n      disposed = true;\n      clearTimeout(timer);\n      document.removeEventListener(\"visibilitychange\", visibility);\n      if (relative && running)\n        run.remaining = Math.max(0, run.deadline - performance.now());\n    };\n  }, [mode, seconds, target, fixedDate, running]);\n\n  return snapshot;\n}\n\n",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nconst timestamp = (value: Date | string | number | undefined) =>\n  value === undefined ? undefined : new Date(value).getTime();\n\nexport function useTimeSource({\n  mode,\n  duration,\n  targetDate,\n  date,\n  running,\n  onComplete,\n}: {\n  mode: \"clock\" | \"countdown\";\n  duration: number;\n  targetDate?: Date | string | number;\n  date?: Date | string | number;\n  running: boolean;\n  onComplete?: () => void;\n}) {\n  const [snapshot, setSnapshot] = React.useState<{\n    time: number | null;\n    remaining: number | null;\n  }>({ time: null, remaining: null });\n  const runRef = React.useRef({\n    signature: \"\",\n    remaining: 0,\n    deadline: 0,\n    completed: false,\n    clockTime: 0,\n  });\n  const completeRef = React.useRef(onComplete);\n  const seconds = bounded(duration, 3600, 0, 863999999);\n  const target = timestamp(targetDate);\n  const fixedDate = timestamp(date);\n\n  React.useEffect(() => {\n    completeRef.current = onComplete;\n  }, [onComplete]);\n\n  React.useEffect(() => {\n    const run = runRef.current;\n    const signature =\n      mode === \"countdown\"\n        ? `${mode}:${seconds}:${target}`\n        : `${mode}:${fixedDate}`;\n    if (run.signature !== signature) {\n      run.signature = signature;\n      run.remaining =\n        mode === \"countdown\" && target !== undefined && Number.isFinite(target)\n          ? Math.max(0, target - Date.now())\n          : seconds * 1000;\n      run.completed = false;\n      run.clockTime = 0;\n    }\n    const relative = mode === \"countdown\" && target === undefined;\n    if (relative && running) run.deadline = performance.now() + run.remaining;\n    let timer: ReturnType<typeof setTimeout> | undefined;\n    let disposed = false;\n\n    const refresh = () => {\n      if (disposed) return;\n      clearTimeout(timer);\n      const wallTime = Date.now();\n      let time: number | null = null;\n      let remaining: number | null = null;\n      if (mode === \"clock\") {\n        if (fixedDate !== undefined)\n          time = Number.isFinite(fixedDate) ? fixedDate : null;\n        else {\n          if (running || !run.clockTime)\n            run.clockTime = Math.floor(wallTime / 1000) * 1000;\n          time = run.clockTime;\n        }\n      } else if (relative) {\n        if (running)\n          run.remaining = Math.max(0, run.deadline - performance.now());\n        remaining = Math.ceil(run.remaining / 1000);\n      } else if (Number.isFinite(target)) {\n        if (running) run.remaining = Math.max(0, target! - wallTime);\n        remaining = Math.ceil(run.remaining / 1000);\n      }\n      setSnapshot((previous) =>\n        previous.time === time && previous.remaining === remaining\n          ? previous\n          : { time, remaining },\n      );\n      if (mode === \"countdown\" && remaining === 0 && !run.completed) {\n        run.completed = true;\n        completeRef.current?.();\n      }\n      const needsTick =\n        running &&\n        (mode === \"clock\"\n          ? fixedDate === undefined\n          : remaining !== null && remaining > 0);\n      if (needsTick && !document.hidden) {\n        const delay = relative\n          ? (run.remaining % 1000 || 1000) + 10\n          : 1010 - (Date.now() % 1000);\n        timer = setTimeout(refresh, delay);\n      }\n    };\n    const visibility = () => {\n      if (document.hidden) clearTimeout(timer);\n      else refresh();\n    };\n    refresh();\n    document.addEventListener(\"visibilitychange\", visibility);\n    return () => {\n      disposed = true;\n      clearTimeout(timer);\n      document.removeEventListener(\"visibilitychange\", visibility);\n      if (relative && running)\n        run.remaining = Math.max(0, run.deadline - performance.now());\n    };\n  }, [mode, seconds, target, fixedDate, running]);\n\n  return snapshot;\n}\n\n",
      "type": "registry:ui",
      "target": "@ui/use-time-source.ts"
    }
  ]
}
