{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-picker-moon",
  "title": "Date Picker Moon",
  "description": "Masked date input whose popover calendar doubles as a moon-phase almanac: every day cell carries a canvas moon with a real synodic terminator, the field icon tracks the focused date's phase live, and committing runs an eclipse transit across the chosen numeral.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/date-picker-moon/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport type { KeyboardEvent as ReactKeyboardEvent } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TerminatorDateField — masked MM/DD/YYYY date input whose popover calendar\n// doubles as a moon-phase almanac. Every one of the 42 day cells carries a\n// 14px canvas moon whose terminator curve comes from the real mean synodic\n// month (29.530588853 d, reference new moon 2000-01-06 18:14 UTC); a 20px\n// canvas in the input's trailing slot mirrors the focused date's phase live\n// (150ms crossfade), and committing a date sweeps an eclipse shadow-disc\n// across the chosen numeral (420ms ease-out-expo, cleared and redrawn every\n// frame — zero accumulation). ONE canvas sits behind the whole grid; glyph\n// positions derive from measured cell rects after layout settles. All ink is\n// getComputedStyle-token-derived at mount and re-derived on documentElement\n// class changes. rAF runs only during crossfade/transit; the static almanac\n// costs zero frames.\n// ---------------------------------------------------------------------------\n\nconst SYNODIC = 29.530588853; // mean synodic month, days\nconst EPOCH_MS = Date.UTC(2000, 0, 6, 18, 14); // reference new moon, UTC\nconst DAY_MS = 86400000;\nconst ICON = 20; // trailing-slot canvas, px\nconst ICON_R = 8;\nconst MOON_R = 7; // 14px grid discs\nconst NUM_Y = 13; // numeral center within a 44px cell\nconst MOON_Y = 31; // moon center within a 44px cell\nconst CROSSFADE_MS = 150;\nconst TRANSIT_MS = 420;\n\nconst PHASE_NAMES = [\n  \"New moon\",\n  \"Waxing crescent\",\n  \"First quarter\",\n  \"Waxing gibbous\",\n  \"Full moon\",\n  \"Waning gibbous\",\n  \"Last quarter\",\n  \"Waning crescent\",\n] as const;\nconst WEEKDAYS = [\n  [\"Su\", \"Sunday\"],\n  [\"Mo\", \"Monday\"],\n  [\"Tu\", \"Tuesday\"],\n  [\"We\", \"Wednesday\"],\n  [\"Th\", \"Thursday\"],\n  [\"Fr\", \"Friday\"],\n  [\"Sa\", \"Saturday\"],\n] as const;\nconst MONTHS = [\n  \"January\",\n  \"February\",\n  \"March\",\n  \"April\",\n  \"May\",\n  \"June\",\n  \"July\",\n  \"August\",\n  \"September\",\n  \"October\",\n  \"November\",\n  \"December\",\n] as const;\n\ntype RGB = readonly [number, number, number];\ninterface Ink {\n  fg: RGB;\n  bd: RGB;\n  bg: RGB;\n}\ninterface CellGeom {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  cx: number;\n  numY: number;\n  moonY: number;\n  f: number; // phase fraction for this cell's date\n  dim: boolean; // outside the viewed month\n}\ninterface GridGeom {\n  w: number;\n  h: number;\n  dpr: number;\n  cells: (CellGeom | null)[];\n}\n\nfunction parseColor(raw: string): RGB | null {\n  const s = raw.trim();\n  if (s.startsWith(\"#\")) {\n    const hex = s.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex.slice(0, 1) + hex.slice(0, 1), 16);\n      const g = parseInt(hex.slice(1, 2) + hex.slice(1, 2), 16);\n      const b = parseInt(hex.slice(2, 3) + hex.slice(2, 3), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    if (hex.length >= 6) {\n      const r = parseInt(hex.slice(0, 2), 16);\n      const g = parseInt(hex.slice(2, 4), 16);\n      const b = parseInt(hex.slice(4, 6), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    return null;\n  }\n  const m = s.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/);\n  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n}\n\nconst rgba = (c: RGB, a: number) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;\n\n/** phase fraction f in [0,1): 0 = new, 0.5 = full. Sampled at local noon. */\nfunction phaseFraction(d: Date): number {\n  const noon = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 12);\n  const days = (noon.getTime() - EPOCH_MS) / DAY_MS;\n  return (((days % SYNODIC) + SYNODIC) % SYNODIC) / SYNODIC;\n}\nconst phaseIndex = (f: number) => Math.round(f * 8) % 8;\nconst illumination = (f: number) => (1 - Math.cos(2 * Math.PI * f)) / 2;\n\n/**\n * Disc stroked --border; lit region --foreground at 85% alpha. Terminator is\n * a semi-ellipse with x-radius r*|cos(2πf)|; the lit limb sits right while\n * waxing (f < 0.5) and left while waning, ellipse sweep flips crescent/gibbous.\n */\nfunction drawMoon(\n  ctx: CanvasRenderingContext2D,\n  cx: number,\n  cy: number,\n  r: number,\n  f: number,\n  ink: Ink,\n  alpha: number\n) {\n  const phi = 2 * Math.PI * f;\n  const c = Math.cos(phi);\n  const k = (1 - c) / 2;\n  ctx.beginPath();\n  ctx.arc(cx, cy, r, 0, Math.PI * 2);\n  ctx.strokeStyle = rgba(ink.bd, 0.9 * alpha);\n  ctx.lineWidth = 1;\n  ctx.stroke();\n  if (k < 0.015) return; // new moon — outline only\n  const ri = r - 0.5;\n  ctx.beginPath();\n  if (k > 0.985) {\n    ctx.arc(cx, cy, ri, 0, Math.PI * 2);\n  } else {\n    const waxing = f < 0.5;\n    const rx = Math.min(ri, Math.max(0.02, ri * Math.abs(c)));\n    // lit limb: right semicircle for waxing, left for waning (top -> bottom)\n    ctx.arc(cx, cy, ri, -Math.PI / 2, Math.PI / 2, !waxing);\n    // terminator: bottom -> top, bulging toward or away from the lit limb\n    ctx.ellipse(cx, cy, rx, ri, 0, Math.PI / 2, -Math.PI / 2, waxing ? c > 0 : c < 0);\n  }\n  ctx.fillStyle = rgba(ink.fg, 0.85 * alpha);\n  ctx.fill();\n}\n\nconst easeOutExpo = (t: number) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));\nconst pad2 = (n: number) => String(n).padStart(2, \"0\");\nconst pad4 = (n: number) => String(n).padStart(4, \"0\");\nconst fmt = (d: Date) =>\n  `${pad2(d.getMonth() + 1)}/${pad2(d.getDate())}/${pad4(d.getFullYear())}`;\nconst addDays = (d: Date, n: number) =>\n  new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);\nfunction addMonths(d: Date, n: number) {\n  const y = d.getFullYear();\n  const m = d.getMonth() + n;\n  const last = new Date(y, m + 1, 0).getDate();\n  return new Date(y, m, Math.min(d.getDate(), last));\n}\nconst sameDay = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() &&\n  a.getMonth() === b.getMonth() &&\n  a.getDate() === b.getDate();\n\n/** 42-cell month sheet starting on the Sunday on/before the 1st. */\nfunction cellDates(y: number, m: number): Date[] {\n  const offset = new Date(y, m, 1).getDay();\n  return Array.from({ length: 42 }, (_, i) => new Date(y, m, i + 1 - offset));\n}\n\n/** MM/DD/YYYY mask: digits only, slashes inserted as you type. */\nfunction maskText(raw: string): string {\n  const d = raw.replace(/\\D/g, \"\").slice(0, 8);\n  let out = d.slice(0, 2);\n  if (d.length > 2) out += \"/\" + d.slice(2, 4);\n  if (d.length > 4) out += \"/\" + d.slice(4, 8);\n  return out;\n}\nfunction parseMasked(text: string): Date | null {\n  const m = text.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{4})$/);\n  if (!m) return null;\n  const mo = Number(m[1]);\n  const da = Number(m[2]);\n  const yr = Number(m[3]);\n  if (yr < 1000 || yr > 9999) return null; // reject non-4-digit-real years\n  const d = new Date(yr, mo - 1, da);\n  return d.getFullYear() === yr && d.getMonth() === mo - 1 && d.getDate() === da\n    ? d\n    : null;\n}\n\nexport function TerminatorDateField({\n  value,\n  defaultValue = null,\n  onValueChange,\n  label = \"Date\",\n  disabled = false,\n  className = \"\",\n}: {\n  /** controlled selected date; omit for uncontrolled */\n  value?: Date | null;\n  /** uncontrolled initial selected date */\n  defaultValue?: Date | null;\n  /** called with the new date when a day is picked */\n  onValueChange?: (date: Date) => void;\n  /** field label rendered above the trigger */\n  label?: string;\n  /** blocks the trigger and closes the calendar if open */\n  disabled?: boolean;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const uid = useId();\n  const inputId = `${uid}-input`;\n  const helperId = `${uid}-helper`;\n  const popId = `${uid}-pop`;\n\n  const isControlled = value !== undefined;\n  const [internal, setInternal] = useState<Date | null>(defaultValue);\n  const selected = isControlled ? (value ?? null) : internal;\n\n  const [text, setText] = useState(() => (selected ? fmt(selected) : \"\"));\n  const [open, setOpen] = useState(false);\n  const [focusedDate, setFocusedDate] = useState<Date>(\n    () => selected ?? new Date()\n  );\n  const [view, setView] = useState(() => {\n    const b = selected ?? new Date();\n    return { y: b.getFullYear(), m: b.getMonth() };\n  });\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const popRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const iconCanvasRef = useRef<HTMLCanvasElement>(null);\n  const gridWrapRef = useRef<HTMLDivElement>(null);\n  const gridCanvasRef = useRef<HTMLCanvasElement>(null);\n  const cellRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const geomRef = useRef<GridGeom | null>(null);\n  const justOpenedRef = useRef(false);\n  const closeTimerRef = useRef(0);\n  const pendingCaretRef = useRef<number | null>(null);\n  const animRef = useRef<{\n    iconF: number | null;\n    iconAnim: { from: number; to: number; start: number } | null;\n    transit: { idx: number; start: number } | null;\n  }>({ iconF: null, iconAnim: null, transit: null });\n  const engineRef = useRef<{\n    setIcon: (f: number) => void;\n    startTransit: (idx: number) => boolean;\n    redrawGrid: () => void;\n  } | null>(null);\n\n  const cells = useMemo(() => cellDates(view.y, view.m), [view.y, view.m]);\n  const today = useMemo(() => new Date(), []);\n  const invalid = text.length === 10 && parseMasked(text) === null;\n  const previewDate = open\n    ? focusedDate\n    : (parseMasked(text) ?? selected ?? today);\n  const previewF = phaseFraction(previewDate);\n  const previewName = PHASE_NAMES[phaseIndex(previewF)];\n\n  // -- engine: token ink, icon crossfade, grid redraw, eclipse transit -------\n  useEffect(() => {\n    const icon = iconCanvasRef.current;\n    const root = rootRef.current;\n    if (!icon || !root) return;\n    const ictx = icon.getContext(\"2d\");\n    if (!ictx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n    const dpr = Math.min(2, window.devicePixelRatio || 1);\n    // <canvas> is a replaced element — size it explicitly, never via inset\n    icon.style.width = `${ICON}px`;\n    icon.style.height = `${ICON}px`;\n    icon.width = ICON * dpr;\n    icon.height = ICON * dpr;\n\n    const ink: Ink = {\n      fg: [237, 237, 237],\n      bd: [46, 46, 46],\n      bg: [10, 10, 10],\n    };\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      ink.fg = parseColor(cs.getPropertyValue(\"--foreground\")) ?? ink.fg;\n      ink.bd = parseColor(cs.getPropertyValue(\"--border\")) ?? ink.bd;\n      ink.bg = parseColor(cs.getPropertyValue(\"--background\")) ?? ink.bg;\n    };\n    derive();\n\n    let raf = 0;\n    let visible = true;\n\n    const drawIcon = (from: number | null, to: number, t: number) => {\n      ictx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ictx.clearRect(0, 0, ICON, ICON);\n      if (from !== null && t < 1)\n        drawMoon(ictx, ICON / 2, ICON / 2, ICON_R, from, ink, 1 - t);\n      drawMoon(\n        ictx,\n        ICON / 2,\n        ICON / 2,\n        ICON_R,\n        to,\n        ink,\n        from !== null ? Math.min(1, t) : 1\n      );\n    };\n\n    const drawGridStatic = () => {\n      const canvas = gridCanvasRef.current;\n      const g = geomRef.current;\n      if (!canvas || !g) return;\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) return;\n      ctx.setTransform(g.dpr, 0, 0, g.dpr, 0, 0);\n      ctx.clearRect(0, 0, g.w, g.h);\n      for (const c of g.cells) {\n        if (!c) continue;\n        drawMoon(ctx, c.cx, c.moonY, MOON_R, c.f, ink, c.dim ? 0.45 : 1);\n      }\n    };\n\n    // full clear + full redraw every transit frame — no accumulation\n    const drawGridFrame = (p: number, idx: number) => {\n      const canvas = gridCanvasRef.current;\n      const g = geomRef.current;\n      if (!canvas || !g) return;\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) return;\n      drawGridStatic();\n      const cell = g.cells[idx];\n      if (!cell) return;\n      const e = easeOutExpo(p);\n      const env = p < 0.15 ? p / 0.15 : p > 0.85 ? (1 - p) / 0.15 : 1;\n      const R = 10;\n      ctx.save();\n      ctx.beginPath();\n      ctx.rect(cell.x, cell.y, cell.w, cell.h);\n      ctx.clip();\n      ctx.globalAlpha = env;\n      // lit corona behind the numeral...\n      ctx.beginPath();\n      ctx.arc(cell.cx, cell.numY, R, 0, Math.PI * 2);\n      ctx.fillStyle = rgba(ink.fg, 0.22);\n      ctx.fill();\n      // ...eclipsed by a background-ink shadow disc sweeping across it\n      const sx = cell.cx - R * 2.6 + e * R * 5.2;\n      ctx.beginPath();\n      ctx.arc(sx, cell.numY, R + 1, 0, Math.PI * 2);\n      ctx.fillStyle = rgba(ink.bg, 1);\n      ctx.fill();\n      ctx.strokeStyle = rgba(ink.bd, 1);\n      ctx.lineWidth = 1;\n      ctx.stroke();\n      ctx.restore();\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      if (document.hidden || !visible) return; // parked; wake resumes\n      const A = animRef.current;\n      let active = false;\n      if (A.iconAnim) {\n        const t = Math.min(1, (now - A.iconAnim.start) / CROSSFADE_MS);\n        drawIcon(A.iconAnim.from, A.iconAnim.to, t);\n        if (t >= 1) A.iconAnim = null;\n        else active = true;\n      }\n      if (A.transit) {\n        const p = Math.min(1, (now - A.transit.start) / TRANSIT_MS);\n        drawGridFrame(p, A.transit.idx);\n        if (p >= 1) {\n          A.transit = null;\n          drawGridStatic();\n        } else active = true;\n      }\n      if (active) raf = requestAnimationFrame(tick);\n    };\n    const wake = () => {\n      if (!raf && !document.hidden && visible) raf = requestAnimationFrame(tick);\n    };\n\n    const setIcon = (f: number) => {\n      const A = animRef.current;\n      if (A.iconF === null || reduced || Math.abs(f - A.iconF) < 1e-4) {\n        A.iconF = f;\n        A.iconAnim = null;\n        drawIcon(null, f, 1);\n        return;\n      }\n      A.iconAnim = { from: A.iconF, to: f, start: performance.now() };\n      A.iconF = f;\n      wake();\n    };\n\n    const startTransit = (idx: number) => {\n      const g = geomRef.current;\n      if (reduced || !g || !g.cells[idx] || !gridCanvasRef.current)\n        return false;\n      animRef.current.transit = { idx, start: performance.now() };\n      wake();\n      return true;\n    };\n\n    engineRef.current = { setIcon, startTransit, redrawGrid: drawGridStatic };\n\n    // live theme re-derive: watch documentElement class flips\n    const mo = new MutationObserver(() => {\n      derive();\n      const A = animRef.current;\n      if (A.iconF !== null) drawIcon(null, A.iconF, 1);\n      drawGridStatic();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n    });\n    io.observe(root);\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      mo.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      animRef.current.iconAnim = null;\n      animRef.current.transit = null;\n      engineRef.current = null;\n    };\n  }, []);\n\n  // trailing icon tracks the focused/typed date's phase\n  useEffect(() => {\n    engineRef.current?.setIcon(previewF);\n  }, [previewF]);\n\n  // -- grid canvas: measure real cell rects after layout settles, draw once --\n  useEffect(() => {\n    if (!open) {\n      geomRef.current = null;\n      return;\n    }\n    const wrap = gridWrapRef.current;\n    const canvas = gridCanvasRef.current;\n    if (!wrap || !canvas) return;\n    let raf1 = 0;\n    let raf2 = 0;\n    const measure = () => {\n      const rect = wrap.getBoundingClientRect();\n      if (rect.width < 8 || rect.height < 8) {\n        geomRef.current = null; // zero-size guard\n        return;\n      }\n      const dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.style.width = `${rect.width}px`; // explicit — replaced element\n      canvas.style.height = `${rect.height}px`;\n      canvas.width = Math.round(rect.width * dpr);\n      canvas.height = Math.round(rect.height * dpr);\n      const gcells: (CellGeom | null)[] = new Array(42).fill(null);\n      cells.forEach((date, i) => {\n        const el = cellRefs.current[i];\n        if (!el) return;\n        const b = el.getBoundingClientRect();\n        const x = b.left - rect.left;\n        const y = b.top - rect.top;\n        gcells[i] = {\n          x,\n          y,\n          w: b.width,\n          h: b.height,\n          cx: x + b.width / 2,\n          numY: y + NUM_Y,\n          moonY: y + MOON_Y,\n          f: phaseFraction(date),\n          dim: date.getMonth() !== view.m,\n        };\n      });\n      geomRef.current = { w: rect.width, h: rect.height, dpr, cells: gcells };\n      engineRef.current?.redrawGrid();\n    };\n    // double rAF: let popover layout settle before reading cell offsets\n    raf1 = requestAnimationFrame(() => {\n      raf2 = requestAnimationFrame(measure);\n    });\n    const ro = new ResizeObserver(() => {\n      cancelAnimationFrame(raf2);\n      raf2 = requestAnimationFrame(measure);\n    });\n    ro.observe(wrap);\n    return () => {\n      cancelAnimationFrame(raf1);\n      cancelAnimationFrame(raf2);\n      ro.disconnect();\n      geomRef.current = null;\n      animRef.current.transit = null;\n    };\n  }, [open, cells, view.m]);\n\n  // roving focus follows the focused cell while the grid owns focus\n  useEffect(() => {\n    if (!open) return;\n    const idx = cells.findIndex((c) => sameDay(c, focusedDate));\n    if (idx < 0) return;\n    const el = cellRefs.current[idx];\n    if (!el) return;\n    const ae = document.activeElement;\n    const inGrid =\n      ae instanceof HTMLElement && ae.getAttribute(\"role\") === \"gridcell\";\n    if (justOpenedRef.current || (popRef.current?.contains(ae) && inGrid)) {\n      el.focus();\n      justOpenedRef.current = false;\n    }\n  }, [open, focusedDate, cells]);\n\n  // outside click closes without stealing focus\n  useEffect(() => {\n    if (!open) return;\n    const onDown = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) close(false);\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open]);\n\n  // sync typed text when a controlled value changes from outside\n  const controlledTime = isControlled ? (value ? value.getTime() : -1) : null;\n  useEffect(() => {\n    if (controlledTime === null) return;\n    setText(value ? fmt(value) : \"\");\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [controlledTime]);\n\n  useEffect(() => {\n    return () => window.clearTimeout(closeTimerRef.current);\n  }, []);\n\n  // restore caret after the mask reformats `text` — React resets the DOM\n  // value on every render, which otherwise collapses the caret to the end\n  useLayoutEffect(() => {\n    const pos = pendingCaretRef.current;\n    if (pos === null) return;\n    pendingCaretRef.current = null;\n    inputRef.current?.setSelectionRange(pos, pos);\n  }, [text]);\n\n  const close = (focusBack: boolean) => {\n    window.clearTimeout(closeTimerRef.current);\n    animRef.current.transit = null;\n    setOpen(false);\n    if (focusBack) inputRef.current?.focus();\n  };\n\n  const openPopover = () => {\n    if (disabled) return;\n    const base = parseMasked(text) ?? selected ?? new Date();\n    setFocusedDate(base);\n    setView({ y: base.getFullYear(), m: base.getMonth() });\n    justOpenedRef.current = true;\n    setOpen(true);\n  };\n\n  const moveFocus = (d: Date) => {\n    setFocusedDate(d);\n    setView({ y: d.getFullYear(), m: d.getMonth() });\n  };\n\n  const commit = (date: Date, idx: number | null) => {\n    if (!isControlled) setInternal(date);\n    onValueChange?.(date);\n    setText(fmt(date));\n    setFocusedDate(date);\n    if (open) {\n      if (idx !== null && engineRef.current?.startTransit(idx)) {\n        closeTimerRef.current = window.setTimeout(\n          () => close(true),\n          TRANSIT_MS + 60\n        );\n      } else close(true);\n    }\n  };\n\n  const onGridKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {\n    let d: Date | null = null;\n    switch (e.key) {\n      case \"ArrowLeft\":\n        d = addDays(focusedDate, -1);\n        break;\n      case \"ArrowRight\":\n        d = addDays(focusedDate, 1);\n        break;\n      case \"ArrowUp\":\n        d = addDays(focusedDate, -7);\n        break;\n      case \"ArrowDown\":\n        d = addDays(focusedDate, 7);\n        break;\n      case \"Home\":\n        d = addDays(focusedDate, -focusedDate.getDay());\n        break;\n      case \"End\":\n        d = addDays(focusedDate, 6 - focusedDate.getDay());\n        break;\n      case \"PageUp\":\n        d = addMonths(focusedDate, e.shiftKey ? -12 : -1);\n        break;\n      case \"PageDown\":\n        d = addMonths(focusedDate, e.shiftKey ? 12 : 1);\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    if (d) moveFocus(d);\n  };\n\n  const focusedIdx = cells.findIndex((c) => sameDay(c, focusedDate));\n  const tabbableIdx =\n    focusedIdx >= 0 ? focusedIdx : new Date(view.y, view.m, 1).getDay();\n  const monthLabel = `${MONTHS[view.m]} ${view.y}`;\n\n  return (\n    <div ref={rootRef} className={`relative ${className}`}>\n      <label\n        htmlFor={inputId}\n        className=\"mb-1.5 block text-[13px] font-medium text-foreground\"\n      >\n        {label}\n      </label>\n\n      <div\n        className={`flex items-center gap-1.5 rounded-sm border bg-background pl-3 pr-1.5 transition-colors ${\n          invalid ? \"border-foreground/40\" : \"border-border\"\n        } ${\n          disabled\n            ? \"cursor-not-allowed opacity-60\"\n            : \"focus-within:border-ns-accent focus-within:ring-2 focus-within:ring-ns-accent/25 hover:border-foreground/25\"\n        }`}\n      >\n        <input\n          ref={inputRef}\n          id={inputId}\n          type=\"text\"\n          inputMode=\"numeric\"\n          autoComplete=\"off\"\n          placeholder=\"MM/DD/YYYY\"\n          disabled={disabled}\n          value={text}\n          aria-invalid={invalid || undefined}\n          aria-describedby={helperId}\n          onChange={(e) => {\n            const raw = e.target.value;\n            const caret = e.target.selectionStart ?? raw.length;\n            // caret position expressed as \"how many digits precede it\" is\n            // stable across reformatting; slash count shifts around it\n            const digitsBeforeCaret = raw\n              .slice(0, caret)\n              .replace(/\\D/g, \"\").length;\n            const next = maskText(raw);\n            let pos = next.length;\n            if (digitsBeforeCaret <= 0) {\n              pos = 0;\n            } else {\n              let seen = 0;\n              for (let i = 0; i < next.length; i++) {\n                if (/\\d/.test(next[i])) {\n                  seen++;\n                  if (seen === digitsBeforeCaret) {\n                    pos = i + 1;\n                    break;\n                  }\n                }\n              }\n            }\n            pendingCaretRef.current = pos;\n            setText(next);\n            const d = parseMasked(next);\n            if (d) {\n              if (!isControlled) setInternal(d);\n              onValueChange?.(d);\n              setFocusedDate(d);\n              setView({ y: d.getFullYear(), m: d.getMonth() });\n            }\n          }}\n          onKeyDown={(e) => {\n            if (e.key === \"ArrowDown\") {\n              e.preventDefault();\n              openPopover();\n            } else if (e.key === \"Escape\" && open) {\n              e.preventDefault();\n              close(true);\n            } else if (e.key === \"Enter\" && open) {\n              e.preventDefault();\n              close(false);\n            }\n          }}\n          onBlur={(e) => {\n            if (rootRef.current?.contains(e.relatedTarget as Node)) return;\n            if (text !== \"\" && parseMasked(text) === null)\n              setText(selected ? fmt(selected) : \"\");\n          }}\n          className=\"h-9 w-full bg-transparent font-mono text-sm text-foreground placeholder:text-ns-muted/60 focus:outline-none disabled:cursor-not-allowed\"\n        />\n        <button\n          type=\"button\"\n          disabled={disabled}\n          aria-label={`${open ? \"Close\" : \"Open\"} calendar. ${previewName}.`}\n          aria-expanded={open}\n          aria-haspopup=\"grid\"\n          aria-controls={open ? popId : undefined}\n          onClick={() => (open ? close(false) : openPopover())}\n          className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-ns-muted transition-colors hover:bg-foreground/[0.06] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ns-accent disabled:cursor-not-allowed\"\n        >\n          <canvas\n            ref={iconCanvasRef}\n            width={ICON}\n            height={ICON}\n            aria-hidden\n            className=\"pointer-events-none\"\n          />\n        </button>\n      </div>\n\n      <p id={helperId} className=\"mt-1.5 font-mono text-xs text-ns-muted\">\n        {invalid\n          ? \"Not a valid date\"\n          : `${previewName} · ${Math.round(illumination(previewF) * 100)}% illuminated`}\n      </p>\n\n      {open && (\n        <div\n          ref={popRef}\n          id={popId}\n          role=\"dialog\"\n          aria-label=\"Choose date\"\n          onKeyDown={(e) => {\n            if (e.key === \"Escape\") {\n              e.preventDefault();\n              close(true);\n            }\n          }}\n          className=\"absolute left-0 top-full z-30 mt-2 w-[292px] rounded-md border border-border bg-surface p-3 shadow-[0_12px_32px_-8px_rgba(0,0,0,0.35)]\"\n        >\n          <div className=\"mb-2 flex items-center justify-between\">\n            <button\n              type=\"button\"\n              aria-label=\"Previous month\"\n              onClick={() => moveFocus(addMonths(focusedDate, -1))}\n              className=\"flex h-7 w-7 items-center justify-center rounded-sm text-ns-muted transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ns-accent\"\n            >\n              <svg\n                width=\"14\"\n                height=\"14\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                aria-hidden\n              >\n                <path d=\"m15 18-6-6 6-6\" />\n              </svg>\n            </button>\n            <p\n              aria-live=\"polite\"\n              className=\"text-[13px] font-semibold text-foreground\"\n            >\n              {monthLabel}\n            </p>\n            <button\n              type=\"button\"\n              aria-label=\"Next month\"\n              onClick={() => moveFocus(addMonths(focusedDate, 1))}\n              className=\"flex h-7 w-7 items-center justify-center rounded-sm text-ns-muted transition-colors hover:bg-foreground/[0.06] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ns-accent\"\n            >\n              <svg\n                width=\"14\"\n                height=\"14\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth=\"2\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                aria-hidden\n              >\n                <path d=\"m9 18 6-6-6-6\" />\n              </svg>\n            </button>\n          </div>\n\n          <div ref={gridWrapRef} className=\"relative\">\n            {/* one canvas behind the whole grid — all 42 moons + transit */}\n            <canvas\n              ref={gridCanvasRef}\n              aria-hidden\n              className=\"pointer-events-none absolute left-0 top-0\"\n            />\n            <div\n              role=\"grid\"\n              aria-label={monthLabel}\n              onKeyDown={onGridKeyDown}\n              className=\"relative z-[1]\"\n            >\n              <div role=\"row\" className=\"grid grid-cols-7\">\n                {WEEKDAYS.map(([abbr, full]) => (\n                  <div\n                    key={abbr}\n                    role=\"columnheader\"\n                    aria-label={full}\n                    className=\"pb-1 text-center font-mono text-[10px] uppercase tracking-wider text-ns-muted\"\n                  >\n                    {abbr}\n                  </div>\n                ))}\n              </div>\n              {Array.from({ length: 6 }, (_, r) => (\n                <div key={r} role=\"row\" className=\"grid grid-cols-7\">\n                  {cells.slice(r * 7, r * 7 + 7).map((date, c) => {\n                    const i = r * 7 + c;\n                    const inMonth = date.getMonth() === view.m;\n                    const isSel = selected !== null && sameDay(date, selected);\n                    const isToday = sameDay(date, today);\n                    const f = phaseFraction(date);\n                    return (\n                      <button\n                        key={i}\n                        ref={(el) => {\n                          cellRefs.current[i] = el;\n                        }}\n                        type=\"button\"\n                        role=\"gridcell\"\n                        tabIndex={i === tabbableIdx ? 0 : -1}\n                        aria-selected={isSel}\n                        aria-current={isToday ? \"date\" : undefined}\n                        aria-label={`${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}. ${PHASE_NAMES[phaseIndex(f)]}.`}\n                        onClick={() => commit(date, i)}\n                        onFocus={() => setFocusedDate(date)}\n                        className={`relative flex h-11 flex-col items-center rounded-sm pt-[7px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ns-accent ${\n                          isSel\n                            ? \"bg-ns-accent/10 text-ns-accent\"\n                            : inMonth\n                              ? \"text-foreground hover:bg-foreground/[0.06]\"\n                              : \"text-ns-muted/70 hover:bg-foreground/[0.04]\"\n                        }`}\n                      >\n                        <span\n                          className={`text-[13px] leading-none ${\n                            isToday && !isSel\n                              ? \"underline decoration-ns-muted underline-offset-[3px]\"\n                              : \"\"\n                          }`}\n                        >\n                          {date.getDate()}\n                        </span>\n                      </button>\n                    );\n                  })}\n                </div>\n              ))}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/date-picker-moon.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "date-picker",
      "input",
      "calendar",
      "canvas",
      "form",
      "moon",
      "keyboard",
      "micro-interaction"
    ],
    "instruction": "Build a date input whose popover calendar doubles as a moon-phase almanac. RENDER: masked text input (MM/DD/YYYY, digits auto-slashed, full validation with revert-on-blur) plus a popover role=grid month calendar of 42 real button cells (role=gridcell, roving tabindex); ONE canvas absolutely positioned behind the whole grid draws all 42 moon glyphs (14px discs) at cell centers measured via getBoundingClientRect after a double-rAF layout settle, plus a 20px canvas in the input's trailing icon slot; both canvases get EXPLICIT style.width/height and dpr-clamped backing stores (a canvas is a replaced element — inset never sizes it). PHASE MATH: mean synodic month 29.530588853 d, reference new moon epoch 2000-01-06 18:14 UTC, sampled at local noon; f = ((days since epoch) mod syn)/syn; illuminated fraction k = (1 - cos(2*pi*f))/2; disc stroked --border, lit region --foreground at 85% alpha, terminator drawn as a semi-ellipse with x-radius r*|cos(2*pi*f)|, lit limb right while waxing (f < 0.5) and left while waning, ellipse sweep direction flipping crescent vs gibbous. MOTION: trailing icon crossfades 150ms (dual-alpha draw, cleared per frame) whenever focus movement or typing changes the previewed date; committing from the grid fires an eclipse transit — a foreground corona behind the chosen numeral occluded by an opaque background-ink shadow disc sweeping left-to-right over 420ms ease-out-expo, clipped to the cell, with the entire grid canvas cleared and all 42 moons redrawn every frame (zero accumulation), then the popover closes and focus returns to the input; month changes re-measure and re-lay glyphs after cell layout settles (double rAF + ResizeObserver). rAF runs only during crossfade/transit and is parked offscreen (IntersectionObserver) and on document.hidden — the static almanac costs zero frames. INTERACTION: Arrows move a day/week, Home/End week edges, PageUp/Down month, Shift+PageUp/Down year (view follows focus), Enter/click commits, Esc or outside pointerdown closes (Esc returns focus), ArrowDown in the input opens; aria-selected and aria-current=date on cells; phase name (e.g. Waxing gibbous) in every cell's accessible label and as visible helper text with percent illumination under the field. REDUCED MOTION: no transit or crossfade, instant selection and close; static moons stay as information. TOKENS: all canvas ink parsed from getComputedStyle(--foreground/--border/--background) at mount and re-derived via MutationObserver on documentElement class; hover/focus affordances token-relative (foreground-alpha tints, accent focus rings). Guard zero-size grids before measuring, and tear down every rAF, timer, observer, and document listener on unmount."
  },
  "type": "registry:ui"
}