Repository: spartan-ng/spartan
Pull request #653: feat: calendar/date-picker multi selection
Author: login: marcjulian
Created: 2025-04-02T09:05:17Z | Merged: 2025-04-08T07:01:05Z

## PR Checklist

Please check if your PR fulfills the following requirements:

- [ ] The commit message follows our
      guidelines: https://github.com/goetzrobin/spartan/blob/main/CONTRIBUTING.md#-commit-message-guidelines
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

## PR Type

What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->

- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Other... Please describe:

## Which package are you modifying?

- [ ] accordion
- [ ] alert
- [ ] alert-dialog
- [ ] aspect-ratio
- [ ] avatar
- [ ] badge
- [ ] button
- [x] calendar
- [ ] card
- [ ] carousel
- [ ] checkbox
- [ ] collapsible
- [ ] combobox
- [ ] command
- [ ] context-menu
- [ ] data-table
- [x] date-picker
- [ ] dialog
- [ ] dropdown-menu
- [ ] hover-card
- [ ] icon
- [ ] input
- [ ] label
- [ ] menubar
- [ ] navigation-menu
- [ ] pagination
- [ ] popover
- [ ] progress
- [ ] radio-group
- [ ] scroll-area
- [ ] select
- [ ] separator
- [ ] sheet
- [ ] skeleton
- [ ] slider
- [ ] sonner
- [ ] spinner
- [ ] switch
- [ ] table
- [ ] tabs
- [ ] textarea
- [ ] toast
- [ ] toggle
- [ ] tooltip
- [ ] typography

## What is the current behavior?

<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Calendar single selection
* deselecting current date not supported

## What is the new behavior?

Calendar single selection
* deselect current date

Calendar multiple selection
* new `brnCalendarMulti` directive and `hlm-calendar-multi`component
* add `minSelection` and `maxSelection` inputs to limit deselection and reset selection when max reached

Date Picker multiple selection
* new `hlm-date-picker-multi`
* `autoCloseOnMaxSelection` option to close when `maxSelection` is reached

## Does this PR introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->

## Other information

replaces #652 



