{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "timer",
  "title": "Timer",
  "description": "Counts down to a moment or up from one, with pause and resume. It reads the clock instead of adding ticks, so a sleeping tab cannot make it wrong.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/timer.tsx",
      "content": "import type * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { Timer as TimerIcon } from \"lucide-react\"\nimport { formatClock, spokenTime, visibleUnits, type TimerMode } from \"@/registry/miseui/ui/timer-math\"\nimport { DEFAULT_LABELS, TimerBoxes, TimerControls, TimerRing, TimerText, type TimerLabels } from \"@/registry/miseui/ui/timer-parts\"\nimport { useTimer, type UseTimerOptions } from \"@/registry/miseui/ui/timer-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype TimerVariant = \"plain\" | \"boxes\" | \"compact\" | \"ring\"\ntype TimerEffect = \"roll\"\n\ntype TimerProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  UseTimerOptions & {\n    /** `plain` a large clock, `boxes` one tile per unit, `compact` a small chip for a row, `ring` a clock inside a progress circle. */\n    variant?: TimerVariant\n    /** `roll` turns each digit like a counter wheel. People who turned motion off get a plain change. */\n    effect?: TimerEffect\n    /** Adds pause, resume and reset buttons. */\n    controls?: boolean\n    labels?: Partial<TimerLabels>\n    /** What a screen reader hears. Called with whole seconds; return the same text for a whole minute, or it will speak every second. */\n    speak?: (seconds: number, mode: TimerMode) => string\n  }\n\nconst ROOT_CLASS: Record<TimerVariant, string> = {\n  // w-full: a size container takes no width from its content, so as a flex item it would otherwise collapse to nothing.\n  plain: \"@container grid w-full min-w-0 justify-items-start gap-2\",\n  boxes: \"@container grid w-full min-w-0 gap-2\",\n  compact: \"inline-flex min-h-8 max-w-full items-center gap-2 rounded-md border bg-background px-2 text-[13px] leading-5 pointer-coarse:min-h-10\",\n  ring: \"@container relative grid aspect-square w-full max-w-48 place-items-center\",\n}\n\n/** Compact sits in a row, so its buttons shrink with it on a mouse and still reach 40 px under a finger. */\nconst CONTROLS_CLASS: Partial<Record<TimerVariant, string>> = {\n  compact: \"-mr-1 [&>button]:size-7 [&>button]:pointer-coarse:size-10\",\n  ring: \"mt-2\",\n}\n\ninterface DisplayProps {\n  timer: ReturnType<typeof useTimer>\n  variant: TimerVariant\n  mode: TimerMode\n  roll: boolean\n  reduceMotion: boolean\n  labels: TimerLabels\n}\n\nfunction TimerDisplay({ timer, variant, mode, roll, reduceMotion, labels }: DisplayProps) {\n  const text = <TimerText text={formatClock(timer.seconds, timer.span)} roll={roll} mode={mode} reduceMotion={reduceMotion} />\n  // role timer is silent by default: the digits stay readable on demand and the live region below does the speaking.\n  const shared = { role: \"timer\", \"data-slot\": \"timer-display\" } as const\n  if (variant === \"boxes\") {\n    return (\n      <span {...shared} className=\"block\">\n        <TimerBoxes seconds={timer.seconds} units={visibleUnits(timer.span)} labels={labels} roll={roll} mode={mode} reduceMotion={reduceMotion} />\n      </span>\n    )\n  }\n  if (variant === \"compact\") {\n    return (\n      <span className=\"inline-flex min-w-0 items-center gap-2\">\n        <TimerIcon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n        <span {...shared} className=\"font-medium tabular-nums\">\n          {text}\n        </span>\n      </span>\n    )\n  }\n  if (variant === \"ring\") {\n    return (\n      <span {...shared} className=\"text-[clamp(20px,15cqi,32px)] leading-none font-semibold tracking-tight tabular-nums\">\n        {text}\n      </span>\n    )\n  }\n  return (\n    <span {...shared} className=\"max-w-full text-[clamp(28px,14cqi,48px)] leading-none font-semibold tracking-tight tabular-nums\">\n      {text}\n    </span>\n  )\n}\n\nfunction Timer({ mode = \"down\", target, duration, running, defaultRunning, onRunningChange, onComplete, now, variant = \"plain\", effect, controls = false, labels, speak = spokenTime, className, ...props }: TimerProps) {\n  const timer = useTimer({ mode, target, duration, running, defaultRunning, onRunningChange, onComplete, now })\n  const reduceMotion = useReducedMotion() ?? false\n  const merged = { ...DEFAULT_LABELS, ...labels }\n  const controlRow = controls ? (\n    <TimerControls\n      running={timer.running}\n      finished={timer.finished}\n      labels={merged}\n      onToggle={() => timer.setRunning(!timer.running)}\n      onReset={timer.reset}\n      className={CONTROLS_CLASS[variant]}\n    />\n  ) : null\n  const display = <TimerDisplay timer={timer} variant={variant} mode={mode} roll={effect === \"roll\"} reduceMotion={reduceMotion} labels={merged} />\n  return (\n    <div\n      data-slot=\"timer\"\n      data-variant={variant}\n      data-effect={effect}\n      data-running={timer.running ? \"true\" : \"false\"}\n      data-finished={timer.finished ? \"true\" : \"false\"}\n      className={cn(ROOT_CLASS[variant], className)}\n      {...props}\n    >\n      {variant === \"ring\" ? <TimerRing fraction={timer.fraction} running={timer.running} /> : null}\n      {variant === \"ring\" ? (\n        <span className=\"relative grid justify-items-center\">\n          {display}\n          {controlRow}\n        </span>\n      ) : (\n        <>\n          {display}\n          {controlRow}\n        </>\n      )}\n      <span aria-live=\"polite\" aria-atomic=\"true\" data-slot=\"timer-announcer\" className=\"sr-only\">\n        {speak(timer.seconds, mode)}\n      </span>\n    </div>\n  )\n}\n\nexport { Timer, formatClock, useTimer, type TimerProps, type TimerVariant, type TimerEffect, type TimerMode, type TimerLabels }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/timer-math.ts",
      "content": "type TimerMode = \"down\" | \"up\"\n\nconst MS_PER_SECOND = 1000\nconst SECONDS_PER_MINUTE = 60\nconst SECONDS_PER_HOUR = 3600\nconst SECONDS_PER_DAY = 86400\n\n/**\n * A timer is a pair of clock readings, never a running sum. Timers in a background tab fire late or not at all;\n * adding up ticks would lose that time, while subtracting two readings cannot.\n */\ninterface TimerClock {\n  running: boolean\n  /** Wall clock reading when it last started running. */\n  anchor: number\n  /** Milliseconds already gone before that reading. */\n  banked: number\n  /** Where it ends, in milliseconds of elapsed time. Null counts up for ever. */\n  total: number | null\n}\n\ninterface ClockSpec {\n  mode: TimerMode\n  /** Counting down: the moment to reach. Counting up: the moment it started. */\n  target?: number\n  /** A length in milliseconds. Counting down starts from it; counting up stops at it. */\n  duration?: number\n  now: number\n  running: boolean\n}\n\nfunction createClock({ mode, target, duration, now, running }: ClockSpec): TimerClock {\n  if (mode === \"down\") {\n    const total = target !== undefined ? target - now : (duration ?? 0)\n    return { running, anchor: now, banked: 0, total: Math.max(0, total) }\n  }\n  const banked = target !== undefined ? Math.max(0, now - target) : 0\n  return { running, anchor: now, banked, total: duration ?? null }\n}\n\nfunction elapsedAt(clock: TimerClock, now: number) {\n  // The system clock can be moved backwards; that must not give time back.\n  const running = clock.running ? Math.max(0, now - clock.anchor) : 0\n  const elapsed = clock.banked + running\n  return clock.total === null ? elapsed : Math.min(clock.total, elapsed)\n}\n\nfunction pauseClock(clock: TimerClock, now: number): TimerClock {\n  if (!clock.running) return clock\n  return { ...clock, running: false, banked: elapsedAt(clock, now), anchor: now }\n}\n\nfunction resumeClock(clock: TimerClock, now: number): TimerClock {\n  if (clock.running) return clock\n  return { ...clock, running: true, anchor: now }\n}\n\nfunction isFinished(clock: TimerClock, now: number) {\n  return clock.total !== null && elapsedAt(clock, now) >= clock.total\n}\n\n/** Rounded so a ten second countdown shows 10 for its whole first second and reaches 0 exactly when time is up. */\nfunction displaySeconds(clock: TimerClock, now: number, mode: TimerMode) {\n  const elapsed = elapsedAt(clock, now)\n  if (mode === \"up\") return Math.floor(elapsed / MS_PER_SECOND)\n  return Math.ceil(((clock.total ?? 0) - elapsed) / MS_PER_SECOND)\n}\n\n/** Waits for the moment the shown digit changes, not a fixed second, so the wait cannot slip against the clock. */\nfunction nextTickDelay(clock: TimerClock, now: number, mode: TimerMode) {\n  if (!clock.running || isFinished(clock, now)) return null\n  const elapsed = elapsedAt(clock, now)\n  if (mode === \"down\") {\n    const left = (clock.total ?? 0) - elapsed\n    return left % MS_PER_SECOND || MS_PER_SECOND\n  }\n  const toDigit = MS_PER_SECOND - (elapsed % MS_PER_SECOND)\n  return clock.total === null ? toDigit : Math.min(toDigit, clock.total - elapsed)\n}\n\nfunction timeParts(totalSeconds: number) {\n  const seconds = Math.max(0, Math.floor(totalSeconds))\n  return {\n    days: Math.floor(seconds / SECONDS_PER_DAY),\n    hours: Math.floor((seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR),\n    minutes: Math.floor((seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE),\n    seconds: seconds % SECONDS_PER_MINUTE,\n  }\n}\n\nconst pad = (value: number) => String(value).padStart(2, \"0\")\n\n/**\n * `span` is the largest value this timer will show, so the shape is fixed from the start:\n * one hour counting down stays 0:59:59 instead of jumping narrower to 59:59.\n */\nfunction formatClock(totalSeconds: number, span = totalSeconds) {\n  const { days, hours, minutes, seconds } = timeParts(totalSeconds)\n  if (span >= SECONDS_PER_DAY) return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`\n  if (span >= SECONDS_PER_HOUR) return `${hours}:${pad(minutes)}:${pad(seconds)}`\n  return `${pad(minutes)}:${pad(seconds)}`\n}\n\ntype TimerUnit = \"days\" | \"hours\" | \"minutes\" | \"seconds\"\n\nfunction visibleUnits(span: number): readonly TimerUnit[] {\n  if (span >= SECONDS_PER_DAY) return [\"days\", \"hours\", \"minutes\", \"seconds\"]\n  if (span >= SECONDS_PER_HOUR) return [\"hours\", \"minutes\", \"seconds\"]\n  return [\"minutes\", \"seconds\"]\n}\n\nconst plural = (count: number, word: string) => `${count} ${word}${count === 1 ? \"\" : \"s\"}`\n\n/** Has one value for a whole minute by construction, so a live region reading it speaks once a minute and no more. */\nfunction spokenTime(totalSeconds: number, mode: TimerMode) {\n  const suffix = mode === \"down\" ? \"left\" : \"elapsed\"\n  if (totalSeconds < SECONDS_PER_MINUTE) {\n    if (mode === \"down\" && totalSeconds <= 0) return \"Time is up\"\n    return `Less than a minute ${suffix}`\n  }\n  const round = mode === \"down\" ? Math.ceil : Math.floor\n  const minutes = round(totalSeconds / SECONDS_PER_MINUTE)\n  const hours = Math.floor(minutes / 60)\n  const rest = minutes % 60\n  const parts = [hours > 0 ? plural(hours, \"hour\") : \"\", rest > 0 ? plural(rest, \"minute\") : \"\"].filter(Boolean)\n  return `${parts.join(\" \")} ${suffix}`\n}\n\n/** How full the ring is. A count up with no end has nothing to fill towards, so it sweeps once a minute like a second hand. */\nfunction ringFraction(clock: TimerClock, now: number, mode: TimerMode) {\n  const elapsed = elapsedAt(clock, now)\n  if (clock.total === null) return (elapsed % (SECONDS_PER_MINUTE * MS_PER_SECOND)) / (SECONDS_PER_MINUTE * MS_PER_SECOND)\n  if (clock.total === 0) return mode === \"down\" ? 0 : 1\n  return mode === \"down\" ? (clock.total - elapsed) / clock.total : elapsed / clock.total\n}\n\nexport {\n  createClock,\n  displaySeconds,\n  elapsedAt,\n  formatClock,\n  isFinished,\n  nextTickDelay,\n  pauseClock,\n  resumeClock,\n  ringFraction,\n  spokenTime,\n  timeParts,\n  visibleUnits,\n  MS_PER_SECOND,\n  type TimerClock,\n  type TimerMode,\n  type TimerUnit,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/timer-parts.tsx",
      "content": "import { AnimatePresence, motion } from \"motion/react\"\nimport { Pause, Play, RotateCcw } from \"lucide-react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { timeParts, type TimerMode, type TimerUnit } from \"@/registry/miseui/ui/timer-math\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst INSTANT = { duration: 0 } as const\n\ninterface TimerLabels {\n  days: string\n  hours: string\n  minutes: string\n  seconds: string\n  pause: string\n  resume: string\n  reset: string\n}\n\nconst DEFAULT_LABELS: TimerLabels = {\n  days: \"Days\",\n  hours: \"Hours\",\n  minutes: \"Min\",\n  seconds: \"Sec\",\n  pause: \"Pause\",\n  resume: \"Resume\",\n  reset: \"Reset\",\n}\n\n/** Counting down, digits fall into place; counting up, they rise. The direction agrees with what the number is doing. */\nfunction RollDigit({ digit, mode, reduceMotion }: { digit: string; mode: TimerMode; reduceMotion: boolean }) {\n  const travel = mode === \"down\" ? \"-100%\" : \"100%\"\n  const leave = mode === \"down\" ? \"100%\" : \"-100%\"\n  return (\n    <span data-slot=\"timer-digit\" className=\"relative inline-block overflow-hidden align-baseline\">\n      <span className=\"invisible\">0</span>\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={digit}\n          className=\"absolute inset-0 text-center\"\n          initial={{ y: travel, opacity: 0 }}\n          animate={{ y: 0, opacity: 1 }}\n          exit={{ y: leave, opacity: 0 }}\n          transition={reduceMotion ? INSTANT : SPRING_DEFAULT}\n        >\n          {digit}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  )\n}\n\ninterface TimerTextProps {\n  text: string\n  roll: boolean\n  mode: TimerMode\n  reduceMotion: boolean\n}\n\nfunction TimerText({ text, roll, mode, reduceMotion }: TimerTextProps) {\n  if (!roll || reduceMotion) return <>{text}</>\n  const characters = Array.from(text)\n  return (\n    <>\n      <span className=\"sr-only\">{text}</span>\n      <span aria-hidden=\"true\" data-slot=\"timer-roll\" className=\"inline-flex\">\n        {/* Keyed from the right, so seconds keep rolling when a column on the left appears or goes. */}\n        {characters.map((character, index) => {\n          const key = characters.length - index\n          return /[0-9]/.test(character) ? (\n            <RollDigit key={key} digit={character} mode={mode} reduceMotion={reduceMotion} />\n          ) : (\n            <span key={key} className=\"whitespace-pre\">\n              {character}\n            </span>\n          )\n        })}\n      </span>\n    </>\n  )\n}\n\ninterface TimerBoxesProps {\n  seconds: number\n  units: readonly TimerUnit[]\n  labels: TimerLabels\n  roll: boolean\n  mode: TimerMode\n  reduceMotion: boolean\n}\n\nfunction TimerBoxes({ seconds, units, labels, roll, mode, reduceMotion }: TimerBoxesProps) {\n  const parts = timeParts(seconds)\n  return (\n    <span className=\"grid auto-cols-[minmax(0,1fr)] grid-flow-col gap-2\" data-slot=\"timer-boxes\">\n      {units.map((unit) => (\n        <span key={unit} data-slot=\"timer-box\" data-unit={unit} className=\"grid min-w-0 justify-items-center gap-1 rounded-md border bg-background px-1 py-2\">\n          <span className=\"text-[clamp(16px,10cqi,24px)] leading-tight font-semibold tabular-nums\">\n            <TimerText text={String(parts[unit]).padStart(2, \"0\")} roll={roll} mode={mode} reduceMotion={reduceMotion} />\n          </span>\n          <span className=\"max-w-full truncate text-xs text-muted-foreground\">{labels[unit]}</span>\n        </span>\n      ))}\n    </span>\n  )\n}\n\nfunction TimerRing({ fraction, running }: { fraction: number; running: boolean }) {\n  return (\n    <svg aria-hidden=\"true\" data-slot=\"timer-ring\" viewBox=\"0 0 100 100\" className=\"absolute inset-0 size-full -rotate-90\">\n      <circle cx=\"50\" cy=\"50\" r=\"46\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"4\" className=\"text-muted\" />\n      <circle\n        cx=\"50\"\n        cy=\"50\"\n        r=\"46\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"4\"\n        strokeLinecap=\"round\"\n        pathLength={1}\n        strokeDasharray=\"1\"\n        strokeDashoffset={1 - fraction}\n        // The digit changes once a second, so a one second linear glide meets the next reading exactly.\n        className={cn(\"text-mise motion-reduce:transition-none\", running && \"transition-[stroke-dashoffset] duration-1000 ease-linear\")}\n      />\n    </svg>\n  )\n}\n\nconst CONTROL_CLASS = [\n  \"inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground pointer-coarse:size-10\",\n  \"transition-[color,background-color] duration-(--duration-fast) ease-(--ease-mise)\",\n  \"outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n  \"active:bg-accent active:duration-0 disabled:pointer-events-none disabled:opacity-50\",\n]\n\ninterface TimerControlsProps {\n  running: boolean\n  finished: boolean\n  labels: TimerLabels\n  onToggle: () => void\n  onReset: () => void\n  className?: string\n}\n\nfunction TimerControls({ running, finished, labels, onToggle, onReset, className }: TimerControlsProps) {\n  const Icon = running ? Pause : Play\n  return (\n    <span data-slot=\"timer-controls\" className={cn(\"inline-flex items-center gap-1\", className)}>\n      <button type=\"button\" aria-label={running ? labels.pause : labels.resume} disabled={finished} className={cn(CONTROL_CLASS)} onClick={onToggle}>\n        <Icon aria-hidden=\"true\" className=\"size-4\" />\n      </button>\n      <button type=\"button\" aria-label={labels.reset} className={cn(CONTROL_CLASS)} onClick={onReset}>\n        <RotateCcw aria-hidden=\"true\" className=\"size-4\" />\n      </button>\n    </span>\n  )\n}\n\nexport { DEFAULT_LABELS, TimerBoxes, TimerControls, TimerRing, TimerText, type TimerLabels }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/timer-state.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\"\nimport {\n  createClock,\n  displaySeconds,\n  isFinished,\n  nextTickDelay,\n  pauseClock,\n  resumeClock,\n  ringFraction,\n  MS_PER_SECOND,\n  type TimerClock,\n  type TimerMode,\n} from \"@/registry/miseui/ui/timer-math\"\n\ninterface UseTimerOptions {\n  mode?: TimerMode\n  /** Counting down: the moment to reach. Counting up: the moment it started. */\n  target?: Date | number\n  /** A length in milliseconds. Counting down starts from it; counting up stops at it. */\n  duration?: number\n  /** Controls pause and resume from outside. */\n  running?: boolean\n  defaultRunning?: boolean\n  onRunningChange?: (running: boolean) => void\n  onComplete?: () => void\n  /** A replaceable clock, for tests and for syncing to server time. */\n  now?: () => number\n}\n\nconst toMs = (target: Date | number | undefined) => (target instanceof Date ? target.getTime() : target)\n\nfunction useTimerClock(options: UseTimerOptions, now: () => number) {\n  const { mode = \"down\", target, duration, running, defaultRunning = true } = options\n  const targetMs = toMs(target)\n  const spec = `${mode}|${targetMs}|${duration}`\n  const build = (time: number, on: boolean) => createClock({ mode, target: targetMs, duration, now: time, running: on })\n  const [state, setState] = useState(() => {\n    const time = now()\n    return { spec, clock: build(time, running ?? defaultRunning), tickAt: time }\n  })\n  // Adjusted while rendering rather than in an effect, so a new target never paints one frame of the old time.\n  if (state.spec !== spec) {\n    const time = now()\n    const next = { spec, clock: build(time, running ?? state.clock.running), tickAt: time }\n    setState(next)\n    return { state: next, setState, build }\n  }\n  if (running !== undefined && running !== state.clock.running) {\n    const time = now()\n    const next = { ...state, clock: running ? resumeClock(state.clock, time) : pauseClock(state.clock, time), tickAt: time }\n    setState(next)\n    return { state: next, setState, build }\n  }\n  return { state, setState, build }\n}\n\nfunction useTimerTicks(clock: TimerClock, tickAt: number, mode: TimerMode, now: () => number, setTick: (time: number) => void) {\n  useEffect(() => {\n    const delay = nextTickDelay(clock, now(), mode)\n    if (delay === null) return\n    const id = setTimeout(() => setTick(now()), delay)\n    return () => clearTimeout(id)\n  }, [clock, tickAt, mode, now, setTick])\n  // A background tab may have slept through its timers; catch up the moment it is visible again.\n  useEffect(() => {\n    const wake = () => setTick(now())\n    document.addEventListener(\"visibilitychange\", wake)\n    return () => document.removeEventListener(\"visibilitychange\", wake)\n  }, [now, setTick])\n}\n\nfunction useTimer(options: UseTimerOptions) {\n  const { mode = \"down\", running, onRunningChange, onComplete, now = Date.now } = options\n  const { state, setState, build } = useTimerClock(options, now)\n  const { clock, tickAt } = state\n  const setTick = useCallback((time: number) => setState((previous) => ({ ...previous, tickAt: time })), [setState])\n  useTimerTicks(clock, tickAt, mode, now, setTick)\n\n  const finished = isFinished(clock, tickAt)\n  const completed = useRef(onComplete)\n  useEffect(() => {\n    completed.current = onComplete\n  }, [onComplete])\n  useEffect(() => {\n    if (finished) completed.current?.()\n  }, [finished])\n\n  const setRunning = (next: boolean) => {\n    const time = now()\n    onRunningChange?.(next)\n    if (running !== undefined) return\n    setState((previous) => ({ ...previous, clock: next ? resumeClock(previous.clock, time) : pauseClock(previous.clock, time), tickAt: time }))\n  }\n  const reset = () => {\n    const time = now()\n    setState((previous) => ({ ...previous, clock: build(time, running ?? true), tickAt: time }))\n  }\n  const seconds = displaySeconds(clock, tickAt, mode)\n  return {\n    seconds,\n    /** The largest value this timer shows: fixes the layout so the digits do not change shape on the way. */\n    span: clock.total === null ? seconds : Math.max(seconds, Math.ceil(clock.total / MS_PER_SECOND)),\n    fraction: ringFraction(clock, tickAt, mode),\n    running: clock.running && !finished,\n    finished,\n    setRunning,\n    reset,\n  }\n}\n\nexport { useTimer, type UseTimerOptions }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}