{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"calendar","type":"registry:ui","title":"Calendar","description":"Calendar from react-fancy","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":[],"files":[{"path":"components\/fancy\/calendar\/Calendar.tsx","content":"import { forwardRef, useCallback, useMemo, useState } from \"react\";\r\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport type { CalendarProps, DateRange } from \".\/Calendar.types\";\r\n\r\nconst DAYS = [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"];\r\nconst MONTHS = [\r\n  \"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\r\n  \"July\", \"August\", \"September\", \"October\", \"November\", \"December\",\r\n];\r\n\r\nfunction isSameDay(a: Date, b: Date): boolean {\r\n  return (\r\n    a.getFullYear() === b.getFullYear() &&\r\n    a.getMonth() === b.getMonth() &&\r\n    a.getDate() === b.getDate()\r\n  );\r\n}\r\n\r\nfunction getDaysInMonth(year: number, month: number): Date[] {\r\n  const days: Date[] = [];\r\n  const firstDay = new Date(year, month, 1);\r\n  const startDayOfWeek = firstDay.getDay();\r\n\r\n  \/\/ Fill leading days from previous month\r\n  for (let i = startDayOfWeek - 1; i >= 0; i--) {\r\n    days.push(new Date(year, month, -i));\r\n  }\r\n\r\n  \/\/ Fill current month\r\n  const lastDate = new Date(year, month + 1, 0).getDate();\r\n  for (let d = 1; d <= lastDate; d++) {\r\n    days.push(new Date(year, month, d));\r\n  }\r\n\r\n  \/\/ Fill trailing days to complete 6 rows\r\n  while (days.length < 42) {\r\n    days.push(new Date(year, month + 1, days.length - startDayOfWeek - lastDate + 1));\r\n  }\r\n\r\n  return days;\r\n}\r\n\r\nexport const Calendar = forwardRef<HTMLDivElement, CalendarProps>(\r\n  function Calendar(\r\n    {\r\n      mode = \"single\",\r\n      value,\r\n      onChange,\r\n      minDate,\r\n      maxDate,\r\n      disabledDates = [],\r\n      className,\r\n    },\r\n    ref,\r\n  ) {\r\n    const today = useMemo(() => new Date(), []);\r\n    const [viewMonth, setViewMonth] = useState(today.getMonth());\r\n    const [viewYear, setViewYear] = useState(today.getFullYear());\r\n    const [rangeHover, setRangeHover] = useState<Date | null>(null);\r\n\r\n    const days = useMemo(\r\n      () => getDaysInMonth(viewYear, viewMonth),\r\n      [viewYear, viewMonth],\r\n    );\r\n\r\n    const prevMonth = useCallback(() => {\r\n      setViewMonth((m) => {\r\n        if (m === 0) {\r\n          setViewYear((y) => y - 1);\r\n          return 11;\r\n        }\r\n        return m - 1;\r\n      });\r\n    }, []);\r\n\r\n    const nextMonth = useCallback(() => {\r\n      setViewMonth((m) => {\r\n        if (m === 11) {\r\n          setViewYear((y) => y + 1);\r\n          return 0;\r\n        }\r\n        return m + 1;\r\n      });\r\n    }, []);\r\n\r\n    const isDisabled = useCallback(\r\n      (date: Date): boolean => {\r\n        if (minDate && date < minDate) return true;\r\n        if (maxDate && date > maxDate) return true;\r\n        return disabledDates.some((d) => isSameDay(d, date));\r\n      },\r\n      [minDate, maxDate, disabledDates],\r\n    );\r\n\r\n    const isSelected = useCallback(\r\n      (date: Date): boolean => {\r\n        if (!value) return false;\r\n        if (mode === \"single\" && value instanceof Date) {\r\n          return isSameDay(date, value);\r\n        }\r\n        if (mode === \"multiple\" && Array.isArray(value)) {\r\n          return value.some((v) => isSameDay(date, v));\r\n        }\r\n        if (mode === \"range\" && value && typeof value === \"object\" && \"start\" in value) {\r\n          const range = value as DateRange;\r\n          if (range.start && isSameDay(date, range.start)) return true;\r\n          if (range.end && isSameDay(date, range.end)) return true;\r\n        }\r\n        return false;\r\n      },\r\n      [value, mode],\r\n    );\r\n\r\n    const isInRange = useCallback(\r\n      (date: Date): boolean => {\r\n        if (mode !== \"range\" || !value || typeof value !== \"object\" || !(\"start\" in value))\r\n          return false;\r\n        const range = value as DateRange;\r\n        if (!range.start) return false;\r\n        const end = range.end ?? rangeHover;\r\n        if (!end) return false;\r\n        const [s, e] = range.start <= end ? [range.start, end] : [end, range.start];\r\n        return date > s && date < e;\r\n      },\r\n      [value, mode, rangeHover],\r\n    );\r\n\r\n    const handleSelect = useCallback(\r\n      (date: Date) => {\r\n        if (isDisabled(date)) return;\r\n\r\n        if (mode === \"single\") {\r\n          onChange?.(date);\r\n        } else if (mode === \"multiple\") {\r\n          const arr = (Array.isArray(value) ? value : []) as Date[];\r\n          const exists = arr.findIndex((d) => isSameDay(d, date));\r\n          if (exists >= 0) {\r\n            onChange?.(arr.filter((_, i) => i !== exists));\r\n          } else {\r\n            onChange?.([...arr, date]);\r\n          }\r\n        } else if (mode === \"range\") {\r\n          const range = (value && typeof value === \"object\" && \"start\" in value\r\n            ? value\r\n            : { start: null, end: null }) as DateRange;\r\n\r\n          if (!range.start || range.end) {\r\n            onChange?.({ start: date, end: null });\r\n          } else {\r\n            const [s, e] =\r\n              date >= range.start ? [range.start, date] : [date, range.start];\r\n            onChange?.({ start: s, end: e });\r\n          }\r\n        }\r\n      },\r\n      [mode, value, onChange, isDisabled],\r\n    );\r\n\r\n    const btnBase =\r\n      \"flex h-8 w-8 items-center justify-center rounded-lg text-sm transition-colors\";\r\n\r\n    return (\r\n      <div\r\n        data-react-fancy-calendar=\"\"\r\n        ref={ref}\r\n        className={cn(\r\n          \"inline-block w-72 rounded-xl border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\",\r\n          className,\r\n        )}\r\n      >\r\n        {\/* Header *\/}\r\n        <div className=\"mb-3 flex items-center justify-between\">\r\n          <button\r\n            type=\"button\"\r\n            onClick={prevMonth}\r\n            className=\"rounded-lg p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800\"\r\n            aria-label=\"Previous month\"\r\n          >\r\n            <ChevronLeft size={16} \/>\r\n          <\/button>\r\n          <span className=\"text-sm font-semibold\">\r\n            {MONTHS[viewMonth]} {viewYear}\r\n          <\/span>\r\n          <button\r\n            type=\"button\"\r\n            onClick={nextMonth}\r\n            className=\"rounded-lg p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800\"\r\n            aria-label=\"Next month\"\r\n          >\r\n            <ChevronRight size={16} \/>\r\n          <\/button>\r\n        <\/div>\r\n\r\n        {\/* Day headers *\/}\r\n        <div className=\"mb-1 grid grid-cols-7 gap-1\">\r\n          {DAYS.map((d) => (\r\n            <div\r\n              key={d}\r\n              className=\"flex h-8 items-center justify-center text-xs font-medium text-zinc-400\"\r\n            >\r\n              {d}\r\n            <\/div>\r\n          ))}\r\n        <\/div>\r\n\r\n        {\/* Day grid *\/}\r\n        <div className=\"grid grid-cols-7 gap-1\">\r\n          {days.map((date, i) => {\r\n            const isCurrentMonth = date.getMonth() === viewMonth;\r\n            const selected = isSelected(date);\r\n            const inRange = isInRange(date);\r\n            const disabled = isDisabled(date);\r\n            const isToday = isSameDay(date, today);\r\n\r\n            return (\r\n              <button\r\n                key={i}\r\n                type=\"button\"\r\n                disabled={disabled}\r\n                onClick={() => handleSelect(date)}\r\n                onMouseEnter={() =>\r\n                  mode === \"range\" && setRangeHover(date)\r\n                }\r\n                className={cn(\r\n                  btnBase,\r\n                  !isCurrentMonth && \"text-zinc-300 dark:text-zinc-600\",\r\n                  isCurrentMonth &&\r\n                    !selected &&\r\n                    !inRange &&\r\n                    \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\",\r\n                  inRange &&\r\n                    \"bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300\",\r\n                  selected &&\r\n                    \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-900\",\r\n                  isToday && !selected && \"font-bold\",\r\n                  disabled && \"cursor-not-allowed opacity-30\",\r\n                )}\r\n              >\r\n                {date.getDate()}\r\n              <\/button>\r\n            );\r\n          })}\r\n        <\/div>\r\n      <\/div>\r\n    );\r\n  },\r\n);\r\n\r\nCalendar.displayName = \"Calendar\";\r\n","type":"registry:ui","target":"components\/fancy\/calendar\/Calendar.tsx"},{"path":"components\/fancy\/calendar\/Calendar.types.ts","content":"export type CalendarMode = \"single\" | \"range\" | \"multiple\";\r\n\r\nexport interface DateRange {\r\n  start: Date | null;\r\n  end: Date | null;\r\n}\r\n\r\nexport interface CalendarProps {\r\n  mode?: CalendarMode;\r\n  value?: Date | Date[] | DateRange | null;\r\n  onChange?: (value: Date | Date[] | DateRange | null) => void;\r\n  minDate?: Date;\r\n  maxDate?: Date;\r\n  disabledDates?: Date[];\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/calendar\/Calendar.types.ts"},{"path":"components\/fancy\/calendar\/index.ts","content":"export { Calendar } from \".\/Calendar\";\r\nexport type { CalendarProps, CalendarMode, DateRange } from \".\/Calendar.types\";\r\n","type":"registry:ui","target":"components\/fancy\/calendar\/index.ts"}]}