{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "calendar-crest",
  "type": "registry:ui",
  "title": "Calendar Crest",
  "description": "Dual-month range picker with physical depth — selected band rises off the surface, endpoints crest highest, proportional shadows.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/calendar-crest.tsx",
      "content": "\"use client\";\n\nimport { useRef, useState, useCallback } from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Calendar Crest — dual-month range picker with physical depth.\n *\n * Two months sit side by side. Click a day to start a range,\n * hover to preview, click again to confirm. The selected band\n * rises off the surface — endpoints crest highest, the range\n * forms a ridge with proportional shadows. Confirmed ranges\n * stand taller than previews.\n *\n * The ridge IS the selection.\n */\n\n/* ── Types ── */\n\nexport interface CalendarCrestProps {\n  defaultStart?: string;\n  defaultEnd?: string;\n  onRangeChange?: (start: string | null, end: string | null) => void;\n  sound?: boolean;\n}\n\n/* ── Constants ── */\n\nconst CELL = 36;\nconst DOW = [\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"];\n\n/* ── Helpers ── */\n\nfunction pad2(n: number): string {\n  return String(n).padStart(2, \"0\");\n}\n\nfunction toKey(y: number, m: number, d: number): string {\n  return `${y}-${pad2(m + 1)}-${pad2(d)}`;\n}\n\nfunction parseKey(key: string): [number, number, number] {\n  const [y, m, d] = key.split(\"-\").map(Number);\n  return [y, m - 1, d];\n}\n\nfunction formatDate(key: string): string {\n  const [y, m, d] = parseKey(key);\n  return new Date(y, m, d).toLocaleDateString(\"en-US\", {\n    month: \"short\",\n    day: \"numeric\",\n  });\n}\n\nfunction daysBetween(a: string, b: string): number {\n  const [ay, am, ad] = parseKey(a);\n  const [by, bm, bd] = parseKey(b);\n  const da = new Date(ay, am, ad);\n  const db = new Date(by, bm, bd);\n  return Math.round((db.getTime() - da.getTime()) / 86400000) + 1;\n}\n\nfunction ordered(a: string, b: string): [string, string] {\n  return a <= b ? [a, b] : [b, a];\n}\n\n/* ── Audio ── */\n\nlet _ctx: AudioContext | null = null;\nlet _buf: AudioBuffer | null = null;\n\nfunction audioCtx() {\n  if (!_ctx) {\n    _ctx = new (window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext })\n        .webkitAudioContext)();\n  }\n  if (_ctx.state === \"suspended\") _ctx.resume();\n  return _ctx;\n}\n\nfunction ensureBuf(ac: AudioContext): AudioBuffer {\n  if (_buf && _buf.sampleRate === ac.sampleRate) return _buf;\n  const rate = ac.sampleRate;\n  const len = Math.floor(rate * 0.003);\n  const buf = ac.createBuffer(1, len, rate);\n  const ch = buf.getChannelData(0);\n  for (let i = 0; i < len; i++) {\n    const t = i / len;\n    ch[i] = (Math.random() * 2 - 1) * (1 - t) ** 4;\n  }\n  _buf = buf;\n  return buf;\n}\n\nfunction playTick(last: React.MutableRefObject<number>) {\n  const now = performance.now();\n  if (now - last.current < 80) return;\n  last.current = now;\n  try {\n    const ac = audioCtx();\n    const buf = ensureBuf(ac);\n    const src = ac.createBufferSource();\n    const gain = ac.createGain();\n    src.buffer = buf;\n    src.playbackRate.value = 1.15;\n    gain.gain.value = 0.03;\n    src.connect(gain);\n    gain.connect(ac.destination);\n    src.start();\n  } catch {\n    /* silent */\n  }\n}\n\n/* ── Component ── */\n\nexport function CalendarCrest({\n  defaultStart,\n  defaultEnd,\n  onRangeChange,\n  sound = true,\n}: CalendarCrestProps) {\n  const [baseMonth, setBaseMonth] = useState(() => new Date().getMonth());\n  const [baseYear, setBaseYear] = useState(() => new Date().getFullYear());\n  const [rangeStart, setRangeStart] = useState<string | null>(\n    defaultStart ?? null,\n  );\n  const [rangeEnd, setRangeEnd] = useState<string | null>(defaultEnd ?? null);\n  const [hoverDate, setHoverDate] = useState<string | null>(null);\n  const [direction, setDirection] = useState(1);\n  const lastSound = useRef(0);\n\n  function tick() {\n    if (sound) playTick(lastSound);\n  }\n\n  /* ── Effective range (includes hover preview) ── */\n\n  const isConfirmed = rangeStart !== null && rangeEnd !== null;\n  let effStart: string | null = null;\n  let effEnd: string | null = null;\n\n  if (rangeStart) {\n    if (rangeEnd) {\n      [effStart, effEnd] = ordered(rangeStart, rangeEnd);\n    } else if (hoverDate) {\n      [effStart, effEnd] = ordered(rangeStart, hoverDate);\n    } else {\n      effStart = rangeStart;\n    }\n  }\n\n  /* ── Today ── */\n\n  const now = new Date();\n  const todayKey = toKey(now.getFullYear(), now.getMonth(), now.getDate());\n\n  /* ── Day click ── */\n\n  const handleDayClick = useCallback(\n    (dateKey: string) => {\n      tick();\n      if (rangeStart === null || rangeEnd !== null) {\n        setRangeStart(dateKey);\n        setRangeEnd(null);\n        onRangeChange?.(dateKey, null);\n      } else {\n        const [s, e] = ordered(rangeStart, dateKey);\n        setRangeStart(s);\n        setRangeEnd(e);\n        onRangeChange?.(s, e);\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [rangeStart, rangeEnd, onRangeChange],\n  );\n\n  /* ── Navigation ── */\n\n  function goMonth(delta: number) {\n    tick();\n    setDirection(delta);\n    let m = baseMonth + delta;\n    let y = baseYear;\n    if (m < 0) {\n      m = 11;\n      y--;\n    } else if (m > 11) {\n      m = 0;\n      y++;\n    }\n    setBaseMonth(m);\n    setBaseYear(y);\n  }\n\n  /* ── Second month ── */\n\n  let month2 = baseMonth + 1;\n  let year2 = baseYear;\n  if (month2 > 11) {\n    month2 = 0;\n    year2++;\n  }\n\n  /* ── Render a single month grid ── */\n\n  function renderMonth(y: number, m: number) {\n    const daysInMonth = new Date(y, m + 1, 0).getDate();\n    const firstOffset = (new Date(y, m, 1).getDay() + 6) % 7;\n    const monthLabel = new Date(y, m).toLocaleDateString(\"en-US\", {\n      month: \"long\",\n    });\n\n    return (\n      <div>\n        {/* Month name */}\n        <div\n          className=\"text-neutral-700 dark:text-neutral-300\"\n          style={{\n            fontSize: 13,\n            fontWeight: 590,\n            textAlign: \"center\",\n            marginBottom: 10,\n            letterSpacing: \"-0.01em\",\n          }}\n        >\n          {monthLabel}\n        </div>\n\n        {/* DOW headers */}\n        <div\n          style={{\n            display: \"grid\",\n            gridTemplateColumns: `repeat(7, ${CELL}px)`,\n            marginBottom: 4,\n          }}\n        >\n          {DOW.map((d) => (\n            <div\n              key={d}\n              className=\"text-neutral-300 dark:text-neutral-700\"\n              style={{\n                fontSize: 10,\n                fontWeight: 500,\n                textAlign: \"center\",\n                textTransform: \"uppercase\",\n                letterSpacing: \"0.06em\",\n              }}\n            >\n              {d}\n            </div>\n          ))}\n        </div>\n\n        {/* Day grid — no gap for continuous band */}\n        <div\n          style={{\n            display: \"grid\",\n            gridTemplateColumns: `repeat(7, ${CELL}px)`,\n          }}\n        >\n          {/* Leading empties */}\n          {Array.from({ length: firstOffset }).map((_, i) => (\n            <div key={`e-${i}`} style={{ width: CELL, height: CELL }} />\n          ))}\n\n          {/* Days */}\n          {Array.from({ length: daysInMonth }).map((_, i) => {\n            const d = i + 1;\n            const dateKey = toKey(y, m, d);\n            const col = (firstOffset + d - 1) % 7;\n            const isToday = dateKey === todayKey;\n\n            /* Range logic */\n            const inRange =\n              effStart !== null &&\n              effEnd !== null &&\n              dateKey >= effStart &&\n              dateKey <= effEnd;\n            const isStart = dateKey === effStart;\n            const isEnd = dateKey === effEnd;\n            const isSingle = isStart && isEnd;\n\n            /* Neighbor checks for band rounding */\n            const leftEmpty = col === 0 || d === 1;\n            const rightEmpty = col === 6 || d === daysInMonth;\n            const prevInRange =\n              !leftEmpty &&\n              effStart !== null &&\n              effEnd !== null &&\n              toKey(y, m, d - 1) >= effStart &&\n              toKey(y, m, d - 1) <= effEnd;\n            const nextInRange =\n              !rightEmpty &&\n              effStart !== null &&\n              effEnd !== null &&\n              toKey(y, m, d + 1) >= effStart &&\n              toKey(y, m, d + 1) <= effEnd;\n\n            const roundLeft = inRange && !prevInRange;\n            const roundRight = inRange && !nextInRange;\n\n            /* Radius */\n            const R = \"10px\";\n            const Z = \"0\";\n            const radius = isSingle\n              ? R\n              : `${roundLeft ? R : Z} ${roundRight ? R : Z} ${roundRight ? R : Z} ${roundLeft ? R : Z}`;\n\n            /* ── Elevation ── depth via shadow + scale, no Y displacement */\n            let elevation = 0;\n            if (isStart || isEnd) {\n              elevation = isConfirmed ? 6 : 4;\n            } else if (inRange) {\n              elevation = isConfirmed ? 3 : 1.5;\n            }\n\n            const isHov = dateKey === hoverDate && !inRange;\n            if (isHov) elevation = 1.5;\n\n            /* Shadow — proportional to elevation (deeper to sell the crest) */\n            const shadow =\n              elevation > 0.5\n                ? `0 ${Math.round(elevation * 1.5)}px ${Math.round(elevation * 5)}px rgba(0,0,0,${(0.04 + elevation * 0.012).toFixed(3)})`\n                : \"none\";\n\n            /* Text color classes */\n            const textCls =\n              isStart || isEnd\n                ? \"text-white dark:text-neutral-950\"\n                : inRange\n                  ? \"text-neutral-700 dark:text-neutral-300\"\n                  : isToday\n                    ? \"text-neutral-900 dark:text-neutral-100\"\n                    : \"text-neutral-400 dark:text-neutral-500\";\n\n            /* Background classes */\n            const bgCls = inRange\n              ? isStart || isEnd\n                ? \"bg-neutral-900 dark:bg-neutral-100\"\n                : \"bg-neutral-100 dark:bg-neutral-800\"\n              : \"\";\n\n            return (\n              <motion.button\n                key={d}\n                onClick={() => handleDayClick(dateKey)}\n                onMouseEnter={() => setHoverDate(dateKey)}\n                onMouseLeave={() => setHoverDate(null)}\n                animate={{\n                  scale: 1 + elevation * 0.012,\n                }}\n                whileTap={{ scale: 0.9 }}\n                transition={{\n                  type: \"spring\",\n                  damping: 22,\n                  stiffness: 320,\n                }}\n                className={cn(\n                  \"border-none transition-colors duration-100\",\n                  bgCls,\n                  !inRange &&\n                    \"rounded-[10px] hover:bg-neutral-100 dark:hover:bg-neutral-800\",\n                )}\n                style={{\n                  width: CELL,\n                  height: CELL,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                  cursor: \"pointer\",\n                  padding: 0,\n                  position: \"relative\",\n                  borderRadius: inRange ? radius : undefined,\n                  boxShadow: shadow,\n                }}\n              >\n                <span\n                  className={cn(\n                    \"relative z-[1] text-[13px] tabular-nums transition-colors duration-100\",\n                    isHov ? \"text-neutral-600 dark:text-neutral-400\" : textCls,\n                  )}\n                  style={{\n                    fontWeight:\n                      isStart || isEnd || isToday ? 650 : inRange ? 500 : 400,\n                    lineHeight: 1,\n                  }}\n                >\n                  {d}\n                </span>\n              </motion.button>\n            );\n          })}\n        </div>\n      </div>\n    );\n  }\n\n  /* ── Range label ── */\n\n  const rangeLabel = (() => {\n    if (effStart && effEnd && effStart !== effEnd) {\n      const count = daysBetween(effStart, effEnd);\n      return `${formatDate(effStart)} – ${formatDate(effEnd)}  ·  ${count} day${count !== 1 ? \"s\" : \"\"}`;\n    }\n    if (effStart) {\n      return isConfirmed\n        ? formatDate(effStart)\n        : `${formatDate(effStart)} — select end`;\n    }\n    return null;\n  })();\n\n  return (\n    <div className=\"w-fit overflow-hidden rounded-[20px] border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950\">\n      {/* Header */}\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          alignItems: \"center\",\n          padding: \"22px 24px 14px\",\n        }}\n      >\n        <motion.button\n          whileTap={{ scale: 0.85 }}\n          onClick={() => goMonth(-1)}\n          className=\"text-neutral-400 transition-colors duration-150 hover:bg-neutral-100 hover:text-neutral-600 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400\"\n          style={{\n            background: \"transparent\",\n            border: \"none\",\n            cursor: \"pointer\",\n            fontSize: 16,\n            lineHeight: 1,\n            padding: \"6px 10px\",\n            borderRadius: 8,\n          }}\n        >\n          ‹\n        </motion.button>\n\n        <span\n          className=\"text-neutral-900 dark:text-neutral-100\"\n          style={{\n            fontSize: 15,\n            fontWeight: 590,\n            letterSpacing: \"-0.01em\",\n          }}\n        >\n          {baseYear}\n        </span>\n\n        <motion.button\n          whileTap={{ scale: 0.85 }}\n          onClick={() => goMonth(1)}\n          className=\"text-neutral-400 transition-colors duration-150 hover:bg-neutral-100 hover:text-neutral-600 dark:text-neutral-600 dark:hover:bg-neutral-800 dark:hover:text-neutral-400\"\n          style={{\n            background: \"transparent\",\n            border: \"none\",\n            cursor: \"pointer\",\n            fontSize: 16,\n            lineHeight: 1,\n            padding: \"6px 10px\",\n            borderRadius: 8,\n          }}\n        >\n          ›\n        </motion.button>\n      </div>\n\n      {/* Twin grids */}\n      <div style={{ padding: \"0 24px 16px\" }}>\n        <AnimatePresence mode=\"wait\" initial={false}>\n          <motion.div\n            key={`${baseYear}-${baseMonth}`}\n            initial={{ opacity: 0, x: direction > 0 ? 12 : -12 }}\n            animate={{ opacity: 1, x: 0 }}\n            exit={{ opacity: 0, x: direction > 0 ? -12 : 12 }}\n            transition={{ duration: 0.18, ease: \"easeOut\" }}\n            style={{\n              display: \"flex\",\n              gap: 24,\n            }}\n          >\n            {renderMonth(baseYear, baseMonth)}\n            {renderMonth(year2, month2)}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n\n      {/* Range info */}\n      <AnimatePresence>\n        {rangeLabel && (\n          <motion.div\n            initial={{ opacity: 0, height: 0 }}\n            animate={{ opacity: 1, height: \"auto\" }}\n            exit={{ opacity: 0, height: 0 }}\n            transition={{ type: \"spring\", damping: 25, stiffness: 300 }}\n            style={{ overflow: \"hidden\" }}\n          >\n            <div\n              className={cn(\n                \"border-t border-neutral-100 dark:border-neutral-800/50\",\n                isConfirmed\n                  ? \"text-neutral-500 dark:text-neutral-500\"\n                  : \"text-neutral-400 dark:text-neutral-600\",\n              )}\n              style={{\n                padding: \"12px 24px 14px\",\n                fontSize: 12,\n                fontWeight: 450,\n                textAlign: \"center\",\n                fontVariantNumeric: \"tabular-nums\",\n                letterSpacing: \"-0.005em\",\n              }}\n            >\n              {rangeLabel}\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport default CalendarCrest;\n",
      "type": "registry:ui",
      "target": "components/ruixen/calendar-crest.tsx"
    }
  ]
}