{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "gummy-calendar",
  "type": "registry:ui",
  "title": "Gummy Calendar",
  "description": "Locale-aware single-date Calendar grid with month navigation, min/max constraints, keyboard grid movement, RTL, and controlled state.",
  "registryDependencies": [
    "https://gummyui.dev/r/gummy-primitives-styles.json"
  ],
  "files": [
    {
      "path": "components/ui/GummyCalendar.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nfunction joinClassNames(...values: Array<string | false | null | undefined>) {\n  return values.filter(Boolean).join(\" \");\n}\n\nfunction startOfMonth(date: Date) {\n  return new Date(date.getFullYear(), date.getMonth(), 1);\n}\n\nfunction addDays(date: Date, days: number) {\n  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);\n}\n\nfunction addMonths(date: Date, months: number) {\n  return new Date(date.getFullYear(), date.getMonth() + months, 1);\n}\n\nfunction sameDay(a?: Date, b?: Date) {\n  return Boolean(a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate());\n}\n\nfunction dateKey(date: Date) {\n  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, \"0\")}-${String(date.getDate()).padStart(2, \"0\")}`;\n}\n\nexport type GummyCalendarProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"onChange\" | \"defaultValue\"\n> & {\n  value?: Date;\n  defaultValue?: Date;\n  onValueChange?: (date: Date) => void;\n  month?: Date;\n  defaultMonth?: Date;\n  onMonthChange?: (month: Date) => void;\n  min?: Date;\n  max?: Date;\n  locale?: string;\n  weekStartsOn?: 0 | 1 | 6;\n  label?: string;\n};\n\nexport const GummyCalendar = React.forwardRef<HTMLDivElement, GummyCalendarProps>(\n  function GummyCalendar(\n    {\n      value,\n      defaultValue,\n      onValueChange,\n      month: controlledMonth,\n      defaultMonth,\n      onMonthChange,\n      min,\n      max,\n      locale = \"en-US\",\n      weekStartsOn = 1,\n      label = \"Choose a date\",\n      className,\n      ...props\n    },\n    ref,\n  ) {\n    const rootRef = React.useRef<HTMLDivElement | null>(null);\n    const setRootRef = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      },\n      [ref],\n    );\n    const today = React.useMemo(() => new Date(), []);\n    const [internalValue, setInternalValue] = React.useState(defaultValue);\n    const selected = value ?? internalValue;\n    const [internalMonth, setInternalMonth] = React.useState(() => startOfMonth(defaultMonth ?? selected ?? today));\n    const visibleMonth = startOfMonth(controlledMonth ?? internalMonth);\n    const monthName = new Intl.DateTimeFormat(locale, { month: \"long\", year: \"numeric\" }).format(visibleMonth);\n    const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: \"short\" });\n    const fullDateFormatter = new Intl.DateTimeFormat(locale, { dateStyle: \"full\" });\n    const firstWeekday = visibleMonth.getDay();\n    const offset = (firstWeekday - weekStartsOn + 7) % 7;\n    const gridStart = addDays(visibleMonth, -offset);\n    const days = Array.from({ length: 42 }, (_, index) => addDays(gridStart, index));\n    const weekdays = Array.from({ length: 7 }, (_, index) => addDays(new Date(2026, 5, 7 + weekStartsOn), index));\n\n    function changeMonth(next: Date) {\n      if (controlledMonth === undefined) setInternalMonth(startOfMonth(next));\n      onMonthChange?.(startOfMonth(next));\n    }\n\n    function choose(day: Date) {\n      if (value === undefined) setInternalValue(day);\n      onValueChange?.(day);\n      if (day.getMonth() !== visibleMonth.getMonth()) changeMonth(day);\n    }\n\n    function isDisabled(day: Date) {\n      const key = dateKey(day);\n      return Boolean((min && key < dateKey(min)) || (max && key > dateKey(max)));\n    }\n\n    function onDayKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, day: Date) {\n      const direction = event.currentTarget.closest(\"[dir='rtl']\") ? -1 : 1;\n      const moves: Record<string, number> = {\n        ArrowLeft: -direction,\n        ArrowRight: direction,\n        ArrowUp: -7,\n        ArrowDown: 7,\n      };\n      let next: Date | undefined;\n      if (event.key in moves) next = addDays(day, moves[event.key]);\n      else if (event.key === \"Home\") next = addDays(day, -((day.getDay() - weekStartsOn + 7) % 7));\n      else if (event.key === \"End\") next = addDays(day, 6 - ((day.getDay() - weekStartsOn + 7) % 7));\n      else if (event.key === \"PageUp\") next = addMonths(day, event.shiftKey ? -12 : -1);\n      else if (event.key === \"PageDown\") next = addMonths(day, event.shiftKey ? 12 : 1);\n      if (!next) return;\n      event.preventDefault();\n      changeMonth(next);\n      const focusNextDay = () => {\n        const target = rootRef.current?.querySelector<HTMLButtonElement>(\n          `[data-calendar-date=\"${dateKey(next!)}\"]`,\n        );\n        target?.focus();\n        return Boolean(target);\n      };\n      if (!focusNextDay()) requestAnimationFrame(focusNextDay);\n    }\n\n    return (\n      <div {...props} ref={setRootRef} className={joinClassNames(\"gummy-calendar\", className)} role=\"group\" aria-label={label}>\n        <div className=\"gummy-calendar__header\">\n          <button type=\"button\" onClick={() => changeMonth(addMonths(visibleMonth, -1))} aria-label=\"Previous month\"><span aria-hidden=\"true\">←</span></button>\n          <strong aria-live=\"polite\">{monthName}</strong>\n          <button type=\"button\" onClick={() => changeMonth(addMonths(visibleMonth, 1))} aria-label=\"Next month\"><span aria-hidden=\"true\">→</span></button>\n        </div>\n        <div className=\"gummy-calendar__grid\" role=\"grid\" aria-label={monthName}>\n          <div role=\"row\" className=\"gummy-calendar__weekdays\">\n            {weekdays.map((day) => <span role=\"columnheader\" key={day.getDay()} aria-label={weekdayFormatter.format(day)}>{weekdayFormatter.format(day).slice(0, 2)}</span>)}\n          </div>\n          {Array.from({ length: 6 }, (_, week) => (\n            <div role=\"row\" key={week}>\n              {days.slice(week * 7, week * 7 + 7).map((day) => {\n                const chosen = sameDay(day, selected);\n                const current = sameDay(day, today);\n                return (\n                  <button\n                    type=\"button\"\n                    role=\"gridcell\"\n                    key={dateKey(day)}\n                    data-calendar-date={dateKey(day)}\n                    data-outside={day.getMonth() !== visibleMonth.getMonth() || undefined}\n                    data-today={current || undefined}\n                    aria-selected={chosen}\n                    aria-label={fullDateFormatter.format(day)}\n                    disabled={isDisabled(day)}\n                    tabIndex={chosen || (!selected && current) || (!selected && sameDay(day, visibleMonth)) ? 0 : -1}\n                    onClick={() => choose(day)}\n                    onKeyDown={(event) => onDayKeyDown(event, day)}\n                  >\n                    {day.getDate()}\n                  </button>\n                );\n              })}\n            </div>\n          ))}\n        </div>\n      </div>\n    );\n  },\n);\n\nGummyCalendar.displayName = \"GummyCalendar\";\n"
    }
  ]
}
