{
  "name": "Scheduler",
  "category": "Data Display",
  "subcategory": "Lists & Trees",
  "description": "Calendar/agenda scheduler wrapping FullCalendar.",
  "thumbnail": "/preview-cards/scheduler.png",
  "hasDocsPage": true,
  "nugetPackage": "Lumeo.Scheduler",
  "files": [
    "UI/Scheduler/Scheduler.razor",
    "UI/Scheduler/SchedulerTypes.cs"
  ],
  "dependencies": [
    "button",
    "toggle-group"
  ],
  "packageDependencies": [
    "Blazicons.Lucide"
  ],
  "cssVars": [
    "--color-border",
    "--color-card",
    "--color-destructive",
    "--color-foreground",
    "--color-muted",
    "--color-muted-foreground",
    "--radius"
  ],
  "api": {
    "name": "Scheduler",
    "category": "Data Display",
    "subcategory": "Lists & Trees",
    "description": "Calendar/agenda scheduler wrapping FullCalendar.",
    "nugetPackage": "Lumeo.Scheduler",
    "files": [
      "UI/Scheduler/Scheduler.razor",
      "UI/Scheduler/SchedulerTypes.cs"
    ],
    "namespace": "Lumeo",
    "inheritsFrom": null,
    "implements": [
      "IAsyncDisposable"
    ],
    "parameters": [
      {
        "name": "Events",
        "type": "IEnumerable<SchedulerEvent>?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "EventsChanged",
        "type": "EventCallback<IEnumerable<SchedulerEvent>>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "InitialView",
        "type": "SchedulerView",
        "default": "SchedulerView.Month",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "InitialDate",
        "type": "DateTime?",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Editable",
        "type": "bool",
        "default": "true",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Selectable",
        "type": "bool",
        "default": "true",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "BusinessHours",
        "type": "bool",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Height",
        "type": "string",
        "default": "\"640px\"",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "FirstDayOfWeek",
        "type": "int",
        "default": "1",
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnEventClick",
        "type": "EventCallback<SchedulerEvent>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnDateSelect",
        "type": "EventCallback<SchedulerDateRange>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnEventChange",
        "type": "EventCallback<SchedulerEvent>",
        "default": null,
        "description": null,
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "NowIndicator",
        "type": "bool",
        "default": "true",
        "description": "Show the current-time indicator line in time-grid views. Default: true. Maps to FullCalendar's nowIndicator option.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SlotMinTime",
        "type": "TimeOnly?",
        "default": null,
        "description": "Earliest visible time slot in time-grid views (e.g. 07:00). Maps to FullCalendar's slotMinTime. Default: 00:00 (FullCalendar shows the full day).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SlotMaxTime",
        "type": "TimeOnly?",
        "default": null,
        "description": "Latest visible time slot in time-grid views (e.g. 22:00). Maps to FullCalendar's slotMaxTime. Default: 24:00 (FullCalendar shows the full day).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "SlotDuration",
        "type": "TimeSpan?",
        "default": null,
        "description": "Duration of each time slot row in time-grid views. Maps to FullCalendar's slotDuration (formatted as HH:mm:ss). Default: 00:30:00.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "Resources",
        "type": "IReadOnlyList<SchedulerResource>?",
        "default": null,
        "description": "Optional list of resources (people, rooms, equipment). When provided, events are color-coded by their ResourceId and a legend is rendered above the calendar. Note: this uses Lumeo's color-coding fallback, NOT FullCalendar Premium resource lanes (which require a paid FullCalendar Premium license not bundled here).",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "EventClassNames",
        "type": "Func<SchedulerEvent, string>?",
        "default": null,
        "description": "Optional callback that returns extra CSS class names for an event chip. The classes are appended to the FullCalendar event's classNames. Use for conditional styling without touching Color.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "EventColor",
        "type": "Func<SchedulerEvent, string>?",
        "default": null,
        "description": "Optional callback that returns a CSS color string for an event. Applied as backgroundColor / borderColor in FullCalendar. Supersedes Color when both are set; resource color applies as a fallback when neither is set.",
        "isCascading": false,
        "captureUnmatched": false
      },
      {
        "name": "OnInitError",
        "type": "EventCallback<string>",
        "default": null,
        "description": "Raised when JS initialization fails (e.g. CDN down, FullCalendar threw). The argument is the error message. When raised the component renders an error card instead of the calendar.",
        "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": "EventsChanged",
        "type": "EventCallback<IEnumerable<SchedulerEvent>>",
        "description": null
      },
      {
        "name": "OnEventClick",
        "type": "EventCallback<SchedulerEvent>",
        "description": null
      },
      {
        "name": "OnDateSelect",
        "type": "EventCallback<SchedulerDateRange>",
        "description": null
      },
      {
        "name": "OnEventChange",
        "type": "EventCallback<SchedulerEvent>",
        "description": null
      },
      {
        "name": "OnInitError",
        "type": "EventCallback<string>",
        "description": "Raised when JS initialization fails (e.g. CDN down, FullCalendar threw). The argument is the error message. When raised the component renders an error card instead of the calendar."
      }
    ],
    "enums": [],
    "records": [],
    "cssVars": [
      "--color-border",
      "--color-card",
      "--color-destructive",
      "--color-foreground",
      "--color-muted",
      "--color-muted-foreground",
      "--radius"
    ],
    "examples": [
      {
        "title": "Month View (basic)",
        "code": "<Scheduler Events=\"_events\" InitialView=\"SchedulerView.Month\" Height=\"620px\" />"
      },
      {
        "title": "Week View (timed + all-day)",
        "code": "<Scheduler Events=\"_events\"\n           InitialView=\"SchedulerView.Week\"\n           Height=\"620px\"\n           BusinessHours=\"true\" />"
      },
      {
        "title": "Click to Create",
        "code": "<Scheduler Events=\"_events\"\n           InitialView=\"SchedulerView.Week\"\n           OnDateSelect=\"HandleDateSelect\" />\n\n<Dialog @bind-Open=\"_open\">\n    <DialogContent>\n        <DialogHeader><DialogTitle>New event</DialogTitle></DialogHeader>\n        <Input @bind-Value=\"_title\" placeholder=\"Title\" />\n        <DialogFooter>\n            <Button OnClick=\"Add\">Add event</Button>\n        </DialogFooter>\n    </DialogContent>\n</Dialog>\n\n@code {\n    private bool _open;\n    private SchedulerDateRange? _range;\n    private string _title = \"\";\n    private List<SchedulerEvent> _events = new();\n\n    void HandleDateSelect(SchedulerDateRange r) { _range = r; _open = true; }\n    void Add() {\n        _events = _events.Append(new SchedulerEvent(\n            Id: Guid.NewGuid().ToString(\"N\"),\n            Title: _title,\n            Start: _range!.Start,\n            End: _range.End,\n            AllDay: _range.AllDay\n        )).ToList();\n        _open = false;\n    }\n}"
      },
      {
        "title": "Drag to Reschedule",
        "code": "<Scheduler @bind-Events=\"_events\" InitialView=\"SchedulerView.Week\" />\n\n@code {\n    private IEnumerable<SchedulerEvent> _events = new List<SchedulerEvent> { /* ... */ };\n}"
      }
    ],
    "subComponents": {},
    "parseFailed": false,
    "parseError": null
  },
  "$schema": "https://lumeo.nativ.sh/registry-component-schema.json",
  "key": "scheduler",
  "docsUrl": "https://lumeo.nativ.sh/components/scheduler",
  "sourceUrl": [
    {
      "file": "UI/Scheduler/Scheduler.razor",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo.Scheduler/UI/Scheduler/Scheduler.razor"
    },
    {
      "file": "UI/Scheduler/SchedulerTypes.cs",
      "url": "https://github.com/Brain2k-0005/Lumeo/tree/master/src/Lumeo.Scheduler/UI/Scheduler/SchedulerTypes.cs"
    }
  ],
  "source": [
    {
      "path": "UI/Scheduler/Scheduler.razor",
      "content": "@namespace Lumeo\n@implements IAsyncDisposable\n@inject Lumeo.Services.IComponentInteropService Interop\n@inject Lumeo.Services.Localization.ILumeoLocalizer L\n\n@if (_initError is not null)\n{\n    <div class=\"@($\"flex items-center justify-center text-sm text-destructive border border-destructive/30 rounded-md bg-destructive/5 {Class}\".Trim())\" style=\"height: @Height;\" @attributes=\"AdditionalAttributes\">\n        <span>@_initError</span>\n    </div>\n}\nelse\n{\n<div class=\"@($\"flex flex-col gap-3 rounded-md border border-border/60 bg-card p-3 {Class}\".Trim())\" @attributes=\"AdditionalAttributes\">\n    @* Lumeo-styled toolbar above the FullCalendar canvas.\n       FullCalendar's built-in header is hidden in JS (headerToolbar: false). *@\n    <div class=\"flex flex-wrap items-center justify-between gap-2\">\n        <div class=\"flex items-center gap-1\">\n            <Button Variant=\"Button.ButtonVariant.Outline\" Size=\"Button.ButtonSize.Sm\" OnClick=\"PrevAsync\" aria-label=\"Previous\">\n                <Blazicon Svg=\"Lucide.ChevronLeft\" class=\"h-4 w-4\" />\n            </Button>\n            <Button Variant=\"Button.ButtonVariant.Outline\" Size=\"Button.ButtonSize.Sm\" OnClick=\"TodayAsync\">@L[\"Scheduler.Today\"]</Button>\n            <Button Variant=\"Button.ButtonVariant.Outline\" Size=\"Button.ButtonSize.Sm\" OnClick=\"NextAsync\" aria-label=\"Next\">\n                <Blazicon Svg=\"Lucide.ChevronRight\" class=\"h-4 w-4\" />\n            </Button>\n        </div>\n        <div class=\"flex-1 text-center text-sm font-medium text-foreground truncate px-2\">\n            @_title\n        </div>\n        <ToggleGroup Type=\"ToggleGroup.ToggleGroupType.Single\"\n                     Variant=\"ToggleGroup.ToggleGroupVariant.Outline\"\n                     Size=\"ToggleGroup.ToggleGroupSize.Sm\"\n                     Value=\"@_currentViewValue\"\n                     ValueChanged=\"OnViewChangedAsync\">\n            <ToggleGroupItem Value=\"Month\">@L[\"Scheduler.Month\"]</ToggleGroupItem>\n            <ToggleGroupItem Value=\"Week\">@L[\"Scheduler.Week\"]</ToggleGroupItem>\n            <ToggleGroupItem Value=\"Day\">@L[\"Scheduler.Day\"]</ToggleGroupItem>\n            <ToggleGroupItem Value=\"List\">@L[\"Scheduler.List\"]</ToggleGroupItem>\n        </ToggleGroup>\n    </div>\n\n    @* Resource legend — rendered when Resources are provided.\n       Uses simple color-coded dots; no FullCalendar Premium plugin required. *@\n    @if (_resolvedResources is { Count: > 0 })\n    {\n        <div class=\"flex flex-wrap items-center gap-x-4 gap-y-1 px-1 pb-1\" aria-label=\"Resource legend\">\n            @foreach (var resource in _resolvedResources)\n            {\n                <div class=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                    <span class=\"inline-block h-2.5 w-2.5 rounded-full flex-shrink-0\"\n                          style=\"background: @(resource.Color ?? \"var(--color-primary)\")\"></span>\n                    <span>@resource.Title</span>\n                </div>\n            }\n        </div>\n    }\n\n    <div @ref=\"_hostRef\" style=\"height: @Height;\"></div>\n</div>\n}\n\n@code {\n    // ── Core parameters ──────────────────────────────────────────────────\n    [Parameter] public IEnumerable<SchedulerEvent>? Events { get; set; }\n    [Parameter] public EventCallback<IEnumerable<SchedulerEvent>> EventsChanged { get; set; }\n    [Parameter] public SchedulerView InitialView { get; set; } = SchedulerView.Month;\n    [Parameter] public DateTime? InitialDate { get; set; }\n    [Parameter] public bool Editable { get; set; } = true;\n    [Parameter] public bool Selectable { get; set; } = true;\n    [Parameter] public bool BusinessHours { get; set; }\n    [Parameter] public string Height { get; set; } = \"640px\";\n    [Parameter] public int FirstDayOfWeek { get; set; } = 1;\n    [Parameter] public EventCallback<SchedulerEvent> OnEventClick { get; set; }\n    [Parameter] public EventCallback<SchedulerDateRange> OnDateSelect { get; set; }\n    [Parameter] public EventCallback<SchedulerEvent> OnEventChange { get; set; }\n\n    // ── Time-grid display parameters ─────────────────────────────────────\n    /// <summary>\n    /// Show the current-time indicator line in time-grid views. Default: true.\n    /// Maps to FullCalendar's <c>nowIndicator</c> option.\n    /// </summary>\n    [Parameter] public bool NowIndicator { get; set; } = true;\n\n    /// <summary>\n    /// Earliest visible time slot in time-grid views (e.g. 07:00).\n    /// Maps to FullCalendar's <c>slotMinTime</c>. Default: 00:00 (FullCalendar shows the full day).\n    /// </summary>\n    [Parameter] public TimeOnly? SlotMinTime { get; set; }\n\n    /// <summary>\n    /// Latest visible time slot in time-grid views (e.g. 22:00).\n    /// Maps to FullCalendar's <c>slotMaxTime</c>. Default: 24:00 (FullCalendar shows the full day).\n    /// </summary>\n    [Parameter] public TimeOnly? SlotMaxTime { get; set; }\n\n    /// <summary>\n    /// Duration of each time slot row in time-grid views.\n    /// Maps to FullCalendar's <c>slotDuration</c> (formatted as HH:mm:ss). Default: 00:30:00.\n    /// </summary>\n    [Parameter] public TimeSpan? SlotDuration { get; set; }\n\n    // ── Resource / color-coding parameters ──────────────────────────────\n    /// <summary>\n    /// Optional list of resources (people, rooms, equipment). When provided, events\n    /// are color-coded by their <see cref=\"SchedulerEvent.ResourceId\"/> and a legend\n    /// is rendered above the calendar.\n    /// <br />\n    /// Note: this uses Lumeo's color-coding fallback, NOT FullCalendar Premium resource\n    /// lanes (which require a paid FullCalendar Premium license not bundled here).\n    /// </summary>\n    [Parameter] public IReadOnlyList<SchedulerResource>? Resources { get; set; }\n\n    // ── Event styling callbacks ──────────────────────────────────────────\n    /// <summary>\n    /// Optional callback that returns extra CSS class names for an event chip.\n    /// The classes are appended to the FullCalendar event's <c>classNames</c>.\n    /// Use for conditional styling without touching <see cref=\"SchedulerEvent.Color\"/>.\n    /// </summary>\n    [Parameter] public Func<SchedulerEvent, string>? EventClassNames { get; set; }\n\n    /// <summary>\n    /// Optional callback that returns a CSS color string for an event.\n    /// Applied as <c>backgroundColor</c> / <c>borderColor</c> in FullCalendar.\n    /// Supersedes <see cref=\"SchedulerEvent.Color\"/> when both are set; resource\n    /// color applies as a fallback when neither is set.\n    /// </summary>\n    [Parameter] public Func<SchedulerEvent, string>? EventColor { get; set; }\n\n    // ── Error handling ───────────────────────────────────────────────────\n    /// <summary>\n    /// Raised when JS initialization fails (e.g. CDN down, FullCalendar threw).\n    /// The argument is the error message. When raised the component renders an\n    /// error card instead of the calendar.\n    /// </summary>\n    [Parameter] public EventCallback<string> OnInitError { get; set; }\n\n    // ── Standard pass-through ────────────────────────────────────────────\n    [Parameter] public string? Class { get; set; }\n    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }\n\n    private ElementReference _hostRef;\n    private DotNetObjectReference<Scheduler>? _selfRef;\n    private string? _instanceId;\n    private bool _initialized;\n    private string? _initError;\n    private int _lastEventsHash;\n    private SchedulerView _currentView;\n    private string _currentViewValue => _currentView.ToString();\n    private string _title = \"\";\n\n    // Snapshot of the most recent events set — used by eventChange to build the\n    // updated collection without reaching back into `Events` (which may have\n    // been replaced by the parent since init).\n    private List<SchedulerEvent> _events = new();\n\n    // Resolved resource lookup — built once and used for both legend rendering\n    // and event color-coding in ToJsEvent.\n    private List<SchedulerResource> _resolvedResources = new();\n    private Dictionary<string, SchedulerResource> _resourceById = new(StringComparer.Ordinal);\n\n    protected override void OnInitialized()\n    {\n        _currentView = InitialView;\n        _events = Events?.ToList() ?? new List<SchedulerEvent>();\n        _lastEventsHash = ComputeEventsHash(_events);\n        _title = ComputeTitle(InitialDate ?? DateTime.Today, _currentView);\n        RebuildResourceLookup();\n    }\n\n    protected override async Task OnParametersSetAsync()\n    {\n        // Rebuild the resource lookup whenever the Resources param changes.\n        RebuildResourceLookup();\n\n        if (_initialized && Events is not null)\n        {\n            var incoming = Events.ToList();\n            var hash = ComputeEventsHash(incoming);\n            if (hash != _lastEventsHash)\n            {\n                _lastEventsHash = hash;\n                _events = incoming;\n                await Interop.SchedulerSetEventsAsync(_instanceId!, incoming.Select(ToJsEvent).Cast<object>());\n            }\n        }\n    }\n\n    protected override async Task OnAfterRenderAsync(bool firstRender)\n    {\n        if (!firstRender || _initialized) return;\n\n        _selfRef = DotNetObjectReference.Create(this);\n\n        var options = new\n        {\n            view = InitialView.ToString(),\n            initialDate = (InitialDate ?? DateTime.Today).ToString(\"yyyy-MM-dd\"),\n            events = _events.Select(ToJsEvent).ToArray(),\n            editable = Editable,\n            selectable = Selectable,\n            businessHours = BusinessHours,\n            height = Height,\n            firstDay = FirstDayOfWeek,\n            nowIndicator = NowIndicator,\n            slotMinTime = SlotMinTime.HasValue ? SlotMinTime.Value.ToString(\"HH:mm:ss\") : (string?)null,\n            slotMaxTime = SlotMaxTime.HasValue ? SlotMaxTime.Value.ToString(\"HH:mm:ss\") : (string?)null,\n            slotDuration = SlotDuration.HasValue ? $\"{(int)SlotDuration.Value.TotalHours:D2}:{SlotDuration.Value.Minutes:D2}:{SlotDuration.Value.Seconds:D2}\" : (string?)null\n        };\n\n        try\n        {\n            _instanceId = await Interop.SchedulerInitAsync(_hostRef, _selfRef, options);\n            _initialized = true;\n            // Read the real title from FullCalendar now that the view is rendered.\n            _title = await Interop.SchedulerGetTitleAsync(_instanceId);\n            StateHasChanged();\n        }\n        catch (Microsoft.JSInterop.JSException ex)\n        {\n            _initError = ex.Message;\n            Console.Error.WriteLine($\"[Lumeo Scheduler] Init failed: {ex.Message}\");\n            if (OnInitError.HasDelegate)\n                await OnInitError.InvokeAsync(ex.Message);\n            StateHasChanged();\n        }\n    }\n\n    private object ToJsEvent(SchedulerEvent ev)\n    {\n        // Determine the effective color: EventColor callback > event.Color > resource color.\n        var effectiveColor = EventColor?.Invoke(ev)\n            ?? ev.Color\n            ?? (ev.ResourceId is not null && _resourceById.TryGetValue(ev.ResourceId, out var res) ? res.Color : null);\n\n        // Effective class names: EventClassNames callback merged with event.ClassNames.\n        var cbClasses = EventClassNames?.Invoke(ev);\n        var effectiveClassNames = string.IsNullOrEmpty(cbClasses)\n            ? ev.ClassNames\n            : string.IsNullOrEmpty(ev.ClassNames) ? cbClasses : $\"{ev.ClassNames} {cbClasses}\";\n\n        // For recurring events with DaysOfWeek we use FullCalendar's free simple\n        // recurrence model: the start/end fields become time-of-day values only.\n        if (ev.DaysOfWeek is { Count: > 0 })\n        {\n            return new\n            {\n                id = ev.Id,\n                title = ev.Title,\n                // Time-of-day string for recurring events (HH:mm:ss).\n                startTime = ev.Start.ToString(\"HH:mm:ss\"),\n                endTime = ev.End.ToString(\"HH:mm:ss\"),\n                daysOfWeek = ev.DaysOfWeek.Select(d => (int)d).ToArray(),\n                startRecur = ev.Start.ToString(\"yyyy-MM-dd\"),\n                endRecur = ev.RecurrenceEnd.HasValue\n                    ? ev.RecurrenceEnd.Value.ToString(\"yyyy-MM-dd\")\n                    : (string?)null,\n                exdate = ev.ExceptionDates?.Select(d => d.ToString(\"yyyy-MM-dd\")).ToArray(),\n                allDay = ev.AllDay,\n                color = effectiveColor,\n                url = ev.Url,\n                classNames = effectiveClassNames,\n                extendedProps = ev.ExtendedProps\n            };\n        }\n\n        // Standard (non-recurring) event.\n        return new\n        {\n            id = ev.Id,\n            title = ev.Title,\n            start = ev.Start.ToString(\"o\"),\n            end = ev.End.ToString(\"o\"),\n            allDay = ev.AllDay,\n            color = effectiveColor,\n            url = ev.Url,\n            classNames = effectiveClassNames,\n            extendedProps = ev.ExtendedProps\n        };\n    }\n\n    private void RebuildResourceLookup()\n    {\n        _resolvedResources = Resources?.ToList() ?? new List<SchedulerResource>();\n        _resourceById = _resolvedResources.ToDictionary(r => r.Id, StringComparer.Ordinal);\n    }\n\n    private static int ComputeEventsHash(IEnumerable<SchedulerEvent> events)\n    {\n        // Cheap identity hash good enough to detect \"did the caller hand us a\n        // new event list?\" without triggering a JS round-trip on every render.\n        var hash = new HashCode();\n        foreach (var ev in events)\n        {\n            hash.Add(ev.Id);\n            hash.Add(ev.Title);\n            hash.Add(ev.Start);\n            hash.Add(ev.End);\n            hash.Add(ev.AllDay);\n            hash.Add(ev.Color);\n            hash.Add(ev.ResourceId);\n            hash.Add(ev.DaysOfWeek?.Count ?? 0);\n            hash.Add(ev.RecurrenceEnd);\n        }\n        return hash.ToHashCode();\n    }\n\n    private string ComputeTitle(DateTime date, SchedulerView view) => view switch\n    {\n        SchedulerView.Month => date.ToString(\"MMMM yyyy\"),\n        SchedulerView.Week => string.Format(L[\"Scheduler.WeekOf\"], StartOfWeek(date).ToString(\"MMM d, yyyy\")),\n        SchedulerView.Day => date.ToString(\"dddd, MMM d, yyyy\"),\n        SchedulerView.List => date.ToString(\"MMMM yyyy\"),\n        _ => date.ToString(\"MMMM yyyy\")\n    };\n\n    private DateTime StartOfWeek(DateTime date)\n    {\n        // FirstDayOfWeek is 0-6 (Sun-Sat).\n        var first = (DayOfWeek)FirstDayOfWeek;\n        var diff = (7 + (date.DayOfWeek - first)) % 7;\n        return date.AddDays(-diff).Date;\n    }\n\n    private async Task PrevAsync()\n    {\n        if (_instanceId is null) return;\n        await Interop.SchedulerPrevAsync(_instanceId);\n        await RefreshTitleAsync();\n    }\n\n    private async Task NextAsync()\n    {\n        if (_instanceId is null) return;\n        await Interop.SchedulerNextAsync(_instanceId);\n        await RefreshTitleAsync();\n    }\n\n    private async Task TodayAsync()\n    {\n        if (_instanceId is null) return;\n        await Interop.SchedulerTodayAsync(_instanceId);\n        await RefreshTitleAsync();\n    }\n\n    private async Task OnViewChangedAsync(string? v)\n    {\n        if (_instanceId is null || string.IsNullOrEmpty(v)) return;\n        if (!Enum.TryParse<SchedulerView>(v, out var parsed)) return;\n        _currentView = parsed;\n        await Interop.SchedulerChangeViewAsync(_instanceId, v);\n        await RefreshTitleAsync();\n    }\n\n    private async Task RefreshTitleAsync()\n    {\n        if (_instanceId is null) return;\n        _title = await Interop.SchedulerGetTitleAsync(_instanceId);\n        StateHasChanged();\n    }\n\n    [JSInvokable]\n    public async Task JsOnEventClick(SchedulerEvent ev)\n    {\n        if (OnEventClick.HasDelegate) await OnEventClick.InvokeAsync(ev);\n    }\n\n    [JSInvokable]\n    public async Task JsOnDateSelect(SchedulerDateRange range)\n    {\n        if (OnDateSelect.HasDelegate) await OnDateSelect.InvokeAsync(range);\n    }\n\n    [JSInvokable]\n    public async Task JsOnEventChange(SchedulerEvent ev)\n    {\n        // Replace the matching event in our local snapshot, then bubble both\n        // the per-event change and the new collection up to the parent.\n        var idx = _events.FindIndex(e => e.Id == ev.Id);\n        if (idx >= 0) _events[idx] = ev;\n\n        _lastEventsHash = ComputeEventsHash(_events);\n\n        if (OnEventChange.HasDelegate) await OnEventChange.InvokeAsync(ev);\n        if (EventsChanged.HasDelegate) await EventsChanged.InvokeAsync(_events.ToList());\n    }\n\n    public async ValueTask DisposeAsync()\n    {\n        if (_instanceId is not null)\n        {\n            try { await Interop.SchedulerDestroyAsync(_instanceId); }\n            catch (Microsoft.JSInterop.JSDisconnectedException) { }\n        }\n        _selfRef?.Dispose();\n    }\n}\n"
    },
    {
      "path": "UI/Scheduler/SchedulerTypes.cs",
      "content": "namespace Lumeo;\n\n/// <summary>\n/// A calendar/scheduler event. Used as the data contract between Blazor and the\n/// FullCalendar JS wrapper.\n/// </summary>\n/// <param name=\"Id\">Stable identifier used to reconcile edits back into the caller's collection.</param>\n/// <param name=\"Title\">The label rendered on the event chip.</param>\n/// <param name=\"Start\">Start timestamp (inclusive).</param>\n/// <param name=\"End\">End timestamp (exclusive, per FullCalendar convention).</param>\n/// <param name=\"AllDay\">When true the event is rendered in the all-day lane.</param>\n/// <param name=\"Color\">CSS color or variable reference, e.g. \"var(--color-primary)\".</param>\n/// <param name=\"Url\">Optional link opened on click instead of firing OnEventClick.</param>\n/// <param name=\"ExtendedProps\">Arbitrary app-level metadata round-tripped through the JS layer.</param>\n/// <param name=\"DaysOfWeek\">\n/// For recurring events: the days of the week on which the event repeats.\n/// Uses FullCalendar's free simple recurrence model (no premium rrule plugin required).\n/// e.g. [DayOfWeek.Monday, DayOfWeek.Wednesday, DayOfWeek.Friday].\n/// When set, <see cref=\"Start\"/> and <see cref=\"End\"/> provide the start/end times\n/// (time-of-day part only) and <see cref=\"RecurrenceEnd\"/> bounds the recurrence.\n/// </param>\n/// <param name=\"RecurrenceEnd\">\n/// Optional end date for a recurring event. The event will not appear after this date.\n/// Only meaningful when <see cref=\"DaysOfWeek\"/> is set.\n/// </param>\n/// <param name=\"ExceptionDates\">\n/// Dates on which a recurring event should be skipped (one-off exclusions).\n/// Only meaningful when <see cref=\"DaysOfWeek\"/> is set.\n/// </param>\n/// <param name=\"ResourceId\">\n/// Optional resource identifier. When a <c>Resources</c> list is provided on the\n/// <see cref=\"Scheduler\"/> component, events are color-coded by their resource and\n/// a resource legend is rendered above the calendar.\n/// </param>\n/// <param name=\"ClassNames\">\n/// Extra CSS class names to apply to the event chip, as produced by the\n/// <c>EventClassNames</c> callback on the Scheduler component (or set directly here).\n/// </param>\npublic record SchedulerEvent(\n    string Id,\n    string Title,\n    DateTime Start,\n    DateTime End,\n    bool AllDay = false,\n    string? Color = null,\n    string? Url = null,\n    Dictionary<string, object>? ExtendedProps = null,\n    IReadOnlyList<DayOfWeek>? DaysOfWeek = null,\n    DateTime? RecurrenceEnd = null,\n    IReadOnlyList<DateTime>? ExceptionDates = null,\n    string? ResourceId = null,\n    string? ClassNames = null\n);\n\n/// <summary>\n/// A named resource (person, room, equipment) used by the Scheduler for\n/// color-coding events. Does not require the FullCalendar Premium resource plugin.\n/// </summary>\n/// <param name=\"Id\">Identifier matched against <see cref=\"SchedulerEvent.ResourceId\"/>.</param>\n/// <param name=\"Title\">Display name shown in the resource legend.</param>\n/// <param name=\"Color\">\n/// CSS color applied to events belonging to this resource when the event itself\n/// does not supply its own <see cref=\"SchedulerEvent.Color\"/>.\n/// </param>\npublic record SchedulerResource(string Id, string Title, string? Color = null);\n\n/// <summary>\n/// A date range produced when the user drag-selects in the calendar.\n/// </summary>\npublic record SchedulerDateRange(DateTime Start, DateTime End, bool AllDay);\n\n/// <summary>\n/// Built-in views exposed by the Lumeo scheduler. Maps onto FullCalendar's\n/// dayGridMonth / timeGridWeek / timeGridDay / listWeek view names.\n/// </summary>\npublic enum SchedulerView\n{\n    Month,\n    Week,\n    Day,\n    List\n}\n"
    }
  ],
  "slots": [],
  "serviceDependencies": [
    {
      "service": "IComponentInteropService",
      "namespace": "Lumeo.Services"
    },
    {
      "service": "ILumeoLocalizer",
      "namespace": "Lumeo.Services.Localization"
    }
  ],
  "cascadingDependencies": [],
  "relatedComponents": [
    {
      "name": "Button",
      "reason": "used internally"
    },
    {
      "name": "ToggleGroup",
      "reason": "used internally"
    }
  ],
  "keyboardInteractions": [],
  "tests": [
    "tests/Lumeo.Docs.Tests/Helpers/DocsTestContext.cs",
    "tests/Lumeo.Tests/Components/Scheduler/SchedulerTests.cs"
  ],
  "mdSummary": "# Scheduler\n\nCalendar/agenda scheduler wrapping FullCalendar.\n\n**Package:** `Lumeo.Scheduler` · **Category:** Data Display/Lists & Trees\n**Docs:** https://lumeo.nativ.sh/components/scheduler\n\n## Parameters\n| Owner | Name | Type | Default | Description |\n|---|---|---|---|---|\n| Scheduler | Events | `IEnumerable<SchedulerEvent>?` | `` |  |\n| Scheduler | EventsChanged | `EventCallback<IEnumerable<SchedulerEvent>>` | `` |  |\n| Scheduler | InitialView | `SchedulerView` | `SchedulerView.Month` |  |\n| Scheduler | InitialDate | `DateTime?` | `` |  |\n| Scheduler | Editable | `bool` | `true` |  |\n| Scheduler | Selectable | `bool` | `true` |  |\n| Scheduler | BusinessHours | `bool` | `` |  |\n| Scheduler | Height | `string` | `\"640px\"` |  |\n| Scheduler | FirstDayOfWeek | `int` | `1` |  |\n| Scheduler | OnEventClick | `EventCallback<SchedulerEvent>` | `` |  |\n| Scheduler | OnDateSelect | `EventCallback<SchedulerDateRange>` | `` |  |\n| Scheduler | OnEventChange | `EventCallback<SchedulerEvent>` | `` |  |\n| Scheduler | NowIndicator | `bool` | `true` | Show the current-time indicator line in time-grid views. Default: true. Maps to FullCalendar's nowIndicator option. |\n| Scheduler | SlotMinTime | `TimeOnly?` | `` | Earliest visible time slot in time-grid views (e.g. 07:00). Maps to FullCalendar's slotMinTime. Default: 00:00 (FullCalendar shows the full day). |\n| Scheduler | SlotMaxTime | `TimeOnly?` | `` | Latest visible time slot in time-grid views (e.g. 22:00). Maps to FullCalendar's slotMaxTime. Default: 24:00 (FullCalendar shows the full day). |\n| Scheduler | SlotDuration | `TimeSpan?` | `` | Duration of each time slot row in time-grid views. Maps to FullCalendar's slotDuration (formatted as HH:mm:ss). Default: 00:30:00. |\n| Scheduler | Resources | `IReadOnlyList<SchedulerResource>?` | `` | Optional list of resources (people, rooms, equipment). When provided, events are color-coded by their ResourceId and a legend is rendered above the calendar. Note: this uses Lumeo's color-coding fallback, NOT FullCalendar Premium resource lanes (which require a paid FullCalendar Premium license not bundled here). |\n| Scheduler | EventClassNames | `Func<SchedulerEvent, string>?` | `` | Optional callback that returns extra CSS class names for an event chip. The classes are appended to the FullCalendar event's classNames. Use for conditional styling without touching Color. |\n| Scheduler | EventColor | `Func<SchedulerEvent, string>?` | `` | Optional callback that returns a CSS color string for an event. Applied as backgroundColor / borderColor in FullCalendar. Supersedes Color when both are set; resource color applies as a fallback when neither is set. |\n| Scheduler | OnInitError | `EventCallback<string>` | `` | Raised when JS initialization fails (e.g. CDN down, FullCalendar threw). The argument is the error message. When raised the component renders an error card instead of the calendar. |\n| Scheduler | Class | `string?` | `` |  |\n| Scheduler | AdditionalAttributes | `Dictionary<string, object>?` | `` |  |\n\n## Slots\n- (none)\n\n## Events\n- `EventsChanged` (`EventCallback<IEnumerable<SchedulerEvent>>`)\n- `OnEventClick` (`EventCallback<SchedulerEvent>`)\n- `OnDateSelect` (`EventCallback<SchedulerDateRange>`)\n- `OnEventChange` (`EventCallback<SchedulerEvent>`)\n- `OnInitError` (`EventCallback<string>`) — Raised when JS initialization fails (e.g. CDN down, FullCalendar threw). The argument is the error message. When raised the component renders an error card instead of the calendar.\n\n## Sub-components\n- (none)\n\n## Cascading values it expects\n- (none)\n\n## Services it injects\n- `IComponentInteropService` (Lumeo.Services)\n- `ILumeoLocalizer` (Lumeo.Services.Localization)\n\n## Keyboard\n—\n\n## Example\n```razor\n<Scheduler />\n```\n\n## CSS variables\n`--color-border`, `--color-card`, `--color-destructive`, `--color-foreground`, `--color-muted`, `--color-muted-foreground`, `--radius`\n\n## Files\n- `UI/Scheduler/Scheduler.razor`\n- `UI/Scheduler/SchedulerTypes.cs`\n"
}