Diff:
diff --git a/apps/app/src/app/pages/(components)/components/(calendar)/calendar--multiple.example.ts b/apps/app/src/app/pages/(components)/components/(calendar)/calendar--multiple.example.ts
new file mode 100644
index 0000000000..8bb0cc5ecb
--- /dev/null
+++ b/apps/app/src/app/pages/(components)/components/(calendar)/calendar--multiple.example.ts
@@ -0,0 +1,61 @@
+import { Component } from '@angular/core';
+import { HlmCalendarMultiComponent } from '@spartan-ng/ui-calendar-helm';
+
+@Component({
+	selector: 'spartan-calendar-multiple',
+	imports: [HlmCalendarMultiComponent],
+	template: `
+		<hlm-calendar-multi
+			[(date)]="selectedDates"
+			[min]="minDate"
+			[max]="maxDate"
+			[minSelection]="2"
+			[maxSelection]="6"
+		/>
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class CalendarMultipleExampleComponent {
+	/** The selected date */
+	public selectedDates = [new Date()];
+
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+}
+
+export const calendarMultipleCode = `
+import { Component } from '@angular/core';
+import { HlmCalendarMultiComponent } from '@spartan-ng/ui-calendar-helm';
+
+@Component({
+	selector: 'spartan-calendar-multiple',
+	imports: [HlmCalendarMultiComponent],
+	template: \`
+		<hlm-calendar-multi
+			[(date)]="selectedDates"
+			[min]="minDate"
+			[max]="maxDate"
+			[minSelection]="2"
+			[maxSelection]="6"
+		/>
+	\`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class CalendarMultipleExampleComponent {
+	/** The selected date */
+	public selectedDates = [new Date()];
+
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+}
+`;
diff --git a/apps/app/src/app/pages/(components)/components/(calendar)/calendar.page.ts b/apps/app/src/app/pages/(components)/components/(calendar)/calendar.page.ts
index d0dd5451bb..ad6ae54c3e 100644
--- a/apps/app/src/app/pages/(components)/components/(calendar)/calendar.page.ts
+++ b/apps/app/src/app/pages/(components)/components/(calendar)/calendar.page.ts
@@ -1,5 +1,6 @@
 import type { RouteMeta } from '@analogjs/router';
 import { Component } from '@angular/core';
+import { hlmCode, hlmH4, hlmP } from '@spartan-ng/ui-typography-helm';
 import { CodeComponent } from '../../../../shared/code/code.component';
 import { PageBottomNavLinkComponent } from '../../../../shared/layout/page-bottom-nav/page-bottom-nav-link.component';
 import { PageBottomNavComponent } from '../../../../shared/layout/page-bottom-nav/page-bottom-nav.component';
@@ -9,6 +10,7 @@ import { SectionSubHeadingComponent } from '../../../../shared/layout/section-su
 import { TabsCliComponent } from '../../../../shared/layout/tabs-cli.component';
 import { TabsComponent } from '../../../../shared/layout/tabs.component';
 import { metaWith } from '../../../../shared/meta/meta.util';
+import { calendarMultipleCode, CalendarMultipleExampleComponent } from './calendar--multiple.example';
 import { CalendarPreviewComponent, codeSkeleton, defaultCode, defaultImports } from './calendar.preview';
 
 export const routeMeta: RouteMeta = {
@@ -29,6 +31,7 @@ export const routeMeta: RouteMeta = {
 		PageBottomNavComponent,
 		PageBottomNavLinkComponent,
 		PageNavComponent,
+		CalendarMultipleExampleComponent,
 	],
 	template: `
 		<section spartanMainSection>
@@ -54,6 +57,26 @@ export const routeMeta: RouteMeta = {
 				<spartan-code [code]="codeSkeleton" />
 			</div>
 
+			<spartan-section-sub-heading id="examples">Examples</spartan-section-sub-heading>
+			<h3 id="examples__default" class="${hlmH4} mb-2 mt-6">Multiple Selection</h3>
+
+			<p class="${hlmP} mb-6">
+				Use
+				<code class="${hlmCode}">hlm-calendar-multi</code>
+				for multiple date selection. Limit the selectable dates using
+				<code class="${hlmCode}">minSelection</code>
+				and
+				<code class="${hlmCode}">maxSelection</code>
+				inputs.
+			</p>
+
+			<spartan-tabs firstTab="Preview" secondTab="Code">
+				<div spartanCodePreview firstTab>
+					<spartan-calendar-multiple />
+				</div>
+				<spartan-code secondTab [code]="calendarMultipleCode" />
+			</spartan-tabs>
+
 			<spartan-page-bottom-nav>
 				<spartan-page-bottom-nav-link href="carousel" label="Carousel" />
 				<spartan-page-bottom-nav-link direction="previous" href="button" label="Button" />
@@ -66,4 +89,5 @@ export default class CardPageComponent {
 	protected readonly defaultCode = defaultCode;
 	protected readonly defaultImports = defaultImports;
 	protected readonly codeSkeleton = codeSkeleton;
+	protected readonly calendarMultipleCode = calendarMultipleCode;
 }
diff --git a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form-multi.example.ts b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form-multi.example.ts
new file mode 100644
index 0000000000..6e15eb24be
--- /dev/null
+++ b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form-multi.example.ts
@@ -0,0 +1,94 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
+import { HlmDatePickerMultiComponent } from '@spartan-ng/ui-date-picker-helm';
+import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
+
+@Component({
+	selector: 'spartan-date-picker-form-multiple',
+	imports: [HlmDatePickerMultiComponent, ReactiveFormsModule, HlmButtonDirective, HlmLabelDirective],
+	template: `
+		<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
+			<label hlmLabel>
+				Available dates
+				<hlm-date-picker-multi
+					[min]="minDate"
+					[max]="maxDate"
+					formControlName="availableDates"
+					[autoCloseOnMaxSelection]="true"
+					[minSelection]="2"
+					[maxSelection]="4"
+				>
+					<span>Pick dates</span>
+				</hlm-date-picker-multi>
+			</label>
+
+			<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
+		</form>
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerFormMultipleExampleComponent {
+	private readonly _formBuilder = inject(FormBuilder);
+
+	public form = this._formBuilder.group({
+		availableDates: [[], Validators.required],
+	});
+
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+
+	submit() {
+		console.log(this.form.value);
+	}
+}
+
+export const datePickerFormMultipleCode = `
+import { Component, inject } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
+import { HlmDatePickerComponent } from '@spartan-ng/ui-date-picker-helm';
+import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
+
+@Component({
+	selector: 'spartan-date-picker-form-multiple',
+	imports: [HlmDatePickerComponent, ReactiveFormsModule, HlmButtonDirective, HlmLabelDirective],
+	template: \`
+		<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
+			<label hlmLabel>
+				Available dates
+				<hlm-date-picker mode="multiple" [min]="minDate" [max]="maxDate" formControlName="availableDates">
+					<span>Pick dates</span>
+				</hlm-date-picker>
+			</label>
+
+			<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
+		</form>
+	\`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerFormMultipleExampleComponent {
+	private readonly _formBuilder = inject(FormBuilder);
+
+	public form = this._formBuilder.group({
+		availableDates: [[], Validators.required],
+	});
+
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+
+	submit() {
+		console.log(this.form.value);
+	}
+}
+`;
diff --git a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form.example.ts b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form.example.ts
index ff364a4f6e..0aea944605 100644
--- a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form.example.ts
+++ b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form.example.ts
@@ -30,9 +30,6 @@ export class DatePickerFormExampleComponent {
 		birthday: [null, Validators.required],
 	});
 
-	/** The selected date */
-	public selectedDate = new Date();
-
 	/** The minimum date */
 	public minDate = new Date(2023, 0, 1);
 
@@ -78,9 +75,6 @@ export class DatePickerFormExampleComponent {
 		birthday: [undefined, Validators.required],
 	});
 
-	/** The selected date */
-	public selectedDate = new Date();
-
 	/** The minimum date */
 	public minDate = new Date(2023, 0, 1);
 
diff --git a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--multi.example.ts b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--multi.example.ts
new file mode 100644
index 0000000000..b831571e15
--- /dev/null
+++ b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--multi.example.ts
@@ -0,0 +1,59 @@
+import { Component } from '@angular/core';
+import { HlmDatePickerMultiComponent } from '@spartan-ng/ui-date-picker-helm';
+
+@Component({
+	selector: 'spartan-date-picker-multiple',
+	imports: [HlmDatePickerMultiComponent],
+	template: `
+		<hlm-date-picker-multi
+			[min]="minDate"
+			[max]="maxDate"
+			[autoCloseOnMaxSelection]="true"
+			[minSelection]="2"
+			[maxSelection]="6"
+		>
+			<span>Pick dates</span>
+		</hlm-date-picker-multi>
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerMultipleExampleComponent {
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+}
+
+export const datePickerMultipleCode = `
+import { Component } from '@angular/core';
+import { HlmDatePickerMultiComponent } from '@spartan-ng/ui-date-picker-helm';
+
+@Component({
+	selector: 'spartan-date-picker-multiple',
+	imports: [HlmDatePickerMultiComponent],
+	template: \`
+		<hlm-date-picker-multi
+			[min]="minDate"
+			[max]="maxDate"
+			[autoCloseOnMaxSelection]="true"
+			[minSelection]="2"
+			[maxSelection]="6"
+		>
+			<span>Pick dates</span>
+		</hlm-date-picker-multi>
+	\`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerMultipleExampleComponent {
+	/** The minimum date */
+	public minDate = new Date(2023, 0, 1);
+
+	/** The maximum date */
+	public maxDate = new Date(2030, 11, 31);
+}
+`;
diff --git a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker.page.ts b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker.page.ts
index 7f6237756e..19de58761f 100644
--- a/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker.page.ts
+++ b/apps/app/src/app/pages/(components)/components/(date-picker)/date-picker.page.ts
@@ -13,8 +13,10 @@ import { TabsCliComponent } from '../../../../shared/layout/tabs-cli.component';
 import { TabsComponent } from '../../../../shared/layout/tabs.component';
 import { metaWith } from '../../../../shared/meta/meta.util';
 import { datePickerConfigCode, DatePickerConfigExampleComponent } from './date-picker--config.example';
+import { datePickerFormMultipleCode, DatePickerFormMultipleExampleComponent } from './date-picker--form-multi.example';
 import { datePickerFormCode, DatePickerFormExampleComponent } from './date-picker--form.example';
 import { datePickerFormatCode, DatePickerFormatExampleComponent } from './date-picker--format.example';
+import { datePickerMultipleCode, DatePickerMultipleExampleComponent } from './date-picker--multi.example';
 import { codeSkeleton, DatePickerPreviewComponent, defaultCode, defaultImports } from './date-picker.preview';
 
 export const routeMeta: RouteMeta = {
@@ -40,6 +42,8 @@ const link = 'h-6 underline text-base px-0.5';
 		DatePickerConfigExampleComponent,
 		DatePickerFormatExampleComponent,
 		DatePickerFormExampleComponent,
+		DatePickerMultipleExampleComponent,
+		DatePickerFormMultipleExampleComponent,
 		RouterLink,
 		HlmButtonDirective,
 	],
@@ -108,6 +112,15 @@ const link = 'h-6 underline text-base px-0.5';
 				<spartan-code secondTab [code]="datePickerConfigCode" />
 			</spartan-tabs>
 
+			<h3 id="examples__default" class="${hlmH4} mb-2 mt-6">Multiple Selection</h3>
+
+			<spartan-tabs firstTab="Preview" secondTab="Code">
+				<div spartanCodePreview firstTab>
+					<spartan-date-picker-multiple />
+				</div>
+				<spartan-code secondTab [code]="datePickerMultipleCode" />
+			</spartan-tabs>
+
 			<h3 id="examples__default" class="${hlmH4} mb-2 mt-6">Format Date</h3>
 
 			<p class="${hlmP} mb-6">
@@ -137,6 +150,21 @@ const link = 'h-6 underline text-base px-0.5';
 				<spartan-code secondTab [code]="datePickerFormCode" />
 			</spartan-tabs>
 
+			<h3 id="examples__default" class="${hlmH4} mb-2 mt-6">Form Multiple Selection</h3>
+			<p class="${hlmP} mb-6">
+				Sync the dates to a form by adding
+				<code class="${hlmCode}">formControlName</code>
+				to
+				<code class="${hlmCode}">hlm-date-picker</code>
+				.
+			</p>
+			<spartan-tabs firstTab="Preview" secondTab="Code">
+				<div spartanCodePreview firstTab>
+					<spartan-date-picker-form-multiple />
+				</div>
+				<spartan-code secondTab [code]="datePickerFormMultipleCode" />
+			</spartan-tabs>
+
 			<spartan-page-bottom-nav>
 				<spartan-page-bottom-nav-link href="dialog" label="Dialog" />
 				<spartan-page-bottom-nav-link direction="previous" href="data-table" label="Data Table" />
@@ -151,5 +179,7 @@ export default class CardPageComponent {
 	protected readonly codeSkeleton = codeSkeleton;
 	protected readonly datePickerConfigCode = datePickerConfigCode;
 	protected readonly datePickerFormCode = datePickerFormCode;
+	protected readonly datePickerFormMultipleCode = datePickerFormMultipleCode;
 	protected readonly datePickerFormatCode = datePickerFormatCode;
+	protected readonly datePickerMultipleCode = datePickerMultipleCode;
 }
diff --git a/libs/brain/calendar/src/index.ts b/libs/brain/calendar/src/index.ts
index 7e5ba4f985..66ae17433e 100644
--- a/libs/brain/calendar/src/index.ts
+++ b/libs/brain/calendar/src/index.ts
@@ -9,6 +9,7 @@ export * from './lib/brn-calendar-weekday.directive';
 export * from './lib/brn-calendar.directive';
 export * from './lib/brn-calendar.token';
 export * from './lib/i18n/calendar-i18n';
+export * from './lib/mode/brn-calendar-multiple.directive';
 
 import { NgModule } from '@angular/core';
 import { BrnCalendarCellButtonDirective } from './lib/brn-calendar-cell-button.directive';
@@ -20,6 +21,7 @@ import { BrnCalendarPreviousButtonDirective } from './lib/brn-calendar-previous-
 import { BrnCalendarWeekDirective } from './lib/brn-calendar-week.directive';
 import { BrnCalendarWeekdayDirective } from './lib/brn-calendar-weekday.directive';
 import { BrnCalendarDirective } from './lib/brn-calendar.directive';
+import { BrnCalendarMultiDirective } from './lib/mode/brn-calendar-multiple.directive';
 
 export const BrnCalendarImports = [
 	BrnCalendarCellButtonDirective,
@@ -31,6 +33,7 @@ export const BrnCalendarImports = [
 	BrnCalendarWeekdayDirective,
 	BrnCalendarDirective,
 	BrnCalendarCellDirective,
+	BrnCalendarMultiDirective,
 ] as const;
 
 @NgModule({
diff --git a/libs/brain/calendar/src/lib/brn-calendar-cell-button.directive.ts b/libs/brain/calendar/src/lib/brn-calendar-cell-button.directive.ts
index f8763ab58f..cbde0a45ff 100644
--- a/libs/brain/calendar/src/lib/brn-calendar-cell-button.directive.ts
+++ b/libs/brain/calendar/src/lib/brn-calendar-cell-button.directive.ts
@@ -41,10 +41,7 @@ export class BrnCalendarCellButtonDirective<T> {
 	public readonly date = input.required<T>();
 
 	/** Whether this date is currently selected */
-	public readonly selected = computed(() => {
-		const selectedDate = this.calendar.date();
-		return selectedDate && this.dateAdapter.isSameDay(this.date(), selectedDate);
-	});
+	public readonly selected = computed(() => this.calendar.isSelected(this.date()));
 
 	/** Whether this date is focusable */
 	public readonly focusable = computed(() => this.dateAdapter.isSameDay(this.calendar.focusedDate(), this.date()));
diff --git a/libs/brain/calendar/src/lib/brn-calendar.directive.ts b/libs/brain/calendar/src/lib/brn-calendar.directive.ts
index 900ba8c7b6..473a41f09f 100644
--- a/libs/brain/calendar/src/lib/brn-calendar.directive.ts
+++ b/libs/brain/calendar/src/lib/brn-calendar.directive.ts
@@ -17,14 +17,14 @@ import {
 import { injectDateAdapter } from '@spartan-ng/brain/date-time';
 import { BrnCalendarCellButtonDirective } from './brn-calendar-cell-button.directive';
 import { BrnCalendarHeaderDirective } from './brn-calendar-header.directive';
-import { provideBrnCalendar } from './brn-calendar.token';
+import { BrnCalendar, provideBrnCalendar } from './brn-calendar.token';
 
 @Directive({
 	selector: '[brnCalendar]',
 	standalone: true,
 	providers: [provideBrnCalendar(BrnCalendarDirective)],
 })
-export class BrnCalendarDirective<T> {
+export class BrnCalendarDirective<T> implements BrnCalendar<T> {
 	/** Access the date adapter */
 	protected readonly dateAdapter = injectDateAdapter<T>();
 
@@ -37,7 +37,7 @@ export class BrnCalendarDirective<T> {
 	/** The minimum date that can be selected.*/
 	public readonly min = input<T>();
 
-	/* * The maximum date that can be selected. */
+	/** The maximum date that can be selected. */
 	public readonly max = input<T>();
 
 	/** Determine if the date picker is disabled. */
@@ -167,8 +167,17 @@ export class BrnCalendarDirective<T> {
 		return false;
 	}
 
+	isSelected(date: T): boolean {
+		const selected = this.date() as T | undefined;
+		return selected !== undefined && this.dateAdapter.isSameDay(date, selected);
+	}
+
 	selectDate(date: T): void {
-		this.date.set(date);
+		if (this.isSelected(date)) {
+			this.date.set(undefined);
+		} else {
+			this.date.set(date);
+		}
 		this.state().focusedDate.set(date);
 	}
 
diff --git a/libs/brain/calendar/src/lib/brn-calendar.token.ts b/libs/brain/calendar/src/lib/brn-calendar.token.ts
index e2dea26a64..3f71f56447 100644
--- a/libs/brain/calendar/src/lib/brn-calendar.token.ts
+++ b/libs/brain/calendar/src/lib/brn-calendar.token.ts
@@ -1,15 +1,32 @@
-import { ExistingProvider, InjectionToken, Type, inject } from '@angular/core';
-import type { BrnCalendarDirective } from './brn-calendar.directive';
+import { ExistingProvider, InjectionToken, Signal, Type, WritableSignal, inject } from '@angular/core';
+import { BrnCalendarHeaderDirective } from './brn-calendar-header.directive';
 
-export const BrnCalendarToken = new InjectionToken<BrnCalendarDirective<unknown>>('BrnCalendarToken');
+export interface BrnCalendar<T> {
+	isSelected: (date: T) => boolean;
+	selectDate: (date: T) => void;
 
-export function provideBrnCalendar<T>(instance: Type<BrnCalendarDirective<T>>): ExistingProvider {
+	constrainDate: (date: T) => T;
+	isDateDisabled: (date: T) => boolean;
+	setFocusedDate: (date: T) => void;
+
+	disabled: Signal<boolean>;
+	focusedDate: Signal<T>;
+	header: Signal<BrnCalendarHeaderDirective | undefined>;
+	state: Signal<{
+		focusedDate: WritableSignal<T>;
+	}>;
+	days: Signal<T[]>;
+}
+
+export const BrnCalendarToken = new InjectionToken<BrnCalendar<unknown>>('BrnCalendarToken');
+
+export function provideBrnCalendar<T>(instance: Type<BrnCalendar<T>>): ExistingProvider {
 	return { provide: BrnCalendarToken, useExisting: instance };
 }
 
 /**
  * Inject the calendar component.
  */
-export function injectBrnCalendar<T>(): BrnCalendarDirective<T> {
-	return inject(BrnCalendarToken) as BrnCalendarDirective<T>;
+export function injectBrnCalendar<T>(): BrnCalendar<T> {
+	return inject(BrnCalendarToken) as BrnCalendar<T>;
 }
diff --git a/libs/brain/calendar/src/lib/mode/brn-calendar-multiple.directive.ts b/libs/brain/calendar/src/lib/mode/brn-calendar-multiple.directive.ts
new file mode 100644
index 0000000000..84a2995ae6
--- /dev/null
+++ b/libs/brain/calendar/src/lib/mode/brn-calendar-multiple.directive.ts
@@ -0,0 +1,237 @@
+import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
+import {
+	afterNextRender,
+	booleanAttribute,
+	ChangeDetectorRef,
+	computed,
+	contentChild,
+	contentChildren,
+	Directive,
+	inject,
+	Injector,
+	input,
+	model,
+	numberAttribute,
+	signal,
+} from '@angular/core';
+import { injectDateAdapter } from '@spartan-ng/brain/date-time';
+import { BrnCalendarCellButtonDirective } from '../brn-calendar-cell-button.directive';
+import { BrnCalendarHeaderDirective } from '../brn-calendar-header.directive';
+import { Weekday } from '../brn-calendar.directive';
+import { BrnCalendar, provideBrnCalendar } from '../brn-calendar.token';
+
+@Directive({
+	selector: '[brnCalendarMulti]',
+	standalone: true,
+	providers: [provideBrnCalendar(BrnCalendarMultiDirective)],
+})
+export class BrnCalendarMultiDirective<T> implements BrnCalendar<T> {
+	// /** Access the date adapter */
+	protected readonly dateAdapter = injectDateAdapter<T>();
+
+	/** Access the change detector */
+	private readonly _changeDetector = inject(ChangeDetectorRef);
+
+	/** Access the injector */
+	private readonly _injector = inject(Injector);
+
+	/** The minimum date that can be selected.*/
+	public readonly min = input<T>();
+
+	/** The maximum date that can be selected. */
+	public readonly max = input<T>();
+
+	/** The minimum selectable dates.  */
+	public readonly minSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** The maximum selectable dates.  */
+	public readonly maxSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** Determine if the date picker is disabled. */
+	public readonly disabled = input<boolean, BooleanInput>(false, {
+		transform: booleanAttribute,
+	});
+
+	/** The selected value. */
+	public readonly date = model<T[]>();
+
+	/** Whether a specific date is disabled. */
+	public readonly dateDisabled = input<(date: T) => boolean>(() => false);
+
+	/** The day the week starts on */
+	public readonly weekStartsOn = input<Weekday, NumberInput>(0, {
+		transform: (v: unknown) => numberAttribute(v) as Weekday,
+	});
+
+	/** The default focused date. */
+	public readonly defaultFocusedDate = input<T>();
+
+	/** @internal Access the header */
+	public readonly header = contentChild(BrnCalendarHeaderDirective);
+
+	/** Store the cells */
+	protected readonly cells = contentChildren<BrnCalendarCellButtonDirective<T>>(BrnCalendarCellButtonDirective, {
+		descendants: true,
+	});
+
+	/**
+	 * @internal
+	 * The internal state of the component.
+	 */
+	public readonly state = computed(() => ({
+		focusedDate: signal(this.constrainDate(this.defaultFocusedDate() ?? this.dateAdapter.now())),
+	}));
+
+	/**
+	 * The focused date.
+	 */
+	public readonly focusedDate = computed(() => this.state().focusedDate());
+
+	/**
+	 * Get all the days to display, this is the days of the current month
+	 * and the days of the previous and next month to fill the grid.
+	 */
+	public readonly days = computed(() => {
+		const weekStartsOn = this.weekStartsOn();
+		const month = this.state().focusedDate();
+		const days: T[] = [];
+
+		// Get the first and last day of the month.
+		let firstDay = this.dateAdapter.startOfMonth(month);
+		let lastDay = this.dateAdapter.endOfMonth(month);
+
+		// we need to subtract until we get the to starting day before or on the start of the month.
+		while (this.dateAdapter.getDay(firstDay) !== weekStartsOn) {
+			firstDay = this.dateAdapter.subtract(firstDay, { days: 1 });
+		}
+
+		const weekEndsOn = (weekStartsOn + 6) % 7;
+
+		// we need to add until we get to the ending day after or on the end of the month.
+		while (this.dateAdapter.getDay(lastDay) !== weekEndsOn) {
+			lastDay = this.dateAdapter.add(lastDay, { days: 1 });
+		}
+
+		// collect all the days to display.
+		while (firstDay <= lastDay) {
+			days.push(firstDay);
+			firstDay = this.dateAdapter.add(firstDay, { days: 1 });
+		}
+
+		return days;
+	});
+
+	isSelected(date: T): boolean {
+		return this.date()?.some((d) => this.dateAdapter.isSameDay(d, date)) ?? false;
+	}
+
+	selectDate(date: T): void {
+		const selected = this.date() as T[] | undefined;
+		if (this.isSelected(date)) {
+			const minSelection = this.minSelection();
+			if (selected?.length === minSelection) {
+				// min selection reached, do not allow to deselect
+				return;
+			}
+
+			this.date.set(selected?.filter((d) => !this.dateAdapter.isSameDay(d, date)));
+		} else {
+			const maxSelection = this.maxSelection();
+			if (selected?.length === maxSelection) {
+				// max selection reached, reset the selection to date
+				this.date.set([date]);
+			} else {
+				// add the date to the selection
+				this.date.set([...(selected ?? []), date]);
+			}
+		}
+	}
+
+	// same as in brn-calendar.directive.ts
+	/** @internal Constrain a date to the min and max boundaries */
+	constrainDate(date: T): T {
+		const min = this.min();
+		const max = this.max();
+
+		// If there is no min or max, return the date.
+		if (!min && !max) {
+			return date;
+		}
+
+		// If there is a min and the date is before the min, return the min.
+		if (min && this.dateAdapter.isBefore(date, this.dateAdapter.startOfDay(min))) {
+			return min;
+		}
+
+		// If there is a max and the date is after the max, return the max.
+		if (max && this.dateAdapter.isAfter(date, this.dateAdapter.endOfDay(max))) {
+			return max;
+		}
+
+		// Return the date.
+		return date;
+	}
+
+	/** @internal Determine if a date is disabled */
+	isDateDisabled(date: T): boolean {
+		// if the calendar is disabled we can't select this date
+		if (this.disabled()) {
+			return true;
+		}
+
+		// if the date is outside the min and max range
+		const min = this.min();
+		const max = this.max();
+
+		if (min && this.dateAdapter.isBefore(date, this.dateAdapter.startOfDay(min))) {
+			return true;
+		}
+
+		if (max && this.dateAdapter.isAfter(date, this.dateAdapter.endOfDay(max))) {
+			return true;
+		}
+
+		// if this specific date is disabled
+		const disabledFn = this.dateDisabled();
+
+		if (disabledFn(date)) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/** @internal Set the focused date */
+	setFocusedDate(date: T): void {
+		// check if the date is disabled.
+		if (this.isDateDisabled(date)) {
+			return;
+		}
+
+		this.state().focusedDate.set(date);
+
+		// wait until the cells have all updated
+		afterNextRender(
+			{
+				write: () => {
+					// focus the cell with the target date.
+					const cell = this.cells().find((c) => this.dateAdapter.isSameDay(c.date(), date));
+
+					if (cell) {
+						cell.focus();
+					}
+				},
+			},
+			{
+				injector: this._injector,
+			},
+		);
+
+		// we must update the view to ensure the focused cell is visible.
+		this._changeDetector.detectChanges();
+	}
+}
diff --git a/libs/ui/calendar/helm/src/index.ts b/libs/ui/calendar/helm/src/index.ts
index ed69340829..e4ba3167b9 100644
--- a/libs/ui/calendar/helm/src/index.ts
+++ b/libs/ui/calendar/helm/src/index.ts
@@ -1,10 +1,14 @@
 import { NgModule } from '@angular/core';
+import { HlmCalendarMultiComponent } from './lib/hlm-calendar-multi.component';
 import { HlmCalendarComponent } from './lib/hlm-calendar.component';
 
+export * from './lib/hlm-calendar-multi.component';
 export * from './lib/hlm-calendar.component';
 
+export const HlmCalendarImports = [HlmCalendarComponent, HlmCalendarMultiComponent] as const;
+
 @NgModule({
-	imports: [HlmCalendarComponent],
-	exports: [HlmCalendarComponent],
+	imports: [...HlmCalendarImports],
+	exports: [...HlmCalendarImports],
 })
 export class HlmCalendarModule {}
diff --git a/libs/ui/calendar/helm/src/lib/hlm-calendar-multi.component.ts b/libs/ui/calendar/helm/src/lib/hlm-calendar-multi.component.ts
new file mode 100644
index 0000000000..565142f75b
--- /dev/null
+++ b/libs/ui/calendar/helm/src/lib/hlm-calendar-multi.component.ts
@@ -0,0 +1,178 @@
+import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
+import { Component, booleanAttribute, computed, input, model, numberAttribute, viewChild } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideChevronLeft, lucideChevronRight } from '@ng-icons/lucide';
+import {
+	BrnCalendarCellButtonDirective,
+	BrnCalendarCellDirective,
+	BrnCalendarGridDirective,
+	BrnCalendarHeaderDirective,
+	BrnCalendarMultiDirective,
+	BrnCalendarNextButtonDirective,
+	BrnCalendarPreviousButtonDirective,
+	BrnCalendarWeekDirective,
+	BrnCalendarWeekdayDirective,
+	Weekday,
+	injectBrnCalendarI18n,
+} from '@spartan-ng/brain/calendar';
+import { hlm } from '@spartan-ng/brain/core';
+import { injectDateAdapter } from '@spartan-ng/brain/date-time';
+import { buttonVariants } from '@spartan-ng/ui-button-helm';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+import type { ClassValue } from 'clsx';
+
+@Component({
+	selector: 'hlm-calendar-multi',
+	imports: [
+		BrnCalendarMultiDirective,
+		BrnCalendarHeaderDirective,
+		BrnCalendarNextButtonDirective,
+		BrnCalendarPreviousButtonDirective,
+		BrnCalendarWeekdayDirective,
+		BrnCalendarWeekDirective,
+		BrnCalendarCellButtonDirective,
+		BrnCalendarCellDirective,
+		BrnCalendarGridDirective,
+		NgIcon,
+		HlmIconDirective,
+	],
+	viewProviders: [provideIcons({ lucideChevronLeft, lucideChevronRight })],
+	template: `
+		<div
+			brnCalendarMulti
+			[min]="min()"
+			[max]="max()"
+			[minSelection]="minSelection()"
+			[maxSelection]="maxSelection()"
+			[disabled]="disabled()"
+			[(date)]="date"
+			[dateDisabled]="dateDisabled()"
+			[weekStartsOn]="weekStartsOn()"
+			[defaultFocusedDate]="defaultFocusedDate()"
+			[class]="_computedCalenderClass()"
+		>
+			<div class="inline-flex flex-col space-y-4">
+				<!-- Header -->
+				<div class="space-y-4">
+					<div class="relative flex items-center justify-center pt-1">
+						<div brnCalendarHeader class="text-sm font-medium">
+							{{ heading() }}
+						</div>
+
+						<div class="flex items-center space-x-1">
+							<button
+								brnCalendarPreviousButton
+								class="ring-offset-background focus-visible:ring-ring border-input hover:bg-accent hover:text-accent-foreground absolute left-1 inline-flex h-7 w-7 items-center justify-center whitespace-nowrap rounded-md border bg-transparent p-0 text-sm font-medium opacity-50 transition-colors hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
+							>
+								<ng-icon hlm name="lucideChevronLeft" size="sm" />
+							</button>
+
+							<button
+								brnCalendarNextButton
+								class="ring-offset-background focus-visible:ring-ring border-input hover:bg-accent hover:text-accent-foreground absolute right-1 inline-flex h-7 w-7 items-center justify-center whitespace-nowrap rounded-md border bg-transparent p-0 text-sm font-medium opacity-50 transition-colors hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
+							>
+								<ng-icon hlm name="lucideChevronRight" size="sm" />
+							</button>
+						</div>
+					</div>
+				</div>
+
+				<table class="w-full border-collapse space-y-1" brnCalendarGrid>
+					<thead>
+						<tr class="flex">
+							<th
+								*brnCalendarWeekday="let weekday"
+								scope="col"
+								class="text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal"
+								[attr.aria-label]="i18n.labelWeekday(weekday)"
+							>
+								{{ i18n.formatWeekdayName(weekday) }}
+							</th>
+						</tr>
+					</thead>
+
+					<tbody role="rowgroup">
+						<tr *brnCalendarWeek="let week" class="mt-2 flex w-full">
+							@for (date of week; track dateAdapter.getTime(date)) {
+								<td
+									brnCalendarCell
+									class="data-[selected]:data-[outside]:bg-accent/50 data-[selected]:bg-accent relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 first:data-[selected]:rounded-l-md last:data-[selected]:rounded-r-md [&:has([aria-selected].day-range-end)]:rounded-r-md"
+								>
+									<button brnCalendarCellButton [date]="date" [class]="btnClass">
+										{{ dateAdapter.getDate(date) }}
+									</button>
+								</td>
+							}
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</div>
+	`,
+})
+export class HlmCalendarMultiComponent<T> {
+	public readonly calendarClass = input<ClassValue>('');
+
+	protected readonly _computedCalenderClass = computed(() => hlm('rounded-md border p-3', this.calendarClass()));
+
+	/** Access the calendar i18n */
+	protected readonly i18n = injectBrnCalendarI18n();
+
+	/** Access the date time adapter */
+	protected readonly dateAdapter = injectDateAdapter<T>();
+
+	/** The minimum date that can be selected.*/
+	public readonly min = input<T>();
+
+	/** The maximum date that can be selected. */
+	public readonly max = input<T>();
+
+	/** The minimum selectable dates.  */
+	public readonly minSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** The maximum selectable dates.  */
+	public readonly maxSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** Determine if the date picker is disabled. */
+	public readonly disabled = input<boolean, BooleanInput>(false, {
+		transform: booleanAttribute,
+	});
+
+	/** The selected value. */
+	public readonly date = model<T[]>();
+
+	/** Whether a specific date is disabled. */
+	public readonly dateDisabled = input<(date: T) => boolean>(() => false);
+
+	/** The day the week starts on */
+	public readonly weekStartsOn = input<Weekday, NumberInput>(0, {
+		transform: (v: unknown) => numberAttribute(v) as Weekday,
+	});
+
+	/** The default focused date. */
+	public readonly defaultFocusedDate = input<T>();
+
+	/** Access the calendar directive */
+	private readonly _calendar = viewChild.required(BrnCalendarMultiDirective);
+
+	/** Get the heading for the current month and year */
+	protected heading = computed(() =>
+		this.i18n.formatHeader(
+			this.dateAdapter.getMonth(this._calendar().focusedDate()),
+			this.dateAdapter.getYear(this._calendar().focusedDate()),
+		),
+	);
+
+	protected readonly btnClass = hlm(
+		buttonVariants({ variant: 'ghost' }),
+		'h-9 w-9 p-0 font-normal aria-selected:opacity-100',
+		'data-[outside]:text-muted-foreground data-[outside]:opacity-50 data-[outside]:aria-selected:bg-accent/50 data-[outside]:aria-selected:text-muted-foreground data-[outside]:aria-selected:opacity-30',
+		'data-[today]:bg-accent data-[today]:text-accent-foreground',
+		'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
+		'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
+	);
+}
diff --git a/libs/ui/calendar/helm/src/lib/hlm-calendar.component.ts b/libs/ui/calendar/helm/src/lib/hlm-calendar.component.ts
index bd754e940d..eb534b87a2 100644
--- a/libs/ui/calendar/helm/src/lib/hlm-calendar.component.ts
+++ b/libs/ui/calendar/helm/src/lib/hlm-calendar.component.ts
@@ -122,7 +122,7 @@ export class HlmCalendarComponent<T> {
 	/** The minimum date that can be selected.*/
 	public readonly min = input<T>();
 
-	/* * The maximum date that can be selected. */
+	/** The maximum date that can be selected. */
 	public readonly max = input<T>();
 
 	/** Determine if the date picker is disabled. */
diff --git a/libs/ui/date-picker/helm/src/index.ts b/libs/ui/date-picker/helm/src/index.ts
index 4dfeb0acaf..d7b2190767 100644
--- a/libs/ui/date-picker/helm/src/index.ts
+++ b/libs/ui/date-picker/helm/src/index.ts
@@ -1,12 +1,16 @@
 import { NgModule } from '@angular/core';
+import { HlmDatePickerMultiComponent } from './lib/hlm-date-picker-multi.component';
 import { HlmDatePickerComponent } from './lib/hlm-date-picker.component';
 
 export * from './lib/hlm-date-picker.token';
 
+export * from './lib/hlm-date-picker-multi.component';
 export * from './lib/hlm-date-picker.component';
 
+export const HlmDatePickerImports = [HlmDatePickerComponent, HlmDatePickerMultiComponent] as const;
+
 @NgModule({
-	imports: [HlmDatePickerComponent],
-	exports: [HlmDatePickerComponent],
+	imports: [...HlmDatePickerImports],
+	exports: [...HlmDatePickerImports],
 })
 export class HlmDatePickerModule {}
diff --git a/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.component.ts b/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.component.ts
new file mode 100644
index 0000000000..0c0deddf38
--- /dev/null
+++ b/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.component.ts
@@ -0,0 +1,184 @@
+import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
+import {
+	booleanAttribute,
+	Component,
+	computed,
+	forwardRef,
+	input,
+	model,
+	numberAttribute,
+	output,
+	signal,
+} from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { lucideCalendar } from '@ng-icons/lucide';
+import { hlm } from '@spartan-ng/brain/core';
+import { BrnDialogState } from '@spartan-ng/brain/dialog';
+import { type ChangeFn, type TouchFn } from '@spartan-ng/brain/forms';
+import { BrnPopoverComponent, BrnPopoverContentDirective, BrnPopoverTriggerDirective } from '@spartan-ng/brain/popover';
+import { HlmCalendarMultiComponent } from '@spartan-ng/ui-calendar-helm';
+import { HlmIconDirective } from '@spartan-ng/ui-icon-helm';
+import { HlmPopoverContentDirective } from '@spartan-ng/ui-popover-helm';
+import type { ClassValue } from 'clsx';
+import { injectHlmDatePickerMultiConfig } from './hlm-date-picker-multi.token';
+
+export const HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR = {
+	provide: NG_VALUE_ACCESSOR,
+	useExisting: forwardRef(() => HlmDatePickerMultiComponent),
+	multi: true,
+};
+
+@Component({
+	selector: 'hlm-date-picker-multi',
+	imports: [
+		NgIcon,
+		HlmIconDirective,
+		BrnPopoverComponent,
+		BrnPopoverTriggerDirective,
+		BrnPopoverContentDirective,
+		HlmPopoverContentDirective,
+		HlmCalendarMultiComponent,
+	],
+	providers: [HLM_DATE_PICKER_MUTLI_VALUE_ACCESSOR, provideIcons({ lucideCalendar })],
+	template: `
+		<brn-popover sideOffset="5" [state]="popoverState()" (stateChanged)="popoverState.set($event)">
+			<button type="button" [class]="_computedClass()" [disabled]="state().disabled()" brnPopoverTrigger>
+				<ng-icon hlm size="sm" name="lucideCalendar" />
+
+				<span class="truncate">
+					@if (formattedDate(); as formattedDate) {
+						{{ formattedDate }}
+					} @else {
+						<ng-content />
+					}
+				</span>
+			</button>
+
+			<div hlmPopoverContent class="w-auto p-0" *brnPopoverContent="let ctx">
+				<hlm-calendar-multi
+					calendarClass="border-0 rounded-none"
+					[date]="date()"
+					[min]="min()"
+					[max]="max()"
+					[minSelection]="minSelection()"
+					[maxSelection]="maxSelection()"
+					[disabled]="state().disabled()"
+					(dateChange)="_handleChange($event)"
+				/>
+			</div>
+		</brn-popover>
+	`,
+	host: {
+		class: 'block',
+	},
+})
+export class HlmDatePickerMultiComponent<T> {
+	private readonly _config = injectHlmDatePickerMultiConfig<T>();
+
+	public readonly userClass = input<ClassValue>('', { alias: 'class' });
+	protected readonly _computedClass = computed(() =>
+		hlm(
+			'inline-flex items-center gap-2 whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 w-[280px] justify-start text-left font-normal',
+			'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
+			'disabled:pointer-events-none disabled:opacity-50',
+			'[&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0',
+			!this.date() ? 'text-muted-foreground' : '',
+			this.userClass(),
+		),
+	);
+
+	/** The minimum date that can be selected.*/
+	public readonly min = input<T>();
+
+	/** The maximum date that can be selected. */
+	public readonly max = input<T>();
+
+	/** The minimum selectable dates.  */
+	public readonly minSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** The maximum selectable dates.  */
+	public readonly maxSelection = input<number, NumberInput>(undefined, {
+		transform: numberAttribute,
+	});
+
+	/** Determine if the date picker is disabled. */
+	public readonly disabled = input<boolean, BooleanInput>(false, {
+		transform: booleanAttribute,
+	});
+
+	/** The selected value. */
+	public readonly date = model<T[]>();
+
+	/** If true, the date picker will close when the max selection of dates is reached.. */
+	public readonly autoCloseOnMaxSelection = input<boolean, BooleanInput>(this._config.autoCloseOnMaxSelection, {
+		transform: booleanAttribute,
+	});
+
+	/** Defines how the date should be displayed in the UI.  */
+	public readonly formatDates = input<(date: T[]) => string>(this._config.formatDates);
+
+	/** Defines how the date should be transformed before saving to model/form. */
+	public readonly transformDates = input<(date: T[]) => T[]>(this._config.transformDates);
+
+	protected readonly popoverState = signal<BrnDialogState | null>(null);
+
+	protected readonly state = computed(() => ({
+		disabled: signal(this.disabled()),
+	}));
+
+	protected readonly formattedDate = computed(() => {
+		const dates = this.date();
+		return dates ? this.formatDates()(dates) : undefined;
+	});
+
+	public readonly changed = output<T[]>();
+
+	protected _onChange?: ChangeFn<T[]>;
+	protected _onTouched?: TouchFn;
+
+	protected _handleChange(value: T[] | undefined) {
+		if (value === undefined) return;
+
+		if (this.state().disabled()) return;
+		const transformedDate = this.transformDates()(value);
+
+		this.date.set(transformedDate);
+		this._onChange?.(transformedDate);
+		this.changed.emit(transformedDate);
+
+		if (this.autoCloseOnMaxSelection() && this.date()?.length === this.maxSelection()) {
+			this.popoverState.set('closed');
+		}
+	}
+
+	/** CONROL VALUE ACCESSOR */
+	writeValue(value: T[] | null): void {
+		// optional FormControl is initialized with null value
+		if (value === null) return;
+
+		this.date.set(this.transformDates()(value));
+	}
+
+	registerOnChange(fn: ChangeFn<T[]>): void {
+		this._onChange = fn;
+	}
+
+	registerOnTouched(fn: TouchFn): void {
+		this._onTouched = fn;
+	}
+
+	setDisabledState(isDisabled: boolean): void {
+		this.state().disabled.set(isDisabled);
+	}
+
+	open() {
+		this.popoverState.set('open');
+	}
+
+	close() {
+		this.popoverState.set('closed');
+	}
+}
diff --git a/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.token.ts b/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.token.ts
new file mode 100644
index 0000000000..1e7ee32b89
--- /dev/null
+++ b/libs/ui/date-picker/helm/src/lib/hlm-date-picker-multi.token.ts
@@ -0,0 +1,43 @@
+import { inject, InjectionToken, ValueProvider } from '@angular/core';
+
+export interface HlmDatePickerMultiConfig<T> {
+	/**
+	 * If true, the date picker will close when the max selection of dates is reached.
+	 */
+	autoCloseOnMaxSelection: boolean;
+
+	/**
+	 * Defines how the date should be displayed in the UI.
+	 *
+	 * @param dates
+	 * @returns formatted date
+	 */
+	formatDates: (dates: T[]) => string;
+
+	/**
+	 * Defines how the date should be transformed before saving to model/form.
+	 *
+	 * @param dates
+	 * @returns transformed date
+	 */
+	transformDates: (dates: T[]) => T[];
+}
+
+function getDefaultConfig<T>(): HlmDatePickerMultiConfig<T> {
+	return {
+		formatDates: (dates) => dates.map((date) => (date instanceof Date ? date.toDateString() : `${date}`)).join(', '),
+		transformDates: (dates) => dates,
+		autoCloseOnMaxSelection: false,
+	};
+}
+
+const HlmDatePickerMultiConfigToken = new InjectionToken<HlmDatePickerMultiConfig<unknown>>('HlmDatePickerMultiConfig');
+
+export function provideHlmDatePickerConfig<T>(config: Partial<HlmDatePickerMultiConfig<T>>): ValueProvider {
+	return { provide: HlmDatePickerMultiConfigToken, useValue: { ...getDefaultConfig(), ...config } };
+}
+
+export function injectHlmDatePickerMultiConfig<T>(): HlmDatePickerMultiConfig<T> {
+	const injectedConfig = inject(HlmDatePickerMultiConfigToken, { optional: true });
+	return injectedConfig ? (injectedConfig as HlmDatePickerMultiConfig<T>) : getDefaultConfig();
+}
diff --git a/libs/ui/date-picker/helm/src/lib/hlm-date-picker.component.ts b/libs/ui/date-picker/helm/src/lib/hlm-date-picker.component.ts
index 73651db2a4..49bed775f8 100644
--- a/libs/ui/date-picker/helm/src/lib/hlm-date-picker.component.ts
+++ b/libs/ui/date-picker/helm/src/lib/hlm-date-picker.component.ts
@@ -36,11 +36,13 @@ export const HLM_DATE_PICKER_VALUE_ACCESSOR = {
 			<button type="button" [class]="_computedClass()" [disabled]="state().disabled()" brnPopoverTrigger>
 				<ng-icon hlm size="sm" name="lucideCalendar" />
 
-				@if (formattedDate(); as formattedDate) {
-					{{ formattedDate }}
-				} @else {
-					<ng-content />
-				}
+				<span class="truncate">
+					@if (formattedDate(); as formattedDate) {
+						{{ formattedDate }}
+					} @else {
+						<ng-content />
+					}
+				</span>
 			</button>
 
 			<div hlmPopoverContent class="w-auto p-0" *brnPopoverContent="let ctx">
@@ -77,7 +79,7 @@ export class HlmDatePickerComponent<T> {
 	/** The minimum date that can be selected.*/
 	public readonly min = input<T>();
 
-	/* * The maximum date that can be selected. */
+	/** The maximum date that can be selected. */
 	public readonly max = input<T>();
 
 	/** Determine if the date picker is disabled. */


Reviews:
user: login: ashley-hunter
body: Awesome work! Thank you! Just two very minor suggestions, works great! Excellent addition!
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T21:19:02Z
user: login: marcjulian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T07:37:56Z
user: login: marcjulian
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-04T07:38:05Z
user: login: ashley-hunter
body: Perfect! Thank you!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-04T07:40:10Z

Review comments:
diff_hunk: @@ -0,0 +1,100 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
+import { HlmDatePickerMultiComponent } from '@spartan-ng/ui-date-picker-helm';
+import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
+
+@Component({
+	selector: 'spartan-date-picker-form-multiple',
+	imports: [HlmDatePickerMultiComponent, ReactiveFormsModule, HlmButtonDirective, HlmLabelDirective],
+	template: `
+		<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
+			<label hlmLabel>
+				Available dates
+				<hlm-date-picker-multi
+					[min]="minDate"
+					[max]="maxDate"
+					formControlName="availableDates"
+					[autoCloseOnMaxSelection]="true"
+					[minSelection]="2"
+					[maxSelection]="4"
+				>
+					<span>Pick dates</span>
+				</hlm-date-picker-multi>
+			</label>
+
+			<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
+		</form>
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerFormMultipleExampleComponent {
+	private readonly _formBuilder = inject(FormBuilder);
+
+	public form = this._formBuilder.group({
+		availableDates: [[], Validators.required],
+	});
+
+	/** The selected date */
+	public selectedDate = new Date();
path: apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form-multi.example.ts
user: login: ashley-hunter
body: I don't think `selectedDate` is used here
created_at: 2025-04-03T21:12:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { Component } from '@angular/core';
+import { HlmCalendarMultiComponent } from '@spartan-ng/ui-calendar-helm';
+
+@Component({
+	selector: 'spartan-calendar-multiple',
+	imports: [HlmCalendarMultiComponent],
+	template: `
+		<hlm-calendar-multi [(date)]="selectedDate" [min]="minDate" [max]="maxDate" [minSelection]="2" [maxSelection]="6" />
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class CalendarMultipleExampleComponent {
+	/** The selected date */
+	public selectedDate = [new Date()];
path: apps/app/src/app/pages/(components)/components/(calendar)/calendar--multiple.example.ts
user: login: ashley-hunter
body: Could this be `selectedDates` just to indicate that it is can be multiple dates.
created_at: 2025-04-03T21:13:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,100 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+import { HlmButtonDirective } from '@spartan-ng/ui-button-helm';
+import { HlmDatePickerMultiComponent } from '@spartan-ng/ui-date-picker-helm';
+import { HlmLabelDirective } from '@spartan-ng/ui-label-helm';
+
+@Component({
+	selector: 'spartan-date-picker-form-multiple',
+	imports: [HlmDatePickerMultiComponent, ReactiveFormsModule, HlmButtonDirective, HlmLabelDirective],
+	template: `
+		<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
+			<label hlmLabel>
+				Available dates
+				<hlm-date-picker-multi
+					[min]="minDate"
+					[max]="maxDate"
+					formControlName="availableDates"
+					[autoCloseOnMaxSelection]="true"
+					[minSelection]="2"
+					[maxSelection]="4"
+				>
+					<span>Pick dates</span>
+				</hlm-date-picker-multi>
+			</label>
+
+			<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
+		</form>
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class DatePickerFormMultipleExampleComponent {
+	private readonly _formBuilder = inject(FormBuilder);
+
+	public form = this._formBuilder.group({
+		availableDates: [[], Validators.required],
+	});
+
+	/** The selected date */
+	public selectedDate = new Date();
path: apps/app/src/app/pages/(components)/components/(date-picker)/date-picker--form-multi.example.ts
user: login: marcjulian
body: True, slipped through.
created_at: 2025-04-04T07:37:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { Component } from '@angular/core';
+import { HlmCalendarMultiComponent } from '@spartan-ng/ui-calendar-helm';
+
+@Component({
+	selector: 'spartan-calendar-multiple',
+	imports: [HlmCalendarMultiComponent],
+	template: `
+		<hlm-calendar-multi [(date)]="selectedDate" [min]="minDate" [max]="maxDate" [minSelection]="2" [maxSelection]="6" />
+	`,
+	host: {
+		class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
+	},
+})
+export class CalendarMultipleExampleComponent {
+	/** The selected date */
+	public selectedDate = [new Date()];
path: apps/app/src/app/pages/(components)/components/(calendar)/calendar--multiple.example.ts
user: login: marcjulian
body: Yes of course!
created_at: 2025-04-04T07:38:05Z
author_association: COLLABORATOR

PR comments:
user: login: marcjulian
created_at: 2025-04-02T09:09:06Z
body: @ashley-hunter as discussed in #652 I created separate `BrnCalendarMultiDirective`, `HlmCalendarMultiComponent` and `HlmDatePickerMultiComponent`. I like the separation of concerns here, it allowed me to easily added additional inputs to the multi directive for `minSelection`/`maxSelection`.
author_association: COLLABORATOR
user: login: ashley-hunter
created_at: 2025-04-02T21:26:14Z
body: Thank you very much for making the changes! Apologies I didn't get a chance to look at the changes today, hope to get some time tomorrow!
author_association: COLLABORATOR
user: login: marcjulian
created_at: 2025-04-03T07:47:59Z
body: Take your time 😄 there is no rush
author_association: COLLABORATOR