{
  "name": "Calendar",
  "category": "Data Display",
  "subcategory": "Lists & Trees",
  "description": "Date picker calendar grid with single, range, and multi-select modes.",
  "thumbnail": "/preview-cards/calendar.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo",
  "files": [
    "UI/Calendar/Calendar.razor"
  ],
  "dependencies": [],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-accent",
    "--color-accent-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--color-primary",
    "--color-primary-foreground",
    "--radius"
  ],
  "api": {
    "name": "Calendar",
    "category": "Data Display",
    "subcategory": "Lists & Trees",
    "description": "Date picker calendar grid with single, range, and multi-select modes.",
    "nugetPackage": "Lumeo",
    "files": [
      "UI/Calendar/Calendar.razor"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "Value",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ValueChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MinDate",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "MaxDate",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsRange",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeStart",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeStartChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeEnd",
        "type": "DateOnly?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "RangeEndChanged",
        "type": "EventCallback<DateOnly?>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "NumberOfMonths",
        "type": "int",
        "default": "1",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowWeekNumbers",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "ShowYearPicker",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "InitialView",
        "type": "CalendarView",
        "default": "CalendarView.Days",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "IsDateDisabled",
        "type": "Func<DateTime, bool>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateClass",
        "type": "Func<DateTime, string>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateTooltip",
        "type": "Func<DateTime, string?>?",
        "default": null,
        "description": "Optional per-date tooltip text. Returned string becomes the day cell's title attribute (native browser tooltip on hover). Return null or empty for no tooltip on that date. Useful for booking calendars: \"Booked by John, 14:00–17:00\" or \"Sold out\".",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "DateBadge",
        "type": "Func<DateTime, RenderFragment?>?",
        "default": null,
        "description": "Optional per-date indicator slot. Returns a RenderFragment rendered absolutely positioned in the bottom-right of the day cell — good for status dots, count badges, icons. Return null for no badge on that date. The slot is non-interactive (pointer-events-none) so clicks still hit the day button.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FirstDayOfWeek",
        "type": "DayOfWeek?",
        "default": null,
        "description": "The day of the week to render in the leftmost column. When null (default), uses CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek — so de-DE / fr-FR / etc. start on Monday automatically, en-US on Sunday. Pass an explicit DayOfWeek to override.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SwipeEnabled",
        "type": "bool",
        "default": "true",
        "description": "When true (default), users can swipe left/right on the day grid to navigate to the next or previous month on touch devices. Vertical page scroll is unaffected (touch-action: pan-y). Set to false to disable swipe navigation.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Class",
        "type": "string?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "AdditionalAttributes",
        "type": "Dictionary<string, object>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": true
      }
    ],
    "events": [
      {
        "name": "ValueChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      },
      {
        "name": "RangeStartChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      },
      {
        "name": "RangeEndChanged",
        "type": "EventCallback<DateOnly?>",
        "description": null
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-accent",
      "--color-accent-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--color-primary",
      "--color-primary-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Default",
        "code": "<Calendar @bind-Value=\"_selectedDate\" />"
      },
      {
        "title": "Date Range Display",
        "code": "<Stack Gap=\"1.5\">\n    <Label>Check-in</Label>\n    <div class=\"rounded-md border p-1\">\n        <Calendar @bind-Value=\"_checkIn\" />\n    </div>\n</Stack>\n@if (_checkIn.HasValue)\n{\n    <Flex Align=\"center\" Gap=\"2\" Class=\"p-3 rounded-md bg-muted/50 text-sm\">\n        <DynamicIcon Name=\"Calendar\" Class=\"h-4 w-4 text-muted-foreground\" />\n        <Lumeo.Text As=\"span\" Size=\"sm\">Check-in: <strong>@_checkIn.Value.ToString(\"MMM d, yyyy\")</strong></Lumeo.Text>\n    </Flex>\n}"
      },
      {
        "title": "Pre-selected Date",
        "code": "<Calendar @bind-Value=\"_date\" />\n\n@code {\n    private DateOnly? _date = DateOnly.FromDateTime(DateTime.Today);\n}"
      },
      {
        "title": "Min & Max Date",
        "code": "<Calendar @bind-Value=\"_date\" MinDate=\"@_minDate\" MaxDate=\"@_maxDate\" />\n\n@code {\n    private DateOnly? _date;\n    private DateOnly _minDate = DateOnly.FromDateTime(DateTime.Today.AddDays(-7));\n    private DateOnly _maxDate = DateOnly.FromDateTime(DateTime.Today.AddDays(30));\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "calendar",
  "docsUrl": "https://lumeo.nativ.sh/components/calendar",
  "sourceUrl": [
    {
      "file": "UI/Calendar/Calendar.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo/UI/Calendar/Calendar.razor"
    }
  ],
  "source": [
    {
      "path": "UI/Calendar/Calendar.razor",
      "content": "@namespace Lumeo\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n@inject Lumeo.Services.IComponentInteropService Interop\n@implements IAsyncDisposable\n\n<div class=\"@CssClass\" @attributes=\"AdditionalAttributes\">\n    @if (NumberOfMonths == 1)\n    {\n        @RenderMonth(DisplayDate)\n    }\n    else\n    {\n        <div class=\"flex gap-4\">\n            @for (var i = 0; i < NumberOfMonths; i++)\n            {\n                var monthDate = DisplayDate.AddMonths(i);\n                <div>\n                    @RenderMonth(monthDate, showPrev: i == 0, showNext: i == NumberOfMonths - 1)\n                </div>\n            }\n        </div>\n    }\n</div>\n\n@code {\n    [Parameter] public DateOnly? Value { get; set; }\n    [Parameter] public EventCallback<DateOnly?> ValueChanged { get; set; }\n    [Parameter] public DateOnly? MinDate { get; set; }\n    [Parameter] public DateOnly? MaxDate { get; set; }\n    [Parameter] public bool IsRange { get; set; }\n    [Parameter] public DateOnly? RangeStart { get; set; }\n    [Parameter] public EventCallback<DateOnly?> RangeStartChanged { get; set; }\n    [Parameter] public DateOnly? RangeEnd { get; set; }\n    [Parameter] public EventCallback<DateOnly?> RangeEndChanged { get; set; }\n    [Parameter] public int NumberOfMonths { get; set; } = 1;\n    [Parameter] public bool ShowWeekNumbers { get; set; }\n    [Parameter] public bool ShowYearPicker { get; set; }\n    [Parameter] public CalendarView InitialView { get; set; } = CalendarView.Days;\n    [Parameter] public Func<DateTime, bool>? IsDateDisabled { get; set; }\n    [Parameter] public Func<DateTime, string>? DateClass { get; set; }\n    /// <summary>Optional per-date tooltip text. Returned string becomes the\n    /// day cell's <c>title</c> attribute (native browser tooltip on hover).\n    /// Return <c>null</c> or empty for no tooltip on that date. Useful for\n    /// booking calendars: \"Booked by John, 14:00–17:00\" or \"Sold out\".</summary>\n    [Parameter] public Func<DateTime, string?>? DateTooltip { get; set; }\n    /// <summary>Optional per-date indicator slot. Returns a <c>RenderFragment</c>\n    /// rendered absolutely positioned in the bottom-right of the day cell —\n    /// good for status dots, count badges, icons. Return <c>null</c> for no\n    /// badge on that date. The slot is non-interactive\n    /// (<c>pointer-events-none</c>) so clicks still hit the day button.</summary>\n    [Parameter] public Func<DateTime, RenderFragment?>? DateBadge { get; set; }\n    /// <summary>The day of the week to render in the leftmost column.\n    /// When <c>null</c> (default), uses\n    /// <c>CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek</c> —\n    /// so de-DE / fr-FR / etc. start on Monday automatically, en-US on\n    /// Sunday. Pass an explicit <see cref=\"DayOfWeek\"/> to override.</summary>\n    [Parameter] public DayOfWeek? FirstDayOfWeek { get; set; }\n    /// <summary>When <c>true</c> (default), users can swipe left/right on the\n    /// day grid to navigate to the next or previous month on touch devices.\n    /// Vertical page scroll is unaffected (<c>touch-action: pan-y</c>).\n    /// Set to <c>false</c> to disable swipe navigation.</summary>\n    [Parameter] public bool SwipeEnabled { get; set; } = true;\n\n    private readonly string _elementId = $\"cal-grid-{Guid.NewGuid():N}\";\n\n    private DayOfWeek EffectiveFirstDayOfWeek =>\n        FirstDayOfWeek ?? System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek;\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private DateOnly DisplayDate { get; set; }\n    private DateOnly? _hoverDate;\n    private CalendarView _currentView = CalendarView.Days;\n    private bool _swipeAttached;\n\n    private string CssClass => $\"p-3 {Class}\".Trim();\n\n    protected override void OnInitialized()\n    {\n        _currentView = InitialView;\n        DisplayDate = (IsRange ? RangeStart : Value) ?? DateOnly.FromDateTime(DateTime.Today);\n    }\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (!SwipeEnabled || NumberOfMonths != 1) return;\n\n        // Attach swipe only in Days view — the day grid element only exists in\n        // that view. When the user switches to Months/Years and back, the grid\n        // div is recreated so we must re-register on each Days → hidden → Days\n        // transition rather than once on first render.\n        var shouldAttach = _currentView == CalendarView.Days;\n        if (shouldAttach && !_swipeAttached)\n        {\n            _swipeAttached = true;\n            try { await Interop.RegisterHorizontalSwipe(_elementId, OnSwipeAsync); }\n            catch (JSDisconnectedException) { _swipeAttached = false; }\n            catch (InvalidOperationException) { _swipeAttached = false; } // JS unavailable during prerender\n        }\n        else if (!shouldAttach && _swipeAttached)\n        {\n            _swipeAttached = false;\n            try { await Interop.UnregisterHorizontalSwipe(_elementId); }\n            catch (JSDisconnectedException) { }\n        }\n    }\n\n    private Task OnSwipeAsync(string direction)\n    {\n        if (_currentView != CalendarView.Days) return Task.CompletedTask;\n        if (direction == \"next\" && !IsNextMonthDisabled)\n            NextMonth();\n        else if (direction == \"prev\" && !IsPrevMonthDisabled)\n            PreviousMonth();\n        StateHasChanged();\n        return Task.CompletedTask;\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (!_swipeAttached) return;\n        try { await Interop.UnregisterHorizontalSwipe(_elementId); }\n        catch (Microsoft.JSInterop.JSDisconnectedException) { }\n    }\n\n    private bool IsPrevMonthDisabled =>\n        MinDate.HasValue && new DateOnly(DisplayDate.Year, DisplayDate.Month, 1) <= new DateOnly(MinDate.Value.Year, MinDate.Value.Month, 1);\n\n    private bool IsNextMonthDisabled\n    {\n        get\n        {\n            var lastPanel = DisplayDate.AddMonths(NumberOfMonths - 1);\n            return MaxDate.HasValue && new DateOnly(lastPanel.Year, lastPanel.Month, 1) >= new DateOnly(MaxDate.Value.Year, MaxDate.Value.Month, 1);\n        }\n    }\n\n    private bool IsDisabled(DateOnly day) =>\n        (MinDate.HasValue && day < MinDate.Value) || (MaxDate.HasValue && day > MaxDate.Value)\n        || (IsDateDisabled != null && IsDateDisabled(day.ToDateTime(TimeOnly.MinValue)));\n\n    private string[] GetDayHeaders()\n    {\n        // .NET's ShortestDayNames collapses to 1 letter per culture\n        // (\"S,M,T,W,T,F,S\"), which is ambiguous in any language. Use\n        // AbbreviatedDayNames and take the first two letters of each so we\n        // get culture-correct, distinguishable headers:\n        //   en-US → Su/Mo/Tu/We/Th/Fr/Sa\n        //   de-DE → So/Mo/Di/Mi/Do/Fr/Sa\n        //   fr-FR → di/lu/ma/me/je/ve/sa\n        var abbr = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.AbbreviatedDayNames;\n        var startIndex = (int)EffectiveFirstDayOfWeek;\n        var result = new string[7];\n        for (var i = 0; i < 7; i++)\n        {\n            var raw = abbr[(startIndex + i) % 7];\n            // Strip trailing punctuation (de-DE returns \"So.\", \"Mo.\" …) before slicing.\n            var letters = new string(raw.TakeWhile(char.IsLetter).ToArray());\n            result[i] = letters.Length >= 2 ? letters[..2] : letters;\n        }\n        return result;\n    }\n\n    private List<DateOnly> GetCalendarDays(DateOnly monthDate)\n    {\n        var days = new List<DateOnly>();\n        var firstOfMonth = new DateOnly(monthDate.Year, monthDate.Month, 1);\n        var startOffset = ((int)firstOfMonth.DayOfWeek - (int)EffectiveFirstDayOfWeek + 7) % 7;\n        var start = firstOfMonth.AddDays(-startOffset);\n\n        for (var i = 0; i < 42; i++)\n        {\n            days.Add(start.AddDays(i));\n        }\n        return days;\n    }\n\n    private int GetIsoWeekNumber(DateOnly date)\n    {\n        var dt = date.ToDateTime(TimeOnly.MinValue);\n        var day = System.Globalization.CultureInfo.InvariantCulture.Calendar.GetDayOfWeek(dt);\n        if (day >= DayOfWeek.Monday && day <= DayOfWeek.Wednesday)\n        {\n            dt = dt.AddDays(3);\n        }\n        return System.Globalization.CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(dt,\n            System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);\n    }\n\n    private int GetDecadeStart(int year) => year / 10 * 10;\n\n    private string DayClasses(DateOnly day, DateOnly monthDate)\n    {\n        var isCurrentMonth = day.Month == monthDate.Month;\n        var isDisabled = IsDisabled(day);\n        var isSelected = !IsRange && Value.HasValue && day == Value.Value;\n        var isToday = day == DateOnly.FromDateTime(DateTime.Today);\n\n        // 'relative' kept on every day so DateBadge slots can absolute-position inside.\n        var baseClass = \"relative inline-flex items-center justify-center rounded-md h-8 w-8 text-sm transition-colors\";\n        var extraClass = DateClass != null ? DateClass(day.ToDateTime(TimeOnly.MinValue)) : \"\";\n\n        if (isDisabled)\n            return $\"{baseClass} pointer-events-none opacity-50 {extraClass}\".Trim();\n\n        if (IsRange)\n        {\n            var isRangeStart = RangeStart.HasValue && day == RangeStart.Value;\n            var isRangeEnd = RangeEnd.HasValue && day == RangeEnd.Value;\n\n            var effectiveEnd = RangeEnd ?? (RangeStart.HasValue && _hoverDate.HasValue ? _hoverDate : null);\n            var rangeFrom = RangeStart;\n            var rangeTo = effectiveEnd;\n            if (rangeFrom.HasValue && rangeTo.HasValue && rangeTo < rangeFrom)\n            {\n                (rangeFrom, rangeTo) = (rangeTo, rangeFrom);\n            }\n\n            var inRange = rangeFrom.HasValue && rangeTo.HasValue && day > rangeFrom.Value && day < rangeTo.Value;\n\n            if (isRangeStart || isRangeEnd)\n                return $\"{baseClass} bg-primary text-primary-foreground font-medium {extraClass}\".Trim();\n            if (inRange)\n                return $\"{baseClass} bg-accent text-accent-foreground {extraClass}\".Trim();\n            if (isToday)\n                return $\"{baseClass} bg-accent text-accent-foreground font-medium {extraClass}\".Trim();\n            if (!isCurrentMonth)\n                return $\"{baseClass} text-muted-foreground/50 hover:bg-accent hover:text-accent-foreground {extraClass}\".Trim();\n            return $\"{baseClass} hover:bg-accent hover:text-accent-foreground {extraClass}\".Trim();\n        }\n\n        if (isSelected)\n            return $\"{baseClass} bg-primary text-primary-foreground font-medium {extraClass}\".Trim();\n        if (isToday)\n            return $\"{baseClass} bg-accent text-accent-foreground font-medium {extraClass}\".Trim();\n        if (!isCurrentMonth)\n            return $\"{baseClass} text-muted-foreground/50 hover:bg-accent hover:text-accent-foreground {extraClass}\".Trim();\n        return $\"{baseClass} hover:bg-accent hover:text-accent-foreground {extraClass}\".Trim();\n    }\n\n    private async Task SelectDay(DateOnly day)\n    {\n        if (IsDisabled(day)) return;\n\n        if (IsRange)\n        {\n            if (!RangeStart.HasValue || (RangeStart.HasValue && RangeEnd.HasValue))\n            {\n                await RangeStartChanged.InvokeAsync(day);\n                await RangeEndChanged.InvokeAsync(null);\n                _hoverDate = null;\n            }\n            else\n            {\n                var start = RangeStart.Value;\n                if (day < start)\n                {\n                    await RangeStartChanged.InvokeAsync(day);\n                    await RangeEndChanged.InvokeAsync(start);\n                }\n                else\n                {\n                    await RangeEndChanged.InvokeAsync(day);\n                }\n                _hoverDate = null;\n            }\n            DisplayDate = new DateOnly(day.Year, day.Month, 1);\n        }\n        else\n        {\n            DisplayDate = new DateOnly(day.Year, day.Month, 1);\n            Value = day;\n            await ValueChanged.InvokeAsync(day);\n        }\n    }\n\n    private void OnDayHover(DateOnly day)\n    {\n        if (IsRange && RangeStart.HasValue && !RangeEnd.HasValue)\n        {\n            _hoverDate = day;\n        }\n    }\n\n    private void PreviousMonth() => DisplayDate = DisplayDate.AddMonths(-1);\n    private void NextMonth() => DisplayDate = DisplayDate.AddMonths(1);\n\n    private void SwitchToMonthView() => _currentView = CalendarView.Months;\n    private void SwitchToYearView() => _currentView = CalendarView.Years;\n\n    private void SelectMonth(int month)\n    {\n        DisplayDate = new DateOnly(DisplayDate.Year, month, 1);\n        _currentView = CalendarView.Days;\n    }\n\n    private void SelectYear(int year)\n    {\n        DisplayDate = new DateOnly(year, DisplayDate.Month, 1);\n        _currentView = CalendarView.Months;\n    }\n\n    private void PreviousDecade()\n    {\n        var decadeStart = GetDecadeStart(DisplayDate.Year);\n        DisplayDate = new DateOnly(Math.Max(1, decadeStart - 10), DisplayDate.Month, 1);\n    }\n\n    private void NextDecade()\n    {\n        var decadeStart = GetDecadeStart(DisplayDate.Year);\n        DisplayDate = new DateOnly(Math.Min(9999, decadeStart + 10), DisplayDate.Month, 1);\n    }\n\n    private void PreviousYear()\n    {\n        DisplayDate = new DateOnly(Math.Max(1, DisplayDate.Year - 1), DisplayDate.Month, 1);\n    }\n\n    private void NextYear()\n    {\n        DisplayDate = new DateOnly(Math.Min(9999, DisplayDate.Year + 1), DisplayDate.Month, 1);\n    }\n\n    private RenderFragment RenderMonth(DateOnly monthDate, bool showPrev = true, bool showNext = true) => __builder =>\n    {\n        @* Fixed 224px content width matches the 7-column day grid (7 × w-8 = 224px),\n           so the Days, Months and Years views all render at the same outer width.\n           Without this the Months / Years 3-col grid (w-full cells) would collapse\n           to its natural width and the picker would visibly resize between views. *@\n        <div class=\"w-[224px] flex flex-col\">\n        <div class=\"flex items-center justify-between px-1 mb-2\">\n            @if (showPrev)\n            {\n                if (_currentView == CalendarView.Years)\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.PrevYear\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground\" @onclick=\"PreviousDecade\">\n                        <Blazicon Svg=\"Lucide.ChevronLeft\" class=\"h-4 w-4\" />\n                    </button>\n                }\n                else if (_currentView == CalendarView.Months)\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.PrevYear\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground\" @onclick=\"PreviousYear\">\n                        <Blazicon Svg=\"Lucide.ChevronLeft\" class=\"h-4 w-4\" />\n                    </button>\n                }\n                else\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.PrevMonth\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-30\" disabled=\"@IsPrevMonthDisabled\" @onclick=\"PreviousMonth\">\n                        <Blazicon Svg=\"Lucide.ChevronLeft\" class=\"h-4 w-4\" />\n                    </button>\n                }\n            }\n            else\n            {\n                <div class=\"w-11 sm:w-7\"></div>\n            }\n\n            @if (_currentView == CalendarView.Years)\n            {\n                var decadeStart = GetDecadeStart(monthDate.Year);\n                <span class=\"text-sm font-medium\">@decadeStart - @(decadeStart + 9)</span>\n            }\n            else if (_currentView == CalendarView.Months)\n            {\n                <button type=\"button\" class=\"text-sm font-medium hover:bg-accent hover:text-accent-foreground rounded-md px-2 py-1\" @onclick=\"SwitchToYearView\">\n                    @monthDate.Year\n                </button>\n            }\n            else\n            {\n                @* Single combined click target: 1st click → Months view, 2nd click\n                   (now in Months view, on the year header) → Years view. Cleaner\n                   than two adjacent buttons that look identical but route to\n                   different pickers. *@\n                <button type=\"button\" class=\"text-sm font-medium hover:bg-accent hover:text-accent-foreground rounded-md px-2 py-1\" @onclick=\"SwitchToMonthView\">\n                    @monthDate.ToString(\"MMMM\") @monthDate.Year\n                </button>\n            }\n\n            @if (showNext)\n            {\n                if (_currentView == CalendarView.Years)\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.NextYear\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground\" @onclick=\"NextDecade\">\n                        <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4\" />\n                    </button>\n                }\n                else if (_currentView == CalendarView.Months)\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.NextYear\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground\" @onclick=\"NextYear\">\n                        <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4\" />\n                    </button>\n                }\n                else\n                {\n                    <button type=\"button\" aria-label=\"@L[\"Calendar.NextMonth\"]\" class=\"inline-flex items-center justify-center rounded-md h-11 w-11 sm:h-7 sm:w-7 hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-30\" disabled=\"@IsNextMonthDisabled\" @onclick=\"NextMonth\">\n                        <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4\" />\n                    </button>\n                }\n            }\n            else\n            {\n                <div class=\"w-11 sm:w-7\"></div>\n            }\n        </div>\n\n        @if (_currentView == CalendarView.Years)\n        {\n            var decadeStart = GetDecadeStart(monthDate.Year);\n            <div class=\"grid grid-cols-3 gap-2 p-1\">\n                @for (var y = decadeStart; y < decadeStart + 12; y++)\n                {\n                    var year = y;\n                    var isCurrentYear = year == DateTime.Today.Year;\n                    var isSelectedYear = year == monthDate.Year;\n                    var yearClass = isSelectedYear\n                        ? \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm font-medium bg-primary text-primary-foreground\"\n                        : isCurrentYear\n                            ? \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm font-medium bg-accent text-accent-foreground\"\n                            : \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer\";\n                    <button type=\"button\" class=\"@yearClass\" @onclick=\"() => SelectYear(year)\">@year</button>\n                }\n            </div>\n        }\n        else if (_currentView == CalendarView.Months)\n        {\n            var monthNames = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.AbbreviatedMonthNames;\n            <div class=\"grid grid-cols-3 gap-2 p-1\">\n                @for (var m = 1; m <= 12; m++)\n                {\n                    var month = m;\n                    var isCurrentMonth = month == DateTime.Today.Month && monthDate.Year == DateTime.Today.Year;\n                    var isSelectedMonth = month == monthDate.Month;\n                    var monthClass = isSelectedMonth\n                        ? \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm font-medium bg-primary text-primary-foreground\"\n                        : isCurrentMonth\n                            ? \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm font-medium bg-accent text-accent-foreground\"\n                            : \"inline-flex items-center justify-center rounded-md h-9 w-full text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer\";\n                    <button type=\"button\" class=\"@monthClass\" @onclick=\"() => SelectMonth(month)\">@monthNames[month - 1]</button>\n                }\n            </div>\n        }\n        else\n        {\n            var gridCols = ShowWeekNumbers ? \"grid-cols-8\" : \"grid-cols-7\";\n            var gridId = (SwipeEnabled && NumberOfMonths == 1) ? _elementId : null;\n            <div id=\"@gridId\" class=\"grid @gridCols gap-0 text-center\">\n                @if (ShowWeekNumbers)\n                {\n                    <div class=\"text-xs font-medium text-muted-foreground py-1\">#</div>\n                }\n                @foreach (var day in GetDayHeaders())\n                {\n                    <div class=\"text-xs font-medium text-muted-foreground py-1\">@day</div>\n                }\n\n                @{ var calDays = GetCalendarDays(monthDate); }\n                @for (var row = 0; row < 6; row++)\n                {\n                    @if (ShowWeekNumbers)\n                    {\n                        var weekDate = calDays[row * 7];\n                        <div class=\"inline-flex items-center justify-center h-8 w-8 text-xs text-muted-foreground\">@GetIsoWeekNumber(weekDate)</div>\n                    }\n                    @for (var col = 0; col < 7; col++)\n                    {\n                        var d = calDays[row * 7 + col];\n                        var dt = d.ToDateTime(TimeOnly.MinValue);\n                        var tooltip = DateTooltip?.Invoke(dt);\n                        var badge = DateBadge?.Invoke(dt);\n                        <button type=\"button\" class=\"@DayClasses(d, monthDate)\" disabled=\"@IsDisabled(d)\" title=\"@tooltip\" @onclick=\"() => SelectDay(d)\" @onmouseenter=\"() => OnDayHover(d)\">\n                            @d.Day\n                            @if (badge is not null)\n                            {\n                                <span class=\"absolute -bottom-0.5 -right-0.5 pointer-events-none flex\">@badge</span>\n                            }\n                        </button>\n                    }\n                }\n            </div>\n        }\n        </div> @* close the fixed-width wrapper *@\n    };\n\n    public enum CalendarView\n    {\n        Days,\n        Months,\n        Years\n    }\n\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    },
    {
      "service": "IComponentInteropService",
      "namespace": "Lumeo.Services"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/Calendar/CalendarSwipeReregistrationTests.cs",
    "tests/Lumeo.Tests/Components/Calendar/CalendarTests.cs",
    "tests/Lumeo.Tests/Components/Command/CommandTests.cs",
    "tests/Lumeo.Tests/Components/DatePicker/DatePickerTests.cs",
    "tests/Lumeo.Tests/Components/DateTimePicker/DateTimePickerTests.cs",
    "tests/Lumeo.Tests/Helpers/TrackingInteropService.cs"
  ],
  "mdSummary": "# Calendar\n\nDate picker calendar grid with single, range, and multi-select modes.\n\n**Package:** `Lumeo` · **Category:** Data Display/Lists & Trees\n**Docs:** https://lumeo.nativ.sh/components/calendar\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Calendar | Value | `DateOnly?` | `` |  |\n| Calendar | ValueChanged | `EventCallback<DateOnly?>` | `` |  |\n| Calendar | MinDate | `DateOnly?` | `` |  |\n| Calendar | MaxDate | `DateOnly?` | `` |  |\n| Calendar | IsRange | `bool` | `` |  |\n| Calendar | RangeStart | `DateOnly?` | `` |  |\n| Calendar | RangeStartChanged | `EventCallback<DateOnly?>` | `` |  |\n| Calendar | RangeEnd | `DateOnly?` | `` |  |\n| Calendar | RangeEndChanged | `EventCallback<DateOnly?>` | `` |  |\n| Calendar | NumberOfMonths | `int` | `1` |  |\n| Calendar | ShowWeekNumbers | `bool` | `` |  |\n| Calendar | ShowYearPicker | `bool` | `` |  |\n| Calendar | InitialView | `CalendarView` | `CalendarView.Days` |  |\n| Calendar | IsDateDisabled | `Func<DateTime, bool>?` | `` |  |\n| Calendar | DateClass | `Func<DateTime, string>?` | `` |  |\n| Calendar | DateTooltip | `Func<DateTime, string?>?` | `` | Optional per-date tooltip text. Returned string becomes the day cell's title attribute (native browser tooltip on hover). Return null or empty for no tooltip on that date. Useful for booking calendars: \"Booked by John, 14:00–17:00\" or \"Sold out\". |\n| Calendar | DateBadge | `Func<DateTime, RenderFragment?>?` | `` | Optional per-date indicator slot. Returns a RenderFragment rendered absolutely positioned in the bottom-right of the day cell — good for status dots, count badges, icons. Return null for no badge on that date. The slot is non-interactive (pointer-events-none) so clicks still hit the day button. |\n| Calendar | FirstDayOfWeek | `DayOfWeek?` | `` | The day of the week to render in the leftmost column. When null (default), uses CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek — so de-DE / fr-FR / etc. start on Monday automatically, en-US on Sunday. Pass an explicit DayOfWeek to override. |\n| Calendar | SwipeEnabled | `bool` | `true` | When true (default), users can swipe left/right on the day grid to navigate to the next or previous month on touch devices. Vertical page scroll is unaffected (touch-action: pan-y). Set to false to disable swipe navigation. |\n| Calendar | Class | `string?` | `` |  |\n| Calendar | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `ValueChanged` (`EventCallback<DateOnly?>`)\n- `RangeStartChanged` (`EventCallback<DateOnly?>`)\n- `RangeEndChanged` (`EventCallback<DateOnly?>`)\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n- `IComponentInteropService` (Lumeo.Services)\n\n## Keyboard\n—\n\n## Example\n```razor\n<Calendar />\n```\n\n## CSS variables\n`--color-accent`, `--color-accent-foreground`, `--color-muted`, `--color-muted-foreground`, `--color-primary`, `--color-primary-foreground`, `--radius`\n\n## Files\n- `UI/Calendar/Calendar.razor`\n"
}