{week.map(renderDay)}
diff --git a/src/form/Calendar/CalendarRange.tsx b/src/form/Calendar/CalendarRange.tsx
new file mode 100644
index 000000000..9cbbd807f
--- /dev/null
+++ b/src/form/Calendar/CalendarRange.tsx
@@ -0,0 +1,273 @@
+import { FC, Fragment, useCallback, useMemo, useState } from 'react';
+import {
+ add,
+ addMonths,
+ addYears,
+ endOfDecade,
+ getMonth,
+ getYear,
+ isSameDay,
+ max as maxDate,
+ min as minDate,
+ setMonth,
+ setYear,
+ startOfDecade,
+ sub,
+ subYears
+} from 'date-fns';
+import { AnimatePresence, motion } from 'framer-motion';
+import { Button } from '../../elements/Button';
+import { CalendarProps, CalendarViewType } from './Calendar';
+import { CalendarDays } from './CalendarDays';
+import { CalendarMonths } from './CalendarMonths';
+import { CalendarYears } from './CalendarYears';
+import { DateFormat } from '../../data/DateFormat';
+import { SmallHeading } from '../../typography';
+import css from './Calendar.module.css';
+
+export interface CalendarRangeProps extends CalendarProps {
+ /**
+ * The number of months to display in the range.
+ * Defaults to 2.
+ */
+ numMonths?: number;
+}
+
+export const CalendarRange: FC
= ({
+ min,
+ max,
+ value,
+ disabled,
+ isRange,
+ previousArrow,
+ nextArrow,
+ dateFormat,
+ animated,
+ onChange,
+ onViewChange,
+ numMonths,
+ enableDayOfWeek
+}) => {
+ const date = useMemo(
+ () => (Array.isArray(value) ? value?.[0] : value) ?? new Date(),
+ [value]
+ );
+ const rangeStart = useMemo(
+ () => value?.[0] ?? date ?? new Date(),
+ [date, value]
+ );
+ const rangeEnd = useMemo(
+ () => value?.[1] ?? date ?? new Date(),
+ [date, value]
+ );
+
+ const [viewValue, setViewValue] = useState(date || new Date());
+ const [monthValue, setMonthValue] = useState(getMonth(date));
+ const [yearValue, setYearValue] = useState(getYear(date));
+ const [decadeStart, setDecadeStart] = useState(startOfDecade(date));
+ const [decadeEnd, setDecadeEnd] = useState(endOfDecade(date));
+ const [view, setView] = useState('days');
+ const [hoveringDate, setHoveringDate] = useState(null);
+ const [scrollDirection, setScrollDirection] = useState<
+ 'forward' | 'back' | null
+ >(null);
+
+ if (numMonths < 0) {
+ return null;
+ }
+
+ const displayMonths = Array.from(Array(numMonths).keys());
+
+ const previousClickHandler = useCallback(() => {
+ setScrollDirection('back');
+ if (view === 'days') {
+ setViewValue(sub(viewValue, { months: 1 }));
+ } else if (view === 'months') {
+ setYearValue(yearValue - 1);
+ } else {
+ setDecadeStart(subYears(decadeStart, 10));
+ setDecadeEnd(subYears(decadeEnd, 10));
+ }
+ }, [decadeEnd, decadeStart, view, viewValue, yearValue]);
+
+ const nextClickHandler = useCallback(() => {
+ setScrollDirection('forward');
+ if (view === 'days') {
+ setViewValue(add(viewValue, { months: 1 }));
+ } else if (view === 'months') {
+ setYearValue(yearValue + 1);
+ } else {
+ setDecadeStart(addYears(decadeStart, 10));
+ setDecadeEnd(addYears(decadeEnd, 10));
+ }
+ }, [decadeEnd, decadeStart, view, viewValue, yearValue]);
+
+ const headerClickHandler = useCallback(() => {
+ const newView = view === 'days' ? 'months' : 'years';
+ setScrollDirection(null);
+ setView(newView);
+ onViewChange?.(newView);
+ }, [onViewChange, view]);
+
+ const dateChangeHandler = useCallback(
+ (date: Date) => {
+ if (!isRange) {
+ onChange?.(date);
+ setMonthValue(getMonth(date));
+ setYearValue(getYear(date));
+ } else {
+ if (isSameDay(rangeStart, rangeEnd)) {
+ onChange?.([minDate([rangeStart, date]), maxDate([rangeEnd, date])]);
+ } else {
+ onChange?.([date, date]);
+ }
+ }
+ },
+ [isRange, onChange, rangeEnd, rangeStart]
+ );
+
+ const monthsChangeHandler = useCallback(
+ month => {
+ setViewValue(setMonth(setYear(min || new Date(), yearValue), month));
+ setMonthValue(month);
+ setView('days');
+ onViewChange?.('days');
+ },
+ [min, yearValue, onViewChange]
+ );
+
+ const yearChangeHandler = useCallback(
+ year => {
+ setViewValue(setYear(min || new Date(), year));
+ setYearValue(year);
+ setView('months');
+ onViewChange?.('months');
+ },
+ [min, onViewChange]
+ );
+
+ const xAnimation = useMemo(() => {
+ switch (scrollDirection) {
+ case 'forward':
+ return '100%';
+ case 'back':
+ return '-100%';
+ default:
+ return 0;
+ }
+ }, [scrollDirection]);
+
+ return (
+
+
+
+
+
+
+
+
+ {view === 'days' &&
+ displayMonths.map(month => (
+
+ 0}
+ hideNextMonthDays={month < numMonths - 1}
+ enableDayOfWeek={enableDayOfWeek}
+ />
+
+ ))}
+ {view === 'months' && (
+
+ )}
+ {view === 'years' && (
+
+ )}
+
+
+
+ );
+};
+
+CalendarRange.defaultProps = {
+ previousArrow: '←',
+ nextArrow: '→',
+ animated: true,
+ dateFormat: 'MMMM yyyy',
+ range: [new Date(), new Date()],
+ numMonths: 2
+};
diff --git a/src/form/Calendar/index.ts b/src/form/Calendar/index.ts
index a72338059..eb3ea071e 100644
--- a/src/form/Calendar/index.ts
+++ b/src/form/Calendar/index.ts
@@ -1 +1,2 @@
export * from './Calendar';
+export * from './CalendarRange';