{"version":3,"file":"clockDial.mjs","names":[],"sources":["../../../../../../package/common/timeInput/components/clockDial/clockDial.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport cx from 'classnames';\nimport { type TimeValue, clampHour, snapMinute, polarPoint, pointerAngle, pointerRadius } from '../../timeFunctions';\nimport type { ClockDialProps, DialNumber } from './clockDial.types';\n\nimport styles from './clockDial.module.pcss';\n\n// viewBox units (0–100). The face scales with its CSS box; geometry stays in this space.\nconst CENTER = 50;\nconst OUTER_RADIUS = 40;\nconst INNER_RADIUS = 25.5;\nconst RING_THRESHOLD = (OUTER_RADIUS + INNER_RADIUS) / 2;\n\nconst HOUR_STEP_DEG = 30;\nconst MINUTE_STEP_DEG = 6;\n\nconst to12 = (hour: number): number => ((hour + 11) % 12) + 1;\n\n/** Build the labelled numbers for the current mode/format. */\nfunction buildNumbers(mode: 'hours' | 'minutes', use24Hour: boolean): DialNumber[] {\n  if (mode === 'minutes') {\n    return Array.from({ length: 12 }, (_, slot) => {\n      const value = slot * 5;\n      return { label: String(value).padStart(2, '0'), slot, value, ring: 'outer' as const };\n    });\n  }\n\n  if (!use24Hour) {\n    return Array.from({ length: 12 }, (_, slot) => {\n      const value = slot === 0 ? 12 : slot; // displayed hour (1–12)\n      return { label: String(value), slot, value, ring: 'outer' as const };\n    });\n  }\n\n  // 24-hour double ring: outer 1–12, inner 00/13–23.\n  const outer = Array.from({ length: 12 }, (_, slot) => {\n    const value = slot === 0 ? 12 : slot;\n    return { label: String(value), slot, value, ring: 'outer' as const };\n  });\n  const inner = Array.from({ length: 12 }, (_, slot) => {\n    const value = slot === 0 ? 0 : slot + 12;\n    return { label: String(value).padStart(2, '0'), slot, value, ring: 'inner' as const };\n  });\n  return [...outer, ...inner];\n}\n\nexport default function ClockDial({\n  time,\n  mode,\n  use24Hour,\n  minuteStep,\n  onChange,\n  onCommit,\n  classNames,\n  styles: slotStyles,\n}: ClockDialProps) {\n  const faceRef = useRef<HTMLDivElement>(null);\n  const draggingRef = useRef(false);\n\n  const numbers = buildNumbers(mode, use24Hour);\n\n  // Resolve the active value and the hand geometry for the current time.\n  const isPm = time.hours >= 12;\n  const selectedHour12 = to12(time.hours);\n\n  const hand = (() => {\n    if (mode === 'minutes') {\n      // 6° per minute → angle = minutes * MINUTE_STEP_DEG.\n      return { slot: time.minutes, radius: OUTER_RADIUS };\n    }\n    if (!use24Hour) {\n      return { slot: selectedHour12 % 12, radius: OUTER_RADIUS };\n    }\n    if (time.hours === 0) return { slot: 0, radius: INNER_RADIUS };\n    if (time.hours <= 12) return { slot: time.hours % 12, radius: OUTER_RADIUS };\n    return { slot: (time.hours - 12) % 12, radius: INNER_RADIUS };\n  })();\n\n  const knob = polarPoint(CENTER, CENTER, hand.radius, hand.slot, mode === 'minutes' ? MINUTE_STEP_DEG : HOUR_STEP_DEG);\n\n  const isSelected = (n: DialNumber): boolean =>\n    mode === 'minutes'\n      ? snapMinute(time.minutes, minuteStep) === n.value || time.minutes === n.value\n      : use24Hour\n        ? time.hours === n.value\n        : selectedHour12 === n.value;\n\n  const valueFromPointer = (clientX: number, clientY: number): TimeValue => {\n    const rect = faceRef.current?.getBoundingClientRect();\n    if (!rect) return time;\n    const dx = clientX - (rect.left + rect.width / 2);\n    const dy = clientY - (rect.top + rect.height / 2);\n    const angle = pointerAngle(dx, dy);\n\n    if (mode === 'minutes') {\n      const minutes = snapMinute(Math.round(angle / MINUTE_STEP_DEG), minuteStep);\n      return { hours: time.hours, minutes };\n    }\n\n    const slot = Math.round(angle / HOUR_STEP_DEG) % 12;\n    if (!use24Hour) {\n      const display = slot === 0 ? 12 : slot;\n      const hours = isPm ? (display % 12) + 12 : display % 12;\n      return { hours: clampHour(hours), minutes: time.minutes };\n    }\n\n    // 24h: inner vs outer ring decided by pointer distance from centre.\n    const radiusFrac = (pointerRadius(dx, dy) / (rect.width / 2)) * CENTER;\n    const inner = radiusFrac < RING_THRESHOLD;\n    const hours = inner ? (slot === 0 ? 0 : slot + 12) : slot === 0 ? 12 : slot;\n    return { hours: clampHour(hours), minutes: time.minutes };\n  };\n\n  const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    draggingRef.current = true;\n    // setPointerCapture keeps move/up events flowing to the face even when the\n    // pointer started on (or drags over) a number. Guarded for jsdom.\n    (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);\n    onChange(valueFromPointer(e.clientX, e.clientY));\n  };\n\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!draggingRef.current) return;\n    onChange(valueFromPointer(e.clientX, e.clientY));\n  };\n\n  const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!draggingRef.current) return;\n    draggingRef.current = false;\n    onCommit(valueFromPointer(e.clientX, e.clientY));\n  };\n\n  return (\n    <div\n      ref={faceRef}\n      className={cx(styles.clockFace, classNames?.clockFace)}\n      style={slotStyles?.clockFace}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={handlePointerUp}\n      role='slider'\n      aria-label={mode === 'minutes' ? 'Minutes' : 'Hours'}\n      aria-valuetext={mode === 'minutes' ? `${time.minutes} minutes` : `${time.hours} hours`}\n    >\n      <svg className={styles.clockHand} viewBox='0 0 100 100' aria-hidden>\n        <circle cx={CENTER} cy={CENTER} r={1.8} className={styles.clockHandCenter} />\n        <line x1={CENTER} y1={CENTER} x2={knob.x} y2={knob.y} className={styles.clockHandLine} />\n        <circle\n          cx={knob.x}\n          cy={knob.y}\n          r={7}\n          className={cx(styles.clockHandKnob, classNames?.clockHand)}\n          style={slotStyles?.clockHand as React.CSSProperties}\n        />\n      </svg>\n\n      {numbers.map((n) => {\n        const p = polarPoint(CENTER, CENTER, n.ring === 'inner' ? INNER_RADIUS : OUTER_RADIUS, n.slot, HOUR_STEP_DEG);\n        const selected = isSelected(n);\n        // Purely visual — the face owns all pointer handling so click and drag\n        // behave identically whether the pointer lands on a number or between two.\n        return (\n          <div\n            key={`${n.ring}-${n.value}`}\n            className={cx(\n              styles.clockNumber,\n              n.ring === 'inner' && styles.clockNumberInner,\n              selected && styles.clockNumberSelected,\n              classNames?.clockNumber,\n            )}\n            style={{ left: `${p.x}%`, top: `${p.y}%`, ...slotStyles?.clockNumber }}\n          >\n            {n.label}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"],"mappings":"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"}