{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-month-grid",
  "title": "Use Month Grid",
  "type": "registry:hook",
  "files": [
    {
      "path": "packages/registry-react/bootstrap/use-month-grid/useMonthGrid.ts",
      "content": "'use client'\n\n// Headless month-grid + drag/shift range-select primitives.\n// Domain-agnostic: knows nothing about events -- just dates, the 42-cell grid,\n// and a normalized [start, end] range driven by mouse / shift-click. Pair with\n// EventCalendar (or any month-shaped UI) by reading the returned values.\n\nimport * as React from 'react'\n\nexport type DateKey = string // YYYY-MM-DD\n\nexport interface UseMonthGridOptions {\n  initialDate?: Date | DateKey\n  weekStartsOn?: 0 | 1 // 0 = Sunday (default), 1 = Monday\n}\n\nexport function isoDate(d: Date): DateKey {\n  const y = d.getFullYear()\n  const m = String(d.getMonth() + 1).padStart(2, '0')\n  const day = String(d.getDate()).padStart(2, '0')\n  return `${y}-${m}-${day}`\n}\n\nexport function dateFromKey(k: DateKey): Date {\n  return new Date(k + 'T00:00:00')\n}\n\nexport function dayDiff(a: DateKey, b: DateKey): number {\n  return Math.round((dateFromKey(b).getTime() - dateFromKey(a).getTime()) / 86400000)\n}\n\nexport function useMonthGrid(options: UseMonthGridOptions = {}) {\n  const weekStartsOn = options.weekStartsOn ?? 0\n\n  // \"today\" is frozen to the moment the hook is first created -- predictable\n  // for tests / SSR. Same for the initial cursor month.\n  const { today, todayKey, initialCursor } = React.useMemo(() => {\n    const init = options.initialDate\n      ? typeof options.initialDate === 'string'\n        ? dateFromKey(options.initialDate)\n        : options.initialDate\n      : new Date()\n    const t = new Date(init.getFullYear(), init.getMonth(), init.getDate())\n    return {\n      today: t,\n      todayKey: isoDate(t),\n      initialCursor: new Date(init.getFullYear(), init.getMonth(), 1),\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  const [cursor, setCursor] = React.useState<Date>(initialCursor)\n\n  const [rangeAnchor, setRangeAnchor] = React.useState<DateKey>(todayKey)\n  const [rangeStart, setRangeStart] = React.useState<DateKey>(todayKey)\n  const [rangeEnd, setRangeEnd] = React.useState<DateKey>(todayKey)\n  const [isDragging, setIsDragging] = React.useState(false)\n\n  // Mirror into refs so the window mouseup/mouseleave listeners and the\n  // anchor read inside onCellMouseEnter always see the latest values.\n  const isDraggingRef = React.useRef(false)\n  isDraggingRef.current = isDragging\n  const rangeAnchorRef = React.useRef(rangeAnchor)\n  rangeAnchorRef.current = rangeAnchor\n\n  const monthLabel = React.useMemo(\n    () => cursor.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }),\n    [cursor],\n  )\n\n  const gridDays = React.useMemo(() => {\n    const first = new Date(cursor.getFullYear(), cursor.getMonth(), 1)\n    const offset = (first.getDay() - weekStartsOn + 7) % 7\n    const start = new Date(first)\n    start.setDate(first.getDate() - offset)\n    const days: { date: Date; key: DateKey; inMonth: boolean }[] = []\n    for (let i = 0; i < 42; i++) {\n      const d = new Date(start)\n      d.setDate(start.getDate() + i)\n      days.push({\n        date: d,\n        key: isoDate(d),\n        inMonth: d.getMonth() === cursor.getMonth(),\n      })\n    }\n    return days\n  }, [cursor, weekStartsOn])\n\n  const weekdays = React.useMemo(() => {\n    const base = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n    return [...base.slice(weekStartsOn), ...base.slice(0, weekStartsOn)]\n  }, [weekStartsOn])\n\n  const rangeBounds = React.useMemo(() => {\n    const a = rangeStart\n    const b = rangeEnd\n    return a <= b ? { lo: a, hi: b } : { lo: b, hi: a }\n  }, [rangeStart, rangeEnd])\n\n  const rangeDayCount = React.useMemo(() => dayDiff(rangeBounds.lo, rangeBounds.hi) + 1, [rangeBounds])\n  const isRange = React.useMemo(() => rangeBounds.lo !== rangeBounds.hi, [rangeBounds])\n\n  const inRange = React.useCallback(\n    (key: DateKey) => {\n      const { lo, hi } = rangeBounds\n      return key >= lo && key <= hi\n    },\n    [rangeBounds],\n  )\n\n  const prevMonth = React.useCallback(() => {\n    setCursor((c) => new Date(c.getFullYear(), c.getMonth() - 1, 1))\n  }, [])\n  const nextMonth = React.useCallback(() => {\n    setCursor((c) => new Date(c.getFullYear(), c.getMonth() + 1, 1))\n  }, [])\n  const goToToday = React.useCallback(() => {\n    setCursor(new Date(today.getFullYear(), today.getMonth(), 1))\n    setRangeAnchor(todayKey)\n    setRangeStart(todayKey)\n    setRangeEnd(todayKey)\n  }, [today, todayKey])\n  const selectDay = React.useCallback((key: DateKey) => {\n    setRangeAnchor(key)\n    setRangeStart(key)\n    setRangeEnd(key)\n  }, [])\n  const selectWeekOf = React.useCallback(\n    (key: DateKey) => {\n      const d = dateFromKey(key)\n      const dow = (d.getDay() - weekStartsOn + 7) % 7\n      const wkStart = new Date(d)\n      wkStart.setDate(d.getDate() - dow)\n      const wkEnd = new Date(wkStart)\n      wkEnd.setDate(wkStart.getDate() + 6)\n      setRangeAnchor(isoDate(wkStart))\n      setRangeStart(isoDate(wkStart))\n      setRangeEnd(isoDate(wkEnd))\n    },\n    [weekStartsOn],\n  )\n  const clearRange = React.useCallback(() => {\n    setRangeAnchor(todayKey)\n    setRangeStart(todayKey)\n    setRangeEnd(todayKey)\n  }, [todayKey])\n\n  const onCellMouseDown = React.useCallback((key: DateKey, ev: React.MouseEvent) => {\n    if (ev.button !== 0) return // ignore right-click -- context menus handle it\n    if (ev.shiftKey) {\n      setRangeEnd(key)\n      return\n    }\n    setRangeAnchor(key)\n    setRangeStart(key)\n    setRangeEnd(key)\n    setIsDragging(true)\n  }, [])\n  const onCellMouseEnter = React.useCallback((key: DateKey) => {\n    if (!isDraggingRef.current) return\n    setRangeEnd(key)\n    setRangeStart(rangeAnchorRef.current)\n  }, [])\n  const endDrag = React.useCallback(() => {\n    setIsDragging((cur) => (cur ? false : cur))\n  }, [])\n\n  // Window listeners are needed because mouseup can fire outside any cell.\n  // useEffect only runs on the client, so this is SSR-safe.\n  React.useEffect(() => {\n    window.addEventListener('mouseup', endDrag)\n    window.addEventListener('mouseleave', endDrag)\n    return () => {\n      window.removeEventListener('mouseup', endDrag)\n      window.removeEventListener('mouseleave', endDrag)\n    }\n  }, [endDrag])\n\n  return {\n    today,\n    todayKey,\n    cursor,\n    monthLabel,\n    gridDays,\n    weekdays,\n    rangeAnchor,\n    rangeStart,\n    rangeEnd,\n    rangeBounds,\n    rangeDayCount,\n    isRange,\n    isDragging,\n    inRange,\n    prevMonth,\n    nextMonth,\n    goToToday,\n    selectDay,\n    selectWeekOf,\n    clearRange,\n    onCellMouseDown,\n    onCellMouseEnter,\n    endDrag,\n  }\n}\n",
      "type": "registry:hook",
      "target": "~/lib/use-month-grid.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Headless month-grid + drag/shift range-select hook. Returns the 42-cell grid, today/cursor state, range state, and mouse handlers. Domain-agnostic -- pair with EventCalendar or any month-shaped UI by reading the returned values. Also exports isoDate / dateFromKey / dayDiff helpers."
}