{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "clock-picker",
  "type": "registry:ui",
  "title": "Clock Picker",
  "description": "An analog time picker: drag the hand round an hour ring, then a minute ring, with snapping, AM / PM and an editable readout.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/clock-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type ClockHourCycle = \"auto\" | \"12\" | \"24\";\ntype Unit = \"hour\" | \"minute\";\n\nexport interface ClockPickerProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"defaultValue\" | \"children\"> {\n  /** Controlled time, 24-hour \"HH:mm\". */\n  value?: string;\n  /** Initial time when uncontrolled, 24-hour \"HH:mm\". */\n  defaultValue?: string;\n  /** Called with the new 24-hour \"HH:mm\" after every change. */\n  onChange?: (value: string) => void;\n  /** 12-hour dial with AM / PM, 24-hour dial with an inner ring, or whatever the locale prefers. */\n  hourCycle?: ClockHourCycle;\n  /** Minute snapping on the dial and keyboard step (a divisor of 60: 1, 5, 15…). */\n  minuteStep?: number;\n  /** The clock face on the page, or a field button that opens it in a native popover. */\n  variant?: \"inline\" | \"popover\";\n  /** BCP 47 locale for the formatted time, AM / PM labels and their order. */\n  locale?: string;\n  /** Visible label. */\n  label?: string;\n  /** Switch to minutes after an hour is picked on the dial. */\n  autoAdvance?: boolean;\n  /** Dial diameter in px. */\n  size?: number;\n  /** Hand, selection and focus colour. */\n  accent?: string;\n  disabled?: boolean;\n  /** When set, a hidden input submits \"HH:mm\" under this name. */\n  name?: string;\n}\n\n/* --------------------------------- helpers --------------------------------- */\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\nconst mod = (n: number, m: number) => ((n % m) + m) % m;\nconst round2 = (n: number) => Math.round(n * 100) / 100;\n\nfunction parseTime(value: string | undefined): { h: number; m: number } {\n  const match = /^(\\d{1,2}):(\\d{2})$/.exec(value ?? \"\");\n  if (!match) return { h: 9, m: 0 };\n  return { h: Math.min(23, Number(match[1])), m: Math.min(59, Number(match[2])) };\n}\n\nconst toValue = (h: number, m: number) => `${pad(h)}:${pad(m)}`;\n\n/** Largest divisor of 60 that is ≤ the requested step, so snapping always wraps cleanly. */\nfunction cleanStep(step: number) {\n  const s = Math.max(1, Math.min(30, Math.round(step)));\n  for (let d = s; d > 1; d--) if (60 % d === 0) return d;\n  return 1;\n}\n\n/** Collapse exotic spaces so server and browser ICU builds produce the same text. */\nconst tidy = (s: string) => s.replace(/[  ]/g, \" \");\n\nfunction localeInfo(locale: string, cycle: ClockHourCycle) {\n  let resolved: Intl.DateTimeFormat;\n  try {\n    resolved = new Intl.DateTimeFormat(locale, { hour: \"numeric\", minute: \"2-digit\", timeZone: \"UTC\" });\n  } catch {\n    resolved = new Intl.DateTimeFormat(\"en\", { hour: \"numeric\", minute: \"2-digit\", timeZone: \"UTC\" });\n  }\n  const lc = resolved.resolvedOptions().locale;\n  const prefers12 = [\"h11\", \"h12\"].includes(resolved.resolvedOptions().hourCycle ?? \"h23\");\n  const twelve = cycle === \"auto\" ? prefers12 : cycle === \"12\";\n  const fmt = new Intl.DateTimeFormat(lc, { hour: \"numeric\", minute: \"2-digit\", hourCycle: twelve ? \"h12\" : \"h23\", timeZone: \"UTC\" });\n  const at = (h: number, m: number) => new Date(Date.UTC(2000, 0, 1, h, m));\n  const period = (h: number) =>\n    tidy(new Intl.DateTimeFormat(lc, { hour: \"numeric\", hourCycle: \"h12\", timeZone: \"UTC\" }).formatToParts(at(h, 0)).find((p) => p.type === \"dayPeriod\")?.value ?? (h < 12 ? \"AM\" : \"PM\"));\n  const parts = fmt.formatToParts(at(9, 30));\n  const periodFirst = parts.findIndex((p) => p.type === \"dayPeriod\") > -1 && parts.findIndex((p) => p.type === \"dayPeriod\") < parts.findIndex((p) => p.type === \"hour\");\n  return {\n    twelve,\n    format: (h: number, m: number) => tidy(fmt.format(at(h, m))),\n    am: period(9),\n    pm: period(21),\n    periodFirst,\n  };\n}\n\n/* ------------------------------- dial geometry ------------------------------ */\n\ninterface DialItem {\n  value: number;\n  label: string;\n  angle: number;\n  inner: boolean;\n}\n\nfunction hourItems(twelve: boolean): DialItem[] {\n  const items: DialItem[] = [];\n  for (let i = 0; i < 12; i++) {\n    // Outer ring 12, 1…11 (12-hour labels or noon / morning hours); inner ring 00, 13…23.\n    items.push({ value: i === 0 ? 12 : i, label: String(i === 0 ? 12 : i), angle: i * 30, inner: false });\n    if (!twelve) items.push({ value: i === 0 ? 0 : i + 12, label: i === 0 ? \"00\" : String(i + 12), angle: i * 30, inner: true });\n  }\n  return items;\n}\n\nfunction minuteItems(): DialItem[] {\n  return Array.from({ length: 12 }, (_, i) => ({ value: i * 5, label: pad(i * 5), angle: i * 30, inner: false }));\n}\n\n/* -------------------------------- component -------------------------------- */\n\n/**\n * An analog time picker: pick the hour on a clock face (12-hour with AM / PM, or 24-hour with an\n * inner ring), then the minute, by tapping or dragging the hand with snapping. The dial is an ARIA\n * slider, the digital readout is editable, and a popover variant uses the native popover API.\n */\nexport function ClockPicker({\n  value,\n  defaultValue = \"09:30\",\n  onChange,\n  hourCycle = \"auto\",\n  minuteStep = 5,\n  variant = \"inline\",\n  locale = \"en-US\",\n  label = \"Time\",\n  autoAdvance = true,\n  size = 232,\n  accent = \"#FF7448\",\n  disabled = false,\n  name,\n  className = \"\",\n  style,\n  ...props\n}: ClockPickerProps) {\n  const uid = React.useId();\n  const id = `cp-${uid}`;\n  const labelId = `${id}-label`;\n  const panelId = `${id}-panel`;\n  const popoverId = `${id}-popover`;\n  const step = cleanStep(minuteStep);\n\n  const [inner, setInner] = React.useState(defaultValue);\n  const current = value ?? inner;\n  const { h, m } = parseTime(current);\n  const info = localeInfo(locale, hourCycle);\n  const isPm = h >= 12;\n\n  const [unit, setUnit] = React.useState<Unit>(\"hour\");\n  const [dragging, setDragging] = React.useState(false);\n  const [drafts, setDrafts] = React.useState<{ hour: string | null; minute: string | null }>({ hour: null, minute: null });\n  const [open, setOpen] = React.useState(false);\n\n  const dialRef = React.useRef<HTMLDivElement>(null);\n  const ringRef = React.useRef<HTMLDivElement>(null);\n  const minuteInputRef = React.useRef<HTMLInputElement>(null);\n  const triggerRef = React.useRef<HTMLButtonElement>(null);\n  const popoverRef = React.useRef<HTMLDivElement>(null);\n  const prevUnit = React.useRef<Unit>(unit);\n\n  const setTime = (nh: number, nm: number) => {\n    const next = toValue(mod(nh, 24), mod(nm, 60));\n    if (next === current) return;\n    if (value === undefined) setInner(next);\n    onChange?.(next);\n  };\n\n  /* ------------------------------ hand rotation ------------------------------ */\n\n  const targetAngle = unit === \"hour\" ? mod(h, 12) * 30 : m * 6;\n  const handInner = unit === \"hour\" && !info.twelve && (h === 0 || h > 12);\n  // Unwrapped angle: always turn the short way (59 → 00 moves forward, not backwards round the dial).\n  const [hand, setHand] = React.useState({ target: targetAngle, angle: targetAngle });\n  if (hand.target !== targetAngle) {\n    const delta = mod(targetAngle - hand.angle + 180, 360) - 180;\n    setHand({ target: targetAngle, angle: hand.angle + delta });\n  }\n\n  // Cross-fade the numbers when switching between hours and minutes.\n  React.useEffect(() => {\n    if (prevUnit.current === unit) return;\n    prevUnit.current = unit;\n    const ring = ringRef.current;\n    if (!ring || window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n    const animation = ring.animate(\n      [\n        { opacity: 0, transform: \"scale(0.92)\" },\n        { opacity: 1, transform: \"scale(1)\" },\n      ],\n      { duration: 220, easing: \"cubic-bezier(0.2, 0.8, 0.2, 1)\" },\n    );\n    return () => animation.cancel();\n  }, [unit]);\n\n  /* ---------------------------------- dial ---------------------------------- */\n\n  const half = size / 2;\n  const outerR = half - 22;\n  const innerR = outerR - 36;\n  const items = unit === \"hour\" ? hourItems(info.twelve) : minuteItems();\n\n  const pickFromPoint = (clientX: number, clientY: number) => {\n    const rect = dialRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    const dx = clientX - (rect.left + rect.width / 2);\n    const dy = clientY - (rect.top + rect.height / 2);\n    const angle = mod((Math.atan2(dx, -dy) * 180) / Math.PI, 360);\n    const scale = rect.width / size;\n    const dist = Math.hypot(dx, dy) / scale;\n    if (unit === \"hour\") {\n      const index = Math.round(angle / 30) % 12;\n      if (info.twelve) setTime((index % 12) + (isPm ? 12 : 0), m);\n      else if (dist < (outerR + innerR) / 2) setTime(index === 0 ? 0 : index + 12, m);\n      else setTime(index === 0 ? 12 : index, m);\n    } else {\n      setTime(h, mod(Math.round(angle / (6 * step)) * step, 60));\n    }\n  };\n\n  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (disabled || event.button !== 0) return;\n    event.preventDefault();\n    event.currentTarget.setPointerCapture(event.pointerId);\n    event.currentTarget.focus({ preventScroll: true });\n    setDragging(true);\n    pickFromPoint(event.clientX, event.clientY);\n  };\n\n  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    // setTime only commits when the snapped value actually changes.\n    if (dragging) pickFromPoint(event.clientX, event.clientY);\n  };\n\n  const endDrag = () => {\n    if (!dragging) return;\n    setDragging(false);\n    if (unit === \"hour\" && autoAdvance) setUnit(\"minute\");\n  };\n\n  const handleDialKey = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    if (disabled) return;\n    const isHour = unit === \"hour\";\n    const unitStep = isHour ? 1 : step;\n    const page = isHour ? 3 : step === 1 ? 5 : 15;\n    const shift = (delta: number) => (isHour ? setTime(h + delta, m) : setTime(h, Math.round((m + delta) / unitStep) * unitStep));\n    switch (event.key) {\n      case \"ArrowUp\":\n      case \"ArrowRight\":\n        shift(unitStep);\n        break;\n      case \"ArrowDown\":\n      case \"ArrowLeft\":\n        shift(-unitStep);\n        break;\n      case \"PageUp\":\n        shift(page);\n        break;\n      case \"PageDown\":\n        shift(-page);\n        break;\n      case \"Home\":\n        if (isHour) setTime(info.twelve && isPm ? 12 : 0, m);\n        else setTime(h, 0);\n        break;\n      case \"End\":\n        if (isHour) setTime(info.twelve && !isPm ? 11 : 23, m);\n        else setTime(h, 60 - step);\n        break;\n      case \"Enter\":\n        if (isHour) setUnit(\"minute\");\n        else if (variant === \"popover\") popoverRef.current?.hidePopover();\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n  };\n\n  /* --------------------------------- readout -------------------------------- */\n\n  const displayHour = info.twelve ? String(h % 12 || 12).padStart(2, \"0\") : pad(h);\n\n  const commitDraft = (which: Unit, text: string) => {\n    setDrafts((d) => ({ ...d, [which]: null }));\n    const n = Number.parseInt(text, 10);\n    if (!Number.isFinite(n)) return;\n    if (which === \"minute\") {\n      if (n >= 0 && n <= 59) setTime(h, n);\n      return;\n    }\n    if (info.twelve) {\n      if (n >= 1 && n <= 12) setTime((n % 12) + (isPm ? 12 : 0), m);\n    } else if (n >= 0 && n <= 23) setTime(n, m);\n  };\n\n  const readoutInput = (which: Unit) => {\n    const isHour = which === \"hour\";\n    const shown = drafts[which] ?? (isHour ? displayHour : pad(m));\n    const min = isHour ? (info.twelve ? 1 : 0) : 0;\n    const max = isHour ? (info.twelve ? 12 : 23) : 59;\n    const now = isHour ? (info.twelve ? h % 12 || 12 : h) : m;\n    const selected = unit === which;\n    return (\n      <input\n        ref={isHour ? undefined : minuteInputRef}\n        type=\"text\"\n        inputMode=\"numeric\"\n        role=\"spinbutton\"\n        aria-label={isHour ? \"Hours\" : \"Minutes\"}\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-valuenow={now}\n        aria-valuetext={isHour ? info.format(h, m) : `${m} ${m === 1 ? \"minute\" : \"minutes\"}`}\n        maxLength={2}\n        disabled={disabled}\n        value={shown}\n        onFocus={(event) => {\n          setUnit(which);\n          event.currentTarget.select();\n        }}\n        onChange={(event) => {\n          const text = event.target.value.replace(/\\D/g, \"\").slice(0, 2);\n          setDrafts((d) => ({ ...d, [which]: text }));\n          if (text.length === 2) {\n            commitDraft(which, text);\n            if (isHour && autoAdvance) minuteInputRef.current?.focus();\n          }\n        }}\n        onBlur={(event) => {\n          if (drafts[which] !== null) commitDraft(which, event.target.value);\n        }}\n        onKeyDown={(event) => {\n          const delta = { ArrowUp: 1, ArrowDown: -1, PageUp: isHour ? 3 : 10, PageDown: isHour ? -3 : -10 }[event.key];\n          if (delta !== undefined) {\n            event.preventDefault();\n            setDrafts((d) => ({ ...d, [which]: null }));\n            if (isHour) setTime(h + delta, m);\n            else setTime(h, m + delta);\n          } else if (event.key === \"Enter\" && drafts[which] !== null) {\n            event.preventDefault();\n            commitDraft(which, event.currentTarget.value);\n          }\n        }}\n        className={`h-14 w-[2.6ch] rounded-lg bg-transparent text-center text-[2.5rem] font-medium leading-none tracking-[-0.04em] tabular-nums outline-hidden transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] disabled:cursor-not-allowed ${\n          selected\n            ? \"bg-[color-mix(in_oklab,var(--cp-accent)_16%,transparent)] text-zinc-950 dark:text-white\"\n            : \"text-zinc-400 hover:text-zinc-700 dark:text-zinc-500 dark:hover:text-zinc-200\"\n        }`}\n      />\n    );\n  };\n\n  const periodToggle = info.twelve ? (\n    <div role=\"group\" aria-label=\"Period\" className=\"flex flex-col overflow-hidden rounded-lg border border-zinc-900/15 dark:border-white/15\">\n      {[\n        { pm: false, text: info.am },\n        { pm: true, text: info.pm },\n      ].map((p) => (\n        <button\n          key={String(p.pm)}\n          type=\"button\"\n          disabled={disabled}\n          aria-pressed={isPm === p.pm}\n          onClick={() => {\n            if (isPm !== p.pm) setTime(h + (p.pm ? 12 : -12), m);\n          }}\n          className=\"h-7 min-w-11 px-2 font-mono text-[11px] uppercase tracking-[0.08em] text-zinc-500 outline-hidden transition-colors first:border-b first:border-zinc-900/15 hover:text-zinc-900 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] aria-pressed:bg-[color-mix(in_oklab,var(--cp-accent)_16%,transparent)] aria-pressed:text-zinc-950 dark:text-zinc-400 dark:first:border-white/15 dark:hover:text-zinc-100 dark:aria-pressed:text-white\"\n        >\n          {p.text}\n        </button>\n      ))}\n    </div>\n  ) : null;\n\n  /* ---------------------------------- popover --------------------------------- */\n\n  const position = React.useCallback(() => {\n    const pop = popoverRef.current;\n    const trigger = triggerRef.current;\n    if (!pop || !trigger) return;\n    const rect = trigger.getBoundingClientRect();\n    const height = pop.offsetHeight;\n    const width = pop.offsetWidth;\n    const below = rect.bottom + 6;\n    const top = below + height > window.innerHeight - 8 && rect.top - height - 6 > 8 ? rect.top - height - 6 : below;\n    pop.style.top = `${Math.round(top)}px`;\n    pop.style.left = `${Math.round(Math.min(Math.max(rect.left, 8), Math.max(window.innerWidth - width - 8, 8)))}px`;\n  }, []);\n\n  React.useEffect(() => {\n    const pop = popoverRef.current;\n    if (!pop || variant !== \"popover\") return;\n    const onToggle = (event: Event) => {\n      const next = (event as ToggleEvent).newState === \"open\";\n      setOpen(next);\n      if (next) {\n        setUnit(\"hour\");\n        requestAnimationFrame(() => dialRef.current?.focus({ preventScroll: true }));\n      } else if (pop.contains(document.activeElement) || document.activeElement === document.body) {\n        triggerRef.current?.focus({ preventScroll: true });\n      }\n    };\n    pop.addEventListener(\"toggle\", onToggle);\n    return () => pop.removeEventListener(\"toggle\", onToggle);\n  }, [variant]);\n\n  React.useEffect(() => {\n    if (!open) return;\n    position();\n    window.addEventListener(\"scroll\", position, { capture: true, passive: true });\n    window.addEventListener(\"resize\", position);\n    return () => {\n      window.removeEventListener(\"scroll\", position, { capture: true });\n      window.removeEventListener(\"resize\", position);\n    };\n  }, [open, position]);\n\n  /* ---------------------------------- render ---------------------------------- */\n\n  const valueText = info.format(h, m);\n  const knobR = 16;\n  const selectedValue = unit === \"hour\" ? (info.twelve ? h % 12 || 12 : h) : m;\n  const handLength = handInner ? innerR : outerR;\n\n  const tabs = (\n    <div role=\"tablist\" aria-label=\"Clock unit\" className=\"flex rounded-md border border-zinc-900/10 p-0.5 dark:border-white/10\">\n      {([\"hour\", \"minute\"] as const).map((u) => (\n        <button\n          key={u}\n          type=\"button\"\n          role=\"tab\"\n          id={`${id}-tab-${u}`}\n          aria-selected={unit === u}\n          aria-controls={panelId}\n          tabIndex={unit === u ? 0 : -1}\n          disabled={disabled}\n          onClick={() => setUnit(u)}\n          onKeyDown={(event) => {\n            if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n              event.preventDefault();\n              const next = u === \"hour\" ? \"minute\" : \"hour\";\n              setUnit(next);\n              event.currentTarget.parentElement?.querySelector<HTMLButtonElement>(`#${CSS.escape(`${id}-tab-${next}`)}`)?.focus();\n            }\n          }}\n          className=\"rounded-[5px] px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 outline-hidden transition-colors hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] aria-selected:bg-zinc-900 aria-selected:text-white dark:text-zinc-400 dark:hover:text-zinc-100 dark:aria-selected:bg-white dark:aria-selected:text-zinc-900\"\n        >\n          {u === \"hour\" ? \"Hour\" : \"Min\"}\n        </button>\n      ))}\n    </div>\n  );\n\n  const panel = (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-center justify-between gap-3\">\n        <span id={labelId} className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n          {label}\n        </span>\n        {tabs}\n      </div>\n\n      <div className=\"flex items-center justify-center gap-2\">\n        {info.periodFirst ? periodToggle : null}\n        <div className=\"flex items-center gap-1\" role=\"group\" aria-label={`${label}, digital`}>\n          {readoutInput(\"hour\")}\n          <span aria-hidden=\"true\" className=\"pb-1 text-[2.25rem] font-light leading-none text-zinc-300 dark:text-zinc-600\">\n            :\n          </span>\n          {readoutInput(\"minute\")}\n        </div>\n        {info.periodFirst ? null : periodToggle}\n      </div>\n\n      <div id={panelId} role=\"tabpanel\" aria-labelledby={`${id}-tab-${unit}`} className=\"flex justify-center\">\n        <div\n          ref={dialRef}\n          role=\"slider\"\n          tabIndex={disabled ? -1 : 0}\n          aria-label={unit === \"hour\" ? \"Hour\" : \"Minute\"}\n          aria-valuemin={0}\n          aria-valuemax={unit === \"hour\" ? 23 : 59}\n          aria-valuenow={unit === \"hour\" ? h : m}\n          aria-valuetext={unit === \"hour\" ? `${valueText}, hour` : `${valueText}, ${m} ${m === 1 ? \"minute\" : \"minutes\"}`}\n          aria-disabled={disabled || undefined}\n          onPointerDown={handlePointerDown}\n          onPointerMove={handlePointerMove}\n          onPointerUp={endDrag}\n          onPointerCancel={endDrag}\n          onKeyDown={handleDialKey}\n          className=\"relative shrink-0 cursor-pointer touch-none select-none rounded-full bg-zinc-900/[0.045] outline-hidden focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] dark:bg-white/[0.06]\"\n          style={{ width: size, height: size }}\n        >\n          {/* Minute ticks (one per minute step) — decorative. */}\n          {unit === \"minute\" && step < 5 ? (\n            <svg aria-hidden=\"true\" className=\"absolute inset-0\" width={size} height={size}>\n              {Array.from({ length: 60 / step }, (_, i) => {\n                const a = (i * step * 6 * Math.PI) / 180;\n                const r1 = half - 5;\n                const r2 = half - ((i * step) % 5 === 0 ? 10 : 8);\n                return (\n                  <line\n                    key={i}\n                    x1={round2(half + Math.sin(a) * r1)}\n                    y1={round2(half - Math.cos(a) * r1)}\n                    x2={round2(half + Math.sin(a) * r2)}\n                    y2={round2(half - Math.cos(a) * r2)}\n                    className=\"stroke-zinc-900/20 dark:stroke-white/20\"\n                    strokeWidth={1}\n                  />\n                );\n              })}\n            </svg>\n          ) : null}\n\n          {/* Hand + knob, rotated as one piece. */}\n          <div\n            aria-hidden=\"true\"\n            className={`pointer-events-none absolute inset-0 ${dragging ? \"\" : \"transition-transform duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none\"}`}\n            style={{ transform: `rotate(${round2(hand.angle)}deg)` }}\n          >\n            <span\n              className=\"absolute left-1/2 w-[2px] -translate-x-1/2 bg-[var(--cp-accent)] transition-[height] duration-200 motion-reduce:transition-none\"\n              style={{ bottom: half, height: handLength }}\n            />\n            <span\n              className=\"absolute left-1/2 rounded-full bg-[var(--cp-accent)] transition-[top] duration-200 motion-reduce:transition-none\"\n              style={{ width: knobR * 2, height: knobR * 2, marginLeft: -knobR, top: half - handLength - knobR }}\n            >\n              {unit === \"minute\" && m % 5 !== 0 ? <span className=\"absolute left-1/2 top-1/2 size-1.5 -translate-1/2 rounded-full bg-zinc-950\" /> : null}\n            </span>\n            <span className=\"absolute left-1/2 top-1/2 size-2 -translate-1/2 rounded-full bg-[var(--cp-accent)]\" />\n          </div>\n\n          {/* Numbers stay upright; the selected one sits on the knob. */}\n          <div ref={ringRef} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n            {items.map((item) => {\n              const r = item.inner ? innerR : outerR;\n              const a = (item.angle * Math.PI) / 180;\n              const isSelected = item.value === selectedValue && (unit === \"minute\" || item.inner === handInner || info.twelve);\n              return (\n                <span\n                  key={`${item.inner ? \"i\" : \"o\"}${item.value}`}\n                  className={`absolute flex size-8 -translate-1/2 items-center justify-center rounded-full font-mono tabular-nums transition-colors duration-150 ${\n                    item.inner ? \"text-[11px]\" : \"text-[13px]\"\n                  } ${\n                    isSelected\n                      ? \"font-semibold text-zinc-950\"\n                      : item.inner\n                        ? \"text-zinc-500 dark:text-zinc-400\"\n                        : \"text-zinc-800 dark:text-zinc-200\"\n                  }`}\n                  style={{ left: round2(half + Math.sin(a) * r), top: round2(half - Math.cos(a) * r) }}\n                >\n                  {item.label}\n                </span>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n\n      <p className=\"text-center font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">\n        {valueText}\n      </p>\n    </div>\n  );\n\n  const rootStyle = { [\"--cp-accent\" as string]: accent, ...style };\n  const hidden = name ? <input type=\"hidden\" name={name} value={toValue(h, m)} /> : null;\n\n  if (variant === \"inline\") {\n    return (\n      <div\n        role=\"group\"\n        aria-labelledby={labelId}\n        className={`inline-flex rounded-2xl border border-zinc-900/10 bg-white p-4 text-zinc-900 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100 ${\n          disabled ? \"opacity-50\" : \"\"\n        } ${className}`}\n        style={rootStyle}\n        {...props}\n      >\n        {panel}\n        {hidden}\n      </div>\n    );\n  }\n\n  return (\n    <div className={`inline-flex flex-col gap-1.5 ${className}`} style={rootStyle} {...props}>\n      <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\">{label}</span>\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        disabled={disabled}\n        popoverTarget={popoverId}\n        aria-haspopup=\"dialog\"\n        aria-expanded={open}\n        aria-controls={popoverId}\n        aria-label={`${label}: ${valueText}. Change time`}\n        className=\"flex h-10 min-w-40 items-center justify-between gap-3 rounded-lg border border-zinc-900/15 bg-white px-3 text-left text-[14px] tabular-nums text-zinc-900 outline-hidden transition-colors hover:border-zinc-900/30 focus-visible:border-[var(--cp-accent)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/15 dark:bg-white/[0.04] dark:text-zinc-100 dark:hover:border-white/30\"\n      >\n        <span>{valueText}</span>\n        <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"size-4 text-zinc-400\">\n          <circle cx=\"8\" cy=\"8\" r=\"6\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.3\" />\n          <path d=\"M8 4.5V8l2.5 1.5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.3\" strokeLinecap=\"round\" />\n        </svg>\n      </button>\n      <div\n        ref={popoverRef}\n        id={popoverId}\n        popover=\"auto\"\n        role=\"dialog\"\n        aria-label={label}\n        onKeyDown={(event) => {\n          // Escape closes only this layer.\n          if (event.key === \"Escape\") {\n            event.preventDefault();\n            event.stopPropagation();\n            popoverRef.current?.hidePopover();\n          }\n        }}\n        className=\"fixed inset-auto m-0 rounded-2xl border border-zinc-900/10 bg-white p-4 text-zinc-900 shadow-[0_24px_48px_-24px_rgb(0_0_0/0.35)] dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-100\"\n        style={rootStyle}\n      >\n        {open ? (\n          <div className=\"flex flex-col gap-3\">\n            {panel}\n            <button\n              type=\"button\"\n              onClick={() => popoverRef.current?.hidePopover()}\n              className=\"h-9 rounded-lg bg-zinc-900 text-[13px] font-medium text-white outline-hidden transition-colors hover:bg-zinc-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--cp-accent)] dark:bg-white dark:text-zinc-900 dark:hover:bg-zinc-200\"\n            >\n              Done\n            </button>\n          </div>\n        ) : null}\n      </div>\n      {hidden}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Documentation: https://wancraft.my/components/clock-picker"
}
