{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "gantt",
	"title": "Gantt",
	"type": "registry:ui",
	"description": "A responsive, navigable Gantt chart with resources, progress, and exclusive end dates.",
	"dependencies": [
		"@internationalized/date@^3.12.2",
		"@lucide/svelte@^1.28.0"
	],
	"registryDependencies": [
		"badge",
		"button",
		"calendar",
		"card",
		"popover",
		"select",
		"skeleton",
		"tooltip"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" generics=\"TData = unknown\">\n\timport { cn } from '$UTILS$.js';\n\n\timport type { GanttRenderRow } from './gantt-rows.js';\n\timport type { GanttTimeline } from './gantt-timeline.js';\n\timport type { GanttEvent, GanttScale } from './gantt-types.js';\n\n\timport GanttLoadingRows from './gantt-loading-rows.svelte';\n\timport GanttRow from './gantt-row.svelte';\n\n\tlet {\n\t\tcollapsed,\n\t\tisFullscreen,\n\t\tloading,\n\t\tlocale,\n\t\tnavigating,\n\t\toffDaySpans,\n\t\tonEventClick,\n\t\tonGoToDate,\n\t\tonScroll,\n\t\tonScrollbarDragEnd,\n\t\tonScrollbarDragStart,\n\t\tonToggleResource,\n\t\trows,\n\t\tscale,\n\t\tscrollbarHidden,\n\t\tscrollElement = $bindable(null),\n\t\ttimeline,\n\t\ttitle,\n\t\ttodayX,\n\t\ttreeWidth,\n\t\tviewportWidth = $bindable(0)\n\t}: {\n\t\tcollapsed: ReadonlySet<string>;\n\t\tisFullscreen: boolean;\n\t\tloading: boolean;\n\t\tlocale: string;\n\t\tnavigating: boolean;\n\t\toffDaySpans: Array<{ left: number; width: number }>;\n\t\tonEventClick?: (event: GanttEvent<TData>) => void;\n\t\tonGoToDate: (date: Date) => void;\n\t\tonScroll: () => void;\n\t\tonScrollbarDragEnd: () => void;\n\t\tonScrollbarDragStart: () => void;\n\t\tonToggleResource: (id: string) => void;\n\t\trows: GanttRenderRow<TData>[];\n\t\tscale: GanttScale;\n\t\tscrollbarHidden: boolean;\n\t\tscrollElement: HTMLDivElement | null;\n\t\ttimeline: GanttTimeline;\n\t\ttitle: string;\n\t\ttodayX: null | number;\n\t\ttreeWidth: number;\n\t\tviewportWidth: number;\n\t} = $props();\n\n\t/** Sub-pixel scroll offsets differ after a round trip through the DOM. */\n\tconst SCROLL_EPSILON = 1;\n\n\tlet horizontalScrollbar = $state<HTMLDivElement | null>(null);\n\tlet verticalScrollbar = $state<HTMLDivElement | null>(null);\n\t// Assigning scrollLeft aborts an in-flight smooth scroll, so a proxy scrollbar\n\t// must not echo a position the chart just pushed into it back onto the chart.\n\tlet mirroredLeft: null | number = null;\n\tlet mirroredTop: null | number = null;\n\tlet scrollBodyHeight = $derived(\n\t\tloading ? 7 * 48 : rows.reduce((height, renderRow) => height + renderRow.height, 0)\n\t);\n\n\tfunction isEcho(value: number, mirrored: null | number): boolean {\n\t\treturn mirrored !== null && Math.abs(value - mirrored) < SCROLL_EPSILON;\n\t}\n\n\tfunction handleChartScroll(): void {\n\t\tconst horizontal = horizontalScrollbar;\n\t\tconst vertical = verticalScrollbar;\n\t\tif (scrollElement && horizontal && horizontal.scrollLeft !== scrollElement.scrollLeft) {\n\t\t\thorizontal.scrollLeft = scrollElement.scrollLeft;\n\t\t\t// Read back so clamping and rounding are part of the remembered value.\n\t\t\tmirroredLeft = horizontal.scrollLeft;\n\t\t}\n\t\tif (scrollElement && vertical && vertical.scrollTop !== scrollElement.scrollTop) {\n\t\t\tvertical.scrollTop = scrollElement.scrollTop;\n\t\t\tmirroredTop = vertical.scrollTop;\n\t\t}\n\t\tonScroll();\n\t}\n\n\tfunction handleHorizontalScrollbarScroll(): void {\n\t\tif (!scrollElement || !horizontalScrollbar) return;\n\n\t\tconst { scrollLeft } = horizontalScrollbar;\n\t\tif (isEcho(scrollLeft, mirroredLeft)) {\n\t\t\tmirroredLeft = null;\n\t\t\treturn;\n\t\t}\n\t\tscrollElement.scrollLeft = scrollLeft;\n\t}\n\n\tfunction handleVerticalScrollbarScroll(): void {\n\t\tif (!scrollElement || !verticalScrollbar) return;\n\n\t\tconst { scrollTop } = verticalScrollbar;\n\t\tif (isEcho(scrollTop, mirroredTop)) {\n\t\t\tmirroredTop = null;\n\t\t\treturn;\n\t\t}\n\t\tscrollElement.scrollTop = scrollTop;\n\t}\n\n\tfunction handleWheel(event: WheelEvent): void {\n\t\tif (!scrollElement) return;\n\t\tconst horizontalDelta = event.deltaX + (event.shiftKey ? event.deltaY : 0);\n\t\tconst verticalDelta = event.shiftKey ? 0 : event.deltaY;\n\t\tconst nextLeft = Math.max(\n\t\t\t0,\n\t\t\tMath.min(\n\t\t\t\tscrollElement.scrollLeft + horizontalDelta,\n\t\t\t\tscrollElement.scrollWidth - scrollElement.clientWidth\n\t\t\t)\n\t\t);\n\t\tconst nextTop = Math.max(\n\t\t\t0,\n\t\t\tMath.min(\n\t\t\t\tscrollElement.scrollTop + verticalDelta,\n\t\t\t\tscrollElement.scrollHeight - scrollElement.clientHeight\n\t\t\t)\n\t\t);\n\t\tif (nextLeft === scrollElement.scrollLeft && nextTop === scrollElement.scrollTop) return;\n\n\t\tevent.preventDefault();\n\t\tscrollElement.scrollTo({ left: nextLeft, top: nextTop });\n\t}\n</script>\n\n<!-- The pointer can leave the thumb, and the window, before it is released. -->\n<svelte:window onpointercancel={onScrollbarDragEnd} onpointerup={onScrollbarDragEnd} />\n\n<div\n\tclass={cn('relative min-h-0', isFullscreen && 'flex flex-1', navigating && 'gantt-navigating')}\n\tstyle:--gantt-tree-width={`${treeWidth}px`}\n\tstyle:--gantt-timeline-width={`${timeline.width}px`}\n\tstyle:--gantt-column-width={`${timeline.columnWidth}px`}\n>\n\t<div\n\t\tbind:this={scrollElement}\n\t\tbind:clientWidth={viewportWidth}\n\t\tonscroll={handleChartScroll}\n\t\tonwheel={handleWheel}\n\t\tclass={cn(\n\t\t\t'gantt-scroll max-h-[min(42rem,70vh)] overflow-hidden',\n\t\t\tisFullscreen && 'min-h-0 flex-1 max-h-none'\n\t\t)}\n\t\trole=\"region\"\n\t\taria-label={`${title} Gantt chart`}\n\t>\n\t\t<div class=\"min-w-max\" style:width={`${treeWidth + timeline.width}px`}>\n\t\t<div class=\"sticky top-0 z-20 flex h-16 bg-card\">\n\t\t\t<div class=\"gantt-tree-header sticky left-0 z-30 grid shrink-0 grid-rows-2 border-r bg-card\">\n\t\t\t\t<div class=\"flex items-center border-b px-3 text-xs font-medium text-muted-foreground\">\n\t\t\t\t\tTasks\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclass=\"grid grid-cols-[minmax(0,1fr)_4rem_4rem] items-center px-3 text-[11px] font-medium text-muted-foreground\"\n\t\t\t\t>\n\t\t\t\t\t<span>Name</span>\n\t\t\t\t\t<span>Start</span>\n\t\t\t\t\t<span>End</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"gantt-timeline-header shrink-0 bg-card\">\n\t\t\t\t<div class=\"flex h-8 border-b\">\n\t\t\t\t\t{#each timeline.groups as group (group.key)}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass=\"flex shrink-0 items-center border-r\"\n\t\t\t\t\t\t\tstyle:width={`${group.unitCount * timeline.columnWidth}px`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclass=\"gantt-group-label sticky shrink-0 px-2 text-[11px] font-medium whitespace-nowrap text-muted-foreground\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{group.label}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/each}\n\t\t\t\t</div>\n\t\t\t\t<div class=\"flex h-8\">\n\t\t\t\t\t{#each timeline.units as unit (unit.key)}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\t'flex shrink-0 items-center justify-center border-r text-[11px] text-muted-foreground',\n\t\t\t\t\t\t\t\tunit.isOffDay && (scale === 'week' || scale === 'month') && 'bg-muted/50'\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle:width={`${timeline.columnWidth}px`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{unit.label}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t{/each}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"relative\">\n\t\t\t<div\n\t\t\t\tclass=\"pointer-events-none absolute inset-y-0\"\n\t\t\t\tstyle:left={`${treeWidth}px`}\n\t\t\t\tstyle:width={`${timeline.width}px`}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t>\n\t\t\t\t{#each offDaySpans as span (span.left)}\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"absolute inset-y-0 bg-muted/35\"\n\t\t\t\t\t\tstyle:left={`${span.left}px`}\n\t\t\t\t\t\tstyle:width={`${span.width}px`}\n\t\t\t\t\t></div>\n\t\t\t\t{/each}\n\t\t\t\t{#if todayX !== null}\n\t\t\t\t\t<div class=\"absolute inset-y-0 w-px bg-destructive\" style:left={`${todayX}px`}></div>\n\t\t\t\t{/if}\n\t\t\t</div>\n\n\t\t\t{#if loading}\n\t\t\t\t<GanttLoadingRows timelineWidth={timeline.width} />\n\t\t\t{:else}\n\t\t\t\t{#each rows as renderRow (renderRow.row.resource.id)}\n\t\t\t\t\t<GanttRow\n\t\t\t\t\t\t{renderRow}\n\t\t\t\t\t\ttimelineWidth={timeline.width}\n\t\t\t\t\t\t{collapsed}\n\t\t\t\t\t\t{locale}\n\t\t\t\t\t\t{onToggleResource}\n\t\t\t\t\t\t{onGoToDate}\n\t\t\t\t\t\t{onEventClick}\n\t\t\t\t\t/>\n\t\t\t\t{/each}\n\t\t\t{/if}\n\t\t</div>\n\t</div>\n\t</div>\n\n\t<div\n\t\tbind:this={horizontalScrollbar}\n\t\tonscroll={handleHorizontalScrollbarScroll}\n\t\tonpointerdown={onScrollbarDragStart}\n\t\tclass={cn(\n\t\t\t'gantt-scrollbar absolute right-0 bottom-0 z-40 h-3 overflow-x-auto overflow-y-hidden transition-opacity',\n\t\t\t// Vanish the moment the timeline window slides; ease back in once it settles.\n\t\t\tscrollbarHidden ? 'pointer-events-none opacity-0 duration-0' : 'opacity-100 duration-300'\n\t\t)}\n\t\tstyle:left={`${treeWidth}px`}\n\t\taria-hidden=\"true\"\n\t>\n\t\t<div class=\"h-px\" style:width={`${timeline.width}px`}></div>\n\t</div>\n\n\t<div\n\t\tbind:this={verticalScrollbar}\n\t\tonscroll={handleVerticalScrollbarScroll}\n\t\tclass=\"gantt-scrollbar absolute top-16 right-0 bottom-0 z-40 w-3 overflow-x-hidden overflow-y-auto\"\n\t\taria-hidden=\"true\"\n\t>\n\t\t<div class=\"w-px\" style:height={`${scrollBodyHeight}px`}></div>\n\t</div>\n</div>\n\n<style>\n\t.gantt-tree-header {\n\t\twidth: var(--gantt-tree-width);\n\t}\n\n\t.gantt-timeline-header {\n\t\twidth: var(--gantt-timeline-width);\n\t}\n\n\t.gantt-group-label {\n\t\tleft: var(--gantt-tree-width);\n\t}\n\n\t/*\n\t * Safari settles sticky offsets a frame behind a composited smooth scroll, so\n\t * during a period step the pinned task column lags and timeline bars flash\n\t * across it. Promoting the pinned cells puts them on the scrolling thread for\n\t * the length of the step; the class comes off once the scroll settles, because\n\t * a permanent layer per row is not worth paying for.\n\t */\n\t.gantt-navigating .gantt-tree-header,\n\t.gantt-navigating :global(.gantt-tree-cell) {\n\t\ttransform: translateZ(0);\n\t\twill-change: transform;\n\t}\n\n\t/*\n\t * These proxy elements exist only to carry a scrollbar, so the scrollbar has to\n\t * be visible at rest. Safari's overlay scrollbars fade out when idle and only\n\t * stay put when the system preference says \"Always\"; opting into the WebKit\n\t * scrollbar pseudo-elements switches both WebKit and Blink to a persistent,\n\t * non-overlay thumb that we style to match the surrounding chrome.\n\t */\n\t.gantt-scrollbar::-webkit-scrollbar {\n\t\twidth: 0.75rem;\n\t\theight: 0.75rem;\n\t}\n\n\t.gantt-scrollbar::-webkit-scrollbar-track {\n\t\tbackground: transparent;\n\t}\n\n\t.gantt-scrollbar::-webkit-scrollbar-thumb {\n\t\t/* Inset via a transparent border so the thumb reads thinner than its track. */\n\t\tborder: 3px solid transparent;\n\t\tborder-radius: 9999px;\n\t\tbackground-clip: content-box;\n\t\tbackground-color: color-mix(in oklab, var(--muted-foreground) 40%, transparent);\n\t}\n\n\t.gantt-scrollbar::-webkit-scrollbar-thumb:hover {\n\t\tbackground-color: color-mix(in oklab, var(--muted-foreground) 65%, transparent);\n\t}\n\n\t/* Firefox has no scrollbar pseudo-elements; its own thin scrollbar is persistent. */\n\t@supports not selector(::-webkit-scrollbar) {\n\t\t.gantt-scrollbar {\n\t\t\tscrollbar-width: thin;\n\t\t\tscrollbar-color: color-mix(in oklab, var(--muted-foreground) 40%, transparent) transparent;\n\t\t}\n\t}\n\n</style>\n",
			"type": "registry:file",
			"target": "gantt/gantt-chart.svelte"
		},
		{
			"content": "import type { GanttScale } from './gantt-types.js';\n\nexport const HOUR_MS = 60 * 60 * 1000;\n\nexport function startOfDay(date: Date): Date {\n\treturn new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n\n/**\n * Timeline math is done in the viewer's local time zone, so an unparseable or\n * missing anchor would spread `NaN` through every column. Fall back to today.\n */\nexport function resolveGanttDate(date: Date | undefined, fallback = new Date()): Date {\n\tif (!date || Number.isNaN(date.getTime())) return new Date(fallback);\n\treturn new Date(date);\n}\n\nexport function startOfWeek(date: Date): Date {\n\tconst result = startOfDay(date);\n\tconst day = result.getDay();\n\tresult.setDate(result.getDate() - ((day + 6) % 7));\n\treturn result;\n}\n\nexport function addDays(date: Date, amount: number): Date {\n\tconst result = new Date(date);\n\tresult.setDate(result.getDate() + amount);\n\treturn result;\n}\n\nexport function addMonths(date: Date, amount: number): Date {\n\treturn new Date(date.getFullYear(), date.getMonth() + amount, 1);\n}\n\nexport function addYears(date: Date, amount: number): Date {\n\treturn new Date(date.getFullYear() + amount, date.getMonth(), 1);\n}\n\nexport function startOfQuarter(date: Date): Date {\n\treturn new Date(date.getFullYear(), Math.floor(date.getMonth() / 3) * 3, 1);\n}\n\nexport function shiftGanttDate(scale: GanttScale, date: Date, steps: number): Date {\n\tif (scale === 'day') return addDays(date, steps);\n\tif (scale === 'week') return addDays(date, steps * 7);\n\tif (scale === 'month') return addMonths(date, steps);\n\tif (scale === 'quarter') return addMonths(date, steps * 3);\n\treturn addYears(date, steps);\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-date.ts"
		},
		{
			"content": "/**\n * Compatibility barrel for the Gantt's pure helpers. New implementation code\n * imports the focused modules directly.\n */\nexport { resolveGanttDate, shiftGanttDate } from './gantt-date.js';\nexport { type FlattenedGanttResource, flattenResources } from './gantt-resources.js';\nexport {\n\tbuildTimeline,\n\tdateToTimelineX,\n\ttype GanttOffDaySpan,\n\ttype GanttTimeline,\n\ttype GanttTimelineGroup,\n\ttype GanttTimelineUnit,\n\tgetFocusUnitCount,\n\tgetOffDaySpans,\n\tgetPeriodIndexAtX,\n\tgetPeriodX,\n\tstretchTimeline,\n\ttimelineXToDate\n} from './gantt-timeline.js';\n",
			"type": "registry:file",
			"target": "gantt/gantt-lib.ts"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { Skeleton } from '$UI$/skeleton/index.js';\n\n\tlet { timelineWidth }: { timelineWidth: number } = $props();\n</script>\n\n{#each [0, 1, 2, 3, 4, 5, 6] as index (index)}\n\t<div class=\"flex h-12 border-b\">\n\t\t<div class=\"gantt-tree-cell sticky left-0 z-10 flex shrink-0 items-center border-r bg-card px-3\">\n\t\t\t<Skeleton class=\"h-4 w-3/5\" />\n\t\t</div>\n\t\t<div class=\"timeline-grid relative shrink-0\" style:width={`${timelineWidth}px`}>\n\t\t\t<Skeleton class=\"absolute top-3 h-5 w-40\" style={`left: ${80 + index * 28}px`} />\n\t\t</div>\n\t</div>\n{/each}\n\n<style>\n\t.gantt-tree-cell {\n\t\twidth: var(--gantt-tree-width);\n\t}\n\n\t.timeline-grid {\n\t\tbackground-image: linear-gradient(to left, var(--border) 1px, transparent 1px);\n\t\tbackground-size: var(--gantt-column-width) 100%;\n\t}\n</style>\n",
			"type": "registry:file",
			"target": "gantt/gantt-loading-rows.svelte"
		},
		{
			"content": "import type { PositionedGanttBar } from './gantt-rows.js';\nimport type { GanttEventTone } from './gantt-types.js';\n\nfunction displayedDate(date: Date, isEnd: boolean): Date {\n\treturn isEnd ? new Date(date.getTime() - 1) : date;\n}\n\nexport function formatGanttTreeDate(\n\tdate: Date | null,\n\tlocale: string,\n\tisEnd = false\n): string {\n\tif (!date) return '—';\n\treturn new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric' }).format(\n\t\tdisplayedDate(date, isEnd)\n\t);\n}\n\nexport function formatGanttBarDate(date: Date, locale: string, isEnd = false): string {\n\treturn new Intl.DateTimeFormat(locale, {\n\t\tmonth: 'short',\n\t\tday: 'numeric',\n\t\tyear: 'numeric'\n\t}).format(displayedDate(date, isEnd));\n}\n\nexport function getGanttToneClass(tone: GanttEventTone, isSummary: boolean): string {\n\tif (isSummary) return 'bg-secondary-foreground/70 text-secondary';\n\tif (tone === 'secondary') return 'bg-secondary text-secondary-foreground ring-1 ring-border';\n\tif (tone === 'accent') return 'bg-accent text-accent-foreground ring-1 ring-border';\n\tif (tone === 'muted') return 'bg-muted text-muted-foreground ring-1 ring-border';\n\tif (tone === 'destructive') {\n\t\treturn 'bg-destructive/15 text-destructive ring-1 ring-destructive/20';\n\t}\n\treturn 'bg-primary text-primary-foreground';\n}\n\nexport function getGanttDotClass(tone: GanttEventTone | null): string {\n\tif (tone === 'destructive') return 'bg-destructive';\n\tif (tone === 'secondary') return 'bg-secondary-foreground';\n\tif (tone === 'accent') return 'bg-accent-foreground';\n\tif (tone === 'muted') return 'bg-muted-foreground';\n\treturn 'bg-primary';\n}\n\nexport function getGanttContinuationLabel<T>(bar: PositionedGanttBar<T>): string {\n\tif (bar.continuesBefore && bar.continuesAfter) return 'Starts and ends outside this range';\n\tif (bar.continuesBefore) return 'Starts before this range';\n\tif (bar.continuesAfter) return 'Continues past this range';\n\treturn '';\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-presentation.ts"
		},
		{
			"content": "import type { GanttResource } from './gantt-types.js';\n\nexport interface FlattenedGanttResource {\n\tdepth: number;\n\tdescendantIds: string[];\n\tisGroup: boolean;\n\tresource: GanttResource;\n}\n\nfunction collectDescendantIds(resource: GanttResource): string[] {\n\treturn (resource.children ?? []).flatMap((child) => [child.id, ...collectDescendantIds(child)]);\n}\n\nexport function flattenResources(\n\tresources: GanttResource[],\n\tcollapsed: ReadonlySet<string>\n): FlattenedGanttResource[] {\n\tconst rows: FlattenedGanttResource[] = [];\n\n\tfunction visit(resource: GanttResource, depth: number): void {\n\t\tconst isGroup = Boolean(resource.children?.length);\n\t\trows.push({\n\t\t\tresource,\n\t\t\tdepth,\n\t\t\tisGroup,\n\t\t\tdescendantIds: collectDescendantIds(resource)\n\t\t});\n\t\tif (isGroup && !collapsed.has(resource.id)) {\n\t\t\tfor (const child of resource.children ?? []) visit(child, depth + 1);\n\t\t}\n\t}\n\n\tfor (const resource of resources) visit(resource, 0);\n\treturn rows;\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-resources.ts"
		},
		{
			"content": "<script lang=\"ts\" generics=\"TData = unknown\">\n\timport ChevronDownIcon from '@lucide/svelte/icons/chevron-down';\n\timport ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';\n\timport ChevronRightIcon from '@lucide/svelte/icons/chevron-right';\n\n\timport { Button } from '$UI$/button/index.js';\n\timport * as Tooltip from '$UI$/tooltip/index.js';\n\timport { cn } from '$UTILS$.js';\n\n\timport type { GanttRenderRow, PositionedGanttBar } from './gantt-rows.js';\n\timport type { GanttEvent } from './gantt-types.js';\n\n\timport {\n\t\tformatGanttBarDate,\n\t\tformatGanttTreeDate,\n\t\tgetGanttContinuationLabel,\n\t\tgetGanttDotClass,\n\t\tgetGanttToneClass\n\t} from './gantt-presentation.js';\n\n\tlet {\n\t\tcollapsed,\n\t\tlocale,\n\t\tonEventClick,\n\t\tonGoToDate,\n\t\tonToggleResource,\n\t\trenderRow,\n\t\ttimelineWidth\n\t}: {\n\t\tcollapsed: ReadonlySet<string>;\n\t\tlocale: string;\n\t\tonEventClick?: (event: GanttEvent<TData>) => void;\n\t\tonGoToDate: (date: Date) => void;\n\t\tonToggleResource: (id: string) => void;\n\t\trenderRow: GanttRenderRow<TData>;\n\t\ttimelineWidth: number;\n\t} = $props();\n\n\tfunction isInteractive(bar: PositionedGanttBar<TData>): boolean {\n\t\treturn Boolean(onEventClick) && bar.event !== null && !bar.event.readOnly;\n\t}\n\n\tfunction activateBar(bar: PositionedGanttBar<TData>): void {\n\t\tif (isInteractive(bar) && bar.event) onEventClick?.(bar.event);\n\t}\n\n\tfunction progressLabel(bar: PositionedGanttBar<TData>): string {\n\t\treturn 'progressLabel' in bar && typeof bar.progressLabel === 'string'\n\t\t\t? bar.progressLabel\n\t\t\t: 'complete';\n\t}\n</script>\n\n<div class=\"flex border-b last:border-b-0\" style:height={`${renderRow.height}px`}>\n\t<div\n\t\tclass=\"gantt-tree-cell sticky left-0 z-10 grid shrink-0 grid-cols-[minmax(0,1fr)_4rem_4rem] items-center border-r bg-card px-3 text-xs\"\n\t>\n\t\t<div\n\t\t\tclass=\"flex min-w-0 items-center gap-1.5\"\n\t\t\tstyle:padding-left={`${renderRow.row.depth * 16}px`}\n\t\t>\n\t\t\t{#if renderRow.row.isGroup}\n\t\t\t\t<Button\n\t\t\t\t\tvariant=\"ghost\"\n\t\t\t\t\tsize=\"icon-xs\"\n\t\t\t\t\taria-label={`${collapsed.has(renderRow.row.resource.id) ? 'Expand' : 'Collapse'} ${renderRow.row.resource.title}`}\n\t\t\t\t\tonclick={() => onToggleResource(renderRow.row.resource.id)}\n\t\t\t\t>\n\t\t\t\t\t{#if collapsed.has(renderRow.row.resource.id)}\n\t\t\t\t\t\t<ChevronRightIcon />\n\t\t\t\t\t{:else}\n\t\t\t\t\t\t<ChevronDownIcon />\n\t\t\t\t\t{/if}\n\t\t\t\t</Button>\n\t\t\t{:else}\n\t\t\t\t<span class={cn('size-2 shrink-0 rounded-full', getGanttDotClass(renderRow.tone))}></span>\n\t\t\t{/if}\n\t\t\t<span class={cn('truncate', renderRow.row.isGroup && 'font-medium')}>\n\t\t\t\t{renderRow.row.resource.title}\n\t\t\t</span>\n\t\t</div>\n\t\t<span class=\"text-muted-foreground\">{formatGanttTreeDate(renderRow.start, locale)}</span>\n\t\t<span class=\"text-muted-foreground\">{formatGanttTreeDate(renderRow.end, locale, true)}</span>\n\t</div>\n\n\t{#if renderRow.before}\n\t\t{@const hint = renderRow.before}\n\t\t<div class=\"gantt-chip-start sticky z-10 flex w-0 items-center\">\n\t\t\t<Tooltip.Root>\n\t\t\t\t<Tooltip.Trigger>\n\t\t\t\t\t{#snippet child({ props })}\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"ml-1 flex items-center gap-0.5 rounded-full border bg-card py-0.5 pr-1.5 pl-1 text-[10px] font-medium text-muted-foreground shadow-xs hover:text-foreground\"\n\t\t\t\t\t\t\tonclick={() => onGoToDate(hint.date)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<ChevronLeftIcon class=\"size-3\" />\n\t\t\t\t\t\t\t{hint.count}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t{/snippet}\n\t\t\t\t</Tooltip.Trigger>\n\t\t\t\t<Tooltip.Content>\n\t\t\t\t\t{hint.count} {hint.count === 1 ? 'item' : 'items'} before this range — jump to\n\t\t\t\t\t{formatGanttBarDate(hint.date, locale)}\n\t\t\t\t</Tooltip.Content>\n\t\t\t</Tooltip.Root>\n\t\t</div>\n\t{/if}\n\n\t<div class=\"timeline-grid relative shrink-0\" style:width={`${timelineWidth}px`}>\n\t\t{#each renderRow.bars as bar (bar.key)}\n\t\t\t<Tooltip.Root>\n\t\t\t\t<Tooltip.Trigger>\n\t\t\t\t\t{#snippet child({ props })}\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass={cn(\n\t\t\t\t\t\t\t\t'gantt-bar absolute flex items-center rounded-md text-left text-[11px] font-medium shadow-xs transition-[filter,box-shadow] outline-none focus-visible:ring-2 focus-visible:ring-ring',\n\t\t\t\t\t\t\t\tbar.isSummary ? 'h-2' : 'h-5 px-2',\n\t\t\t\t\t\t\t\tgetGanttToneClass(bar.tone, bar.isSummary),\n\t\t\t\t\t\t\t\tisInteractive(bar) ? 'cursor-pointer hover:brightness-95' : 'cursor-default',\n\t\t\t\t\t\t\t\tbar.continuesBefore && 'rounded-l-none',\n\t\t\t\t\t\t\t\tbar.continuesAfter && 'rounded-r-none'\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tstyle:left={`${bar.left}px`}\n\t\t\t\t\t\t\tstyle:--gantt-left={`${bar.left}px`}\n\t\t\t\t\t\t\tstyle:top={bar.isSummary\n\t\t\t\t\t\t\t\t? `${(renderRow.height - 8) / 2}px`\n\t\t\t\t\t\t\t\t: `${8 + bar.lane * 24}px`}\n\t\t\t\t\t\t\tstyle:width={`${bar.width}px`}\n\t\t\t\t\t\t\tonclick={() => activateBar(bar)}\n\t\t\t\t\t\t\taria-label={`${bar.title}, ${formatGanttBarDate(bar.start, locale)} to ${formatGanttBarDate(bar.end, locale, true)}${getGanttContinuationLabel(bar) ? `, ${getGanttContinuationLabel(bar).toLowerCase()}` : ''}`}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{#if bar.progress !== null}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"pointer-events-none absolute inset-y-0 left-0 rounded-[inherit] bg-foreground/15\"\n\t\t\t\t\t\t\t\t\tstyle:width={`${bar.progress}%`}\n\t\t\t\t\t\t\t\t></span>\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{#if !bar.isSummary}\n\t\t\t\t\t\t\t\t{#if bar.continuesBefore}\n\t\t\t\t\t\t\t\t\t<ChevronLeftIcon\n\t\t\t\t\t\t\t\t\t\tclass=\"relative -ml-1 size-3 shrink-0 opacity-70\"\n\t\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t\t<span class=\"relative truncate\">{bar.title}</span>\n\t\t\t\t\t\t\t\t{#if bar.continuesAfter}\n\t\t\t\t\t\t\t\t\t<ChevronRightIcon\n\t\t\t\t\t\t\t\t\t\tclass=\"relative -mr-1 ml-auto size-3 shrink-0 opacity-70\"\n\t\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t{/snippet}\n\t\t\t\t</Tooltip.Trigger>\n\t\t\t\t<Tooltip.Content class=\"flex flex-col items-start gap-1\">\n\t\t\t\t\t<span class=\"font-medium\">{bar.title}</span>\n\t\t\t\t\t<span>\n\t\t\t\t\t\t{formatGanttBarDate(bar.start, locale)} –\n\t\t\t\t\t\t{formatGanttBarDate(bar.end, locale, true)}\n\t\t\t\t\t</span>\n\t\t\t\t\t{#if bar.progress !== null}\n\t\t\t\t\t\t<span>{Math.round(bar.progress)}% {progressLabel(bar)}</span>\n\t\t\t\t\t{/if}\n\t\t\t\t\t{#if getGanttContinuationLabel(bar)}\n\t\t\t\t\t\t<span class=\"text-muted-foreground\">{getGanttContinuationLabel(bar)}</span>\n\t\t\t\t\t{/if}\n\t\t\t\t</Tooltip.Content>\n\t\t\t</Tooltip.Root>\n\t\t{/each}\n\t</div>\n\n\t{#if renderRow.after}\n\t\t{@const hint = renderRow.after}\n\t\t<div class=\"sticky right-0 z-10 flex w-0 items-center justify-end\">\n\t\t\t<Tooltip.Root>\n\t\t\t\t<Tooltip.Trigger>\n\t\t\t\t\t{#snippet child({ props })}\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"mr-1 flex items-center gap-0.5 rounded-full border bg-card py-0.5 pr-1 pl-1.5 text-[10px] font-medium text-muted-foreground shadow-xs hover:text-foreground\"\n\t\t\t\t\t\t\tonclick={() => onGoToDate(hint.date)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{hint.count}\n\t\t\t\t\t\t\t<ChevronRightIcon class=\"size-3\" />\n\t\t\t\t\t\t</button>\n\t\t\t\t\t{/snippet}\n\t\t\t\t</Tooltip.Trigger>\n\t\t\t\t<Tooltip.Content>\n\t\t\t\t\t{hint.count} {hint.count === 1 ? 'item' : 'items'} after this range — jump to\n\t\t\t\t\t{formatGanttBarDate(hint.date, locale)}\n\t\t\t\t</Tooltip.Content>\n\t\t\t</Tooltip.Root>\n\t\t</div>\n\t{/if}\n</div>\n\n<style>\n\t.gantt-tree-cell {\n\t\twidth: var(--gantt-tree-width);\n\t}\n\n\t.gantt-chip-start {\n\t\tleft: var(--gantt-tree-width);\n\t}\n\n\t.timeline-grid {\n\t\tbackground-image: linear-gradient(to left, var(--border) 1px, transparent 1px);\n\t\tbackground-size: var(--gantt-column-width) 100%;\n\t}\n\n\t.gantt-bar {\n\t\tmax-width: calc(var(--gantt-timeline-width) - var(--gantt-left, 0px));\n\t}\n</style>\n",
			"type": "registry:file",
			"target": "gantt/gantt-row.svelte"
		},
		{
			"content": "import type { FlattenedGanttResource } from './gantt-resources.js';\nimport type { GanttEvent, GanttEventTone } from './gantt-types.js';\n\nimport { dateToTimelineX, type GanttTimeline } from './gantt-timeline.js';\n\nexport interface PositionedGanttBar<T> {\n\tcontinuesAfter: boolean;\n\tcontinuesBefore: boolean;\n\tend: Date;\n\tevent: GanttEvent<T> | null;\n\tisSummary: boolean;\n\tkey: string;\n\tlane: number;\n\tleft: number;\n\tprogress: null | number;\n\tprogressLabel: string;\n\tstart: Date;\n\ttitle: string;\n\ttone: GanttEventTone;\n\twidth: number;\n}\n\nexport interface GanttOffscreenHint {\n\tcount: number;\n\tdate: Date;\n}\n\nexport interface GanttRenderRow<T> {\n\tafter: GanttOffscreenHint | null;\n\tbars: PositionedGanttBar<T>[];\n\tbefore: GanttOffscreenHint | null;\n\tend: Date | null;\n\theight: number;\n\trow: FlattenedGanttResource;\n\tstart: Date | null;\n\ttone: GanttEventTone | null;\n}\n\nfunction clampProgress(progress: number | undefined): null | number {\n\tif (progress === undefined || !Number.isFinite(progress)) return null;\n\treturn Math.max(0, Math.min(100, progress));\n}\n\nfunction getSummaryProgress<T>(source: GanttEvent<T>[]): null | number {\n\tlet totalDuration = 0;\n\tlet completedDuration = 0;\n\tfor (const event of source) {\n\t\tif (event.progress === undefined) continue;\n\t\tconst duration = Math.max(1, event.end.getTime() - event.start.getTime());\n\t\ttotalDuration += duration;\n\t\tcompletedDuration += duration * (clampProgress(event.progress) ?? 0);\n\t}\n\treturn totalDuration > 0 ? completedDuration / totalDuration : null;\n}\n\nfunction positionBars<T>(\n\tsource: GanttEvent<T>[],\n\tview: GanttTimeline,\n\tisSummary: boolean,\n\tsummaryTitle = '',\n\tsummaryProgressLabel = 'complete'\n): PositionedGanttBar<T>[] {\n\tconst visible = source\n\t\t.filter((event) => event.end > view.start && event.start < view.end && event.end >= event.start)\n\t\t.toSorted((a, b) => a.start.getTime() - b.start.getTime());\n\tconst laneEnds: number[] = [];\n\n\treturn visible.map((event) => {\n\t\tlet lane = laneEnds.findIndex((end) => end <= event.start.getTime());\n\t\tif (lane === -1) lane = laneEnds.length;\n\t\tlaneEnds[lane] = event.end.getTime();\n\t\tconst left = dateToTimelineX(event.start, view);\n\t\tconst right = dateToTimelineX(event.end, view);\n\t\treturn {\n\t\t\tkey: isSummary ? `summary-${event.id}` : event.id,\n\t\t\ttitle: isSummary ? summaryTitle : event.title,\n\t\t\tstart: event.start,\n\t\t\tend: event.end,\n\t\t\tprogress: clampProgress(event.progress),\n\t\t\tprogressLabel: isSummary ? summaryProgressLabel : (event.progressLabel ?? 'complete'),\n\t\t\ttone: event.tone ?? 'primary',\n\t\t\tleft,\n\t\t\twidth: Math.max(isSummary ? 8 : 6, right - left),\n\t\t\tlane,\n\t\t\tisSummary,\n\t\t\tcontinuesBefore: event.start < view.start,\n\t\t\tcontinuesAfter: event.end > view.end,\n\t\t\tevent: isSummary ? null : event\n\t\t};\n\t});\n}\n\nfunction getOffscreen<T>(source: GanttEvent<T>[], view: GanttTimeline) {\n\tconst before = source.filter((event) => event.end <= view.start);\n\tconst after = source.filter((event) => event.start >= view.end);\n\treturn {\n\t\tbefore:\n\t\t\tbefore.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\tcount: before.length,\n\t\t\t\t\t\tdate: new Date(Math.max(...before.map((event) => event.start.getTime())))\n\t\t\t\t\t}\n\t\t\t\t: null,\n\t\tafter:\n\t\t\tafter.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\tcount: after.length,\n\t\t\t\t\t\tdate: new Date(Math.min(...after.map((event) => event.start.getTime())))\n\t\t\t\t\t}\n\t\t\t\t: null\n\t};\n}\n\nexport function buildGanttRows<T>(\n\tflat: FlattenedGanttResource[],\n\tallEvents: GanttEvent<T>[],\n\tview: GanttTimeline\n): GanttRenderRow<T>[] {\n\tconst byResource: Record<string, GanttEvent<T>[]> = {};\n\tfor (const event of allEvents) (byResource[event.resourceId] ??= []).push(event);\n\n\treturn flat.map((row) => {\n\t\tconst resourceIds = row.isGroup ? [row.resource.id, ...row.descendantIds] : [row.resource.id];\n\t\tconst rowEvents = resourceIds.flatMap((id) => byResource[id] ?? []);\n\t\tconst starts = rowEvents.map((event) => event.start.getTime());\n\t\tconst ends = rowEvents.map((event) => event.end.getTime());\n\t\tconst start = starts.length > 0 ? new Date(Math.min(...starts)) : null;\n\t\tconst end = ends.length > 0 ? new Date(Math.max(...ends)) : null;\n\t\tconst offscreen = getOffscreen(rowEvents, view);\n\n\t\tif (row.isGroup && start && end) {\n\t\t\tconst summary: GanttEvent<T> = {\n\t\t\t\tid: row.resource.id,\n\t\t\t\ttitle: row.resource.title,\n\t\t\t\tstart,\n\t\t\t\tend,\n\t\t\t\tresourceId: row.resource.id,\n\t\t\t\tprogress:\n\t\t\t\t\trow.resource.aggregateProgress === false\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: (getSummaryProgress(rowEvents) ?? undefined),\n\t\t\t\ttone: 'secondary'\n\t\t\t};\n\t\t\treturn {\n\t\t\t\trow,\n\t\t\t\tbars: positionBars(\n\t\t\t\t\t[summary],\n\t\t\t\t\tview,\n\t\t\t\t\ttrue,\n\t\t\t\t\trow.resource.title,\n\t\t\t\t\trow.resource.progressLabel\n\t\t\t\t),\n\t\t\t\theight: 48,\n\t\t\t\tstart,\n\t\t\t\tend,\n\t\t\t\ttone: 'secondary',\n\t\t\t\t...offscreen\n\t\t\t};\n\t\t}\n\n\t\tconst bars = positionBars(rowEvents, view, false);\n\t\tconst laneCount = bars.reduce((maximum, bar) => Math.max(maximum, bar.lane + 1), 1);\n\t\treturn {\n\t\t\trow,\n\t\t\tbars,\n\t\t\theight: Math.max(48, 16 + laneCount * 24),\n\t\t\tstart,\n\t\t\tend,\n\t\t\ttone: rowEvents[0]?.tone ?? null,\n\t\t\t...offscreen\n\t\t};\n\t});\n}\n\nexport function getTodayX(view: GanttTimeline): null | number {\n\tconst now = new Date();\n\treturn now >= view.start && now < view.end ? dateToTimelineX(now, view) : null;\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-rows.ts"
		},
		{
			"content": "import type { GanttScale } from './gantt-types.js';\n\nimport {\n\taddDays,\n\taddMonths,\n\taddYears,\n\tHOUR_MS,\n\tshiftGanttDate,\n\tstartOfDay,\n\tstartOfQuarter,\n\tstartOfWeek\n} from './gantt-date.js';\n\nexport interface GanttTimelineUnit {\n\tend: Date;\n\tgroupKey: string;\n\tgroupLabel: string;\n\tisOffDay: boolean;\n\tkey: string;\n\tlabel: string;\n\tstart: Date;\n}\n\nexport interface GanttTimelineGroup {\n\tkey: string;\n\tlabel: string;\n\tunitCount: number;\n}\n\nexport interface GanttTimeline {\n\tcolumnWidth: number;\n\tend: Date;\n\tgroups: GanttTimelineGroup[];\n\t/** Index into `periodStarts` of the period the header controls are anchored to. */\n\tperiodIndex: number;\n\t/** Unit index each rendered period begins at; padding periods flank the focused one. */\n\tperiodStarts: number[];\n\tstart: Date;\n\ttitle: string;\n\tunits: GanttTimelineUnit[];\n\twidth: number;\n}\n\n/** A contiguous run of off-days, merged so a weekend is one element rather than two. */\nexport interface GanttOffDaySpan {\n\tleft: number;\n\twidth: number;\n}\n\n/** One period's worth of columns, before padding periods are joined around it. */\ninterface GanttPeriod {\n\tcolumnWidth: number;\n\tend: Date;\n\tstart: Date;\n\ttitle: string;\n\tunits: GanttTimelineUnit[];\n}\n\nfunction getWeekNumber(date: Date): number {\n\tconst target = startOfDay(date);\n\tconst day = target.getDay() || 7;\n\ttarget.setDate(target.getDate() + 4 - day);\n\tconst yearStart = new Date(target.getFullYear(), 0, 1);\n\treturn Math.ceil(((target.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7);\n}\n\nfunction formatDate(date: Date, locale: string, options: Intl.DateTimeFormatOptions): string {\n\treturn new Intl.DateTimeFormat(locale, options).format(date);\n}\n\nfunction formatRange(start: Date, end: Date, locale: string): string {\n\tconst visibleEnd = new Date(end.getTime() - 1);\n\tif (start.getFullYear() === visibleEnd.getFullYear()) {\n\t\tif (start.getMonth() === visibleEnd.getMonth()) {\n\t\t\tconst month = formatDate(start, locale, { month: 'short' });\n\t\t\treturn `${month} ${start.getDate()}–${visibleEnd.getDate()}, ${start.getFullYear()}`;\n\t\t}\n\t\treturn `${formatDate(start, locale, { month: 'short', day: 'numeric' })} – ${formatDate(visibleEnd, locale, { month: 'short', day: 'numeric' })}, ${start.getFullYear()}`;\n\t}\n\treturn `${formatDate(start, locale, { month: 'short', day: 'numeric', year: 'numeric' })} – ${formatDate(visibleEnd, locale, { month: 'short', day: 'numeric', year: 'numeric' })}`;\n}\n\nfunction makeUnit(\n\tstart: Date,\n\tend: Date,\n\tlabel: string,\n\tgroupKey: string,\n\tgroupLabel: string,\n\tkey: string\n): GanttTimelineUnit {\n\tconst weekday = start.getDay();\n\treturn {\n\t\tkey,\n\t\tstart,\n\t\tend,\n\t\tlabel,\n\t\tgroupKey,\n\t\tgroupLabel,\n\t\tisOffDay: weekday === 0 || weekday === 6\n\t};\n}\n\nfunction groupUnits(units: GanttTimelineUnit[]): GanttTimelineGroup[] {\n\tconst groups: GanttTimelineGroup[] = [];\n\tfor (const unit of units) {\n\t\tconst previous = groups.at(-1);\n\t\tif (previous?.key === unit.groupKey) previous.unitCount += 1;\n\t\telse groups.push({ key: unit.groupKey, label: unit.groupLabel, unitCount: 1 });\n\t}\n\treturn groups;\n}\n\nfunction buildPeriod(scale: GanttScale, anchor: Date, locale: string): GanttPeriod {\n\tconst units: GanttTimelineUnit[] = [];\n\tlet rangeStart: Date;\n\tlet rangeEnd: Date;\n\tlet columnWidth: number;\n\tlet title: string;\n\n\tif (scale === 'day') {\n\t\trangeStart = startOfDay(anchor);\n\t\trangeEnd = addDays(rangeStart, 1);\n\t\tcolumnWidth = 80;\n\t\ttitle = formatDate(rangeStart, locale, {\n\t\t\tweekday: 'long',\n\t\t\tmonth: 'long',\n\t\t\tday: 'numeric',\n\t\t\tyear: 'numeric'\n\t\t});\n\t\tfor (let hour = 0; hour < 24; hour += 2) {\n\t\t\tconst start = new Date(rangeStart.getTime() + hour * HOUR_MS);\n\t\t\tconst end = new Date(start.getTime() + 2 * HOUR_MS);\n\t\t\tunits.push(\n\t\t\t\tmakeUnit(\n\t\t\t\t\tstart,\n\t\t\t\t\tend,\n\t\t\t\t\tformatDate(start, locale, { hour: 'numeric' }),\n\t\t\t\t\tstart.toDateString(),\n\t\t\t\t\tformatDate(start, locale, { weekday: 'short', month: 'short', day: 'numeric' }),\n\t\t\t\t\tstart.toISOString()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t} else if (scale === 'week' || scale === 'month') {\n\t\trangeStart =\n\t\t\tscale === 'week' ? startOfWeek(anchor) : new Date(anchor.getFullYear(), anchor.getMonth(), 1);\n\t\trangeEnd = scale === 'week' ? addDays(rangeStart, 7) : addMonths(rangeStart, 1);\n\t\tcolumnWidth = scale === 'week' ? 96 : 44;\n\t\ttitle =\n\t\t\tscale === 'week'\n\t\t\t\t? formatRange(rangeStart, rangeEnd, locale)\n\t\t\t\t: formatDate(rangeStart, locale, { month: 'long', year: 'numeric' });\n\t\tfor (let cursor = rangeStart; cursor < rangeEnd; cursor = addDays(cursor, 1)) {\n\t\t\tconst end = addDays(cursor, 1);\n\t\t\tconst monthKey = `${cursor.getFullYear()}-${cursor.getMonth()}`;\n\t\t\tunits.push(\n\t\t\t\tmakeUnit(\n\t\t\t\t\tcursor,\n\t\t\t\t\tend,\n\t\t\t\t\tscale === 'week'\n\t\t\t\t\t\t? formatDate(cursor, locale, { weekday: 'short', day: 'numeric' })\n\t\t\t\t\t\t: String(cursor.getDate()),\n\t\t\t\t\tmonthKey,\n\t\t\t\t\tformatDate(cursor, locale, { month: 'long', year: 'numeric' }),\n\t\t\t\t\tcursor.toISOString()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t} else if (scale === 'quarter') {\n\t\tconst quarterStart = startOfQuarter(anchor);\n\t\tconst quarterEnd = addMonths(quarterStart, 3);\n\t\trangeStart = startOfWeek(quarterStart);\n\t\trangeEnd = startOfWeek(addDays(quarterEnd, 6));\n\t\tcolumnWidth = 80;\n\t\ttitle = `Q${Math.floor(quarterStart.getMonth() / 3) + 1} ${quarterStart.getFullYear()}`;\n\t\tfor (let cursor = rangeStart; cursor < rangeEnd; cursor = addDays(cursor, 7)) {\n\t\t\tconst end = addDays(cursor, 7);\n\t\t\tconst midpoint = addDays(cursor, 3);\n\t\t\tconst monthKey = `${midpoint.getFullYear()}-${midpoint.getMonth()}`;\n\t\t\tunits.push(\n\t\t\t\tmakeUnit(\n\t\t\t\t\tcursor,\n\t\t\t\t\tend,\n\t\t\t\t\t`W${getWeekNumber(cursor)}`,\n\t\t\t\t\tmonthKey,\n\t\t\t\t\tformatDate(midpoint, locale, { month: 'long' }),\n\t\t\t\t\tcursor.toISOString()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t} else {\n\t\trangeStart = new Date(anchor.getFullYear(), 0, 1);\n\t\trangeEnd = addYears(rangeStart, 1);\n\t\tcolumnWidth = 96;\n\t\ttitle = String(rangeStart.getFullYear());\n\t\tfor (let month = 0; month < 12; month += 1) {\n\t\t\tconst start = new Date(rangeStart.getFullYear(), month, 1);\n\t\t\tconst end = addMonths(start, 1);\n\t\t\tconst quarter = Math.floor(month / 3) + 1;\n\t\t\tunits.push(\n\t\t\t\tmakeUnit(\n\t\t\t\t\tstart,\n\t\t\t\t\tend,\n\t\t\t\t\tformatDate(start, locale, { month: 'short' }),\n\t\t\t\t\t`${start.getFullYear()}-Q${quarter}`,\n\t\t\t\t\t`Q${quarter}`,\n\t\t\t\t\tstart.toISOString()\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { start: rangeStart, end: rangeEnd, units, columnWidth, title };\n}\n\n/** Builds the focused period plus `padPeriods` on each side. */\nexport function buildTimeline(\n\tscale: GanttScale,\n\tanchor: Date,\n\tlocale: string,\n\tpadPeriods = 1\n): GanttTimeline {\n\tconst focus = buildPeriod(scale, anchor, locale);\n\tconst pad = Number.isFinite(padPeriods) ? Math.max(0, Math.trunc(padPeriods)) : 0;\n\tconst before: GanttTimelineUnit[][] = [];\n\tlet leadingEdge = focus.start;\n\tfor (let step = 1; step <= pad; step += 1) {\n\t\tconst period = buildPeriod(scale, shiftGanttDate(scale, anchor, -step), locale);\n\t\tconst kept = period.units.filter((unit) => unit.end <= leadingEdge);\n\t\tif (kept.length === 0) break;\n\t\tbefore.unshift(kept);\n\t\tleadingEdge = kept[0].start;\n\t}\n\n\tconst after: GanttTimelineUnit[][] = [];\n\tlet trailingEdge = focus.end;\n\tfor (let step = 1; step <= pad; step += 1) {\n\t\tconst period = buildPeriod(scale, shiftGanttDate(scale, anchor, step), locale);\n\t\tconst kept = period.units.filter((unit) => unit.start >= trailingEdge);\n\t\tif (kept.length === 0) break;\n\t\tafter.push(kept);\n\t\ttrailingEdge = kept[kept.length - 1].end;\n\t}\n\n\tconst chunks = [...before, focus.units, ...after];\n\tconst periodStarts: number[] = [];\n\tlet cursor = 0;\n\tfor (const chunk of chunks) {\n\t\tperiodStarts.push(cursor);\n\t\tcursor += chunk.length;\n\t}\n\tconst units = chunks.flat();\n\treturn {\n\t\tstart: units[0]?.start ?? focus.start,\n\t\tend: units.at(-1)?.end ?? focus.end,\n\t\tunits,\n\t\tgroups: groupUnits(units),\n\t\tperiodStarts,\n\t\tperiodIndex: before.length,\n\t\tcolumnWidth: focus.columnWidth,\n\t\twidth: units.length * focus.columnWidth,\n\t\ttitle: focus.title\n\t};\n}\n\nexport function getFocusUnitCount(timeline: GanttTimeline): number {\n\tconst start = timeline.periodStarts[timeline.periodIndex] ?? 0;\n\tconst end = timeline.periodStarts[timeline.periodIndex + 1] ?? timeline.units.length;\n\treturn Math.max(0, end - start);\n}\n\nexport function getPeriodX(timeline: GanttTimeline, periodIndex: number): number {\n\treturn (timeline.periodStarts[periodIndex] ?? 0) * timeline.columnWidth;\n}\n\nexport function getPeriodIndexAtX(timeline: GanttTimeline, x: number): number {\n\tif (timeline.columnWidth <= 0 || timeline.units.length === 0) return timeline.periodIndex;\n\tconst unitIndex = Math.floor(x / timeline.columnWidth);\n\tif (unitIndex < 0) return 0;\n\tif (unitIndex >= timeline.units.length) return timeline.periodStarts.length - 1;\n\tlet index = 0;\n\tfor (const [period, start] of timeline.periodStarts.entries()) {\n\t\tif (start > unitIndex) break;\n\t\tindex = period;\n\t}\n\treturn index;\n}\n\nexport function timelineXToDate(x: number, timeline: GanttTimeline): Date {\n\tif (timeline.units.length === 0 || timeline.columnWidth <= 0) return new Date(timeline.start);\n\tconst clamped = Math.max(0, Math.min(x, timeline.width));\n\tconst index = Math.min(timeline.units.length - 1, Math.floor(clamped / timeline.columnWidth));\n\tconst unit = timeline.units[index];\n\tconst fraction = (clamped - index * timeline.columnWidth) / timeline.columnWidth;\n\treturn new Date(unit.start.getTime() + fraction * (unit.end.getTime() - unit.start.getTime()));\n}\n\nexport function stretchTimeline(timeline: GanttTimeline, availableWidth: number): GanttTimeline {\n\tif (!Number.isFinite(availableWidth) || timeline.units.length === 0) return timeline;\n\tconst focusUnits = getFocusUnitCount(timeline);\n\tif (focusUnits === 0) return timeline;\n\tconst fitted = Math.floor(availableWidth / focusUnits);\n\tif (fitted <= timeline.columnWidth) return timeline;\n\treturn { ...timeline, columnWidth: fitted, width: fitted * timeline.units.length };\n}\n\nexport function getOffDaySpans(timeline: GanttTimeline): GanttOffDaySpan[] {\n\tconst spans: GanttOffDaySpan[] = [];\n\tfor (const [index, unit] of timeline.units.entries()) {\n\t\tif (!unit.isOffDay) continue;\n\t\tconst left = index * timeline.columnWidth;\n\t\tconst previous = spans.at(-1);\n\t\tif (previous && previous.left + previous.width === left) previous.width += timeline.columnWidth;\n\t\telse spans.push({ left, width: timeline.columnWidth });\n\t}\n\treturn spans;\n}\n\nexport function dateToTimelineX(date: Date, timeline: GanttTimeline): number {\n\tif (date <= timeline.start) return 0;\n\tif (date >= timeline.end) return timeline.width;\n\tconst unitIndex = timeline.units.findIndex((unit) => date < unit.end);\n\tconst unit = timeline.units[Math.max(0, unitIndex)];\n\tconst duration = unit.end.getTime() - unit.start.getTime();\n\tconst fraction = duration > 0 ? (date.getTime() - unit.start.getTime()) / duration : 0;\n\treturn (Math.max(0, unitIndex) + Math.max(0, Math.min(1, fraction))) * timeline.columnWidth;\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-timeline.ts"
		},
		{
			"content": "<script lang=\"ts\">\n\timport type { DateValue } from '@internationalized/date';\n\timport CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';\n\timport ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';\n\timport ChevronRightIcon from '@lucide/svelte/icons/chevron-right';\n\timport Maximize2Icon from '@lucide/svelte/icons/maximize-2';\n\timport Minimize2Icon from '@lucide/svelte/icons/minimize-2';\n\n\timport { Button } from '$UI$/button/index.js';\n\timport { Calendar } from '$UI$/calendar/index.js';\n\timport * as Popover from '$UI$/popover/index.js';\n\timport * as Select from '$UI$/select/index.js';\n\n\timport type { GanttScale } from './gantt-types.js';\n\n\tconst scales: Array<{ label: string; value: GanttScale }> = [\n\t\t{ value: 'day', label: 'Day' },\n\t\t{ value: 'week', label: 'Week' },\n\t\t{ value: 'month', label: 'Month' },\n\t\t{ value: 'quarter', label: 'Quarter' },\n\t\t{ value: 'year', label: 'Year' }\n\t];\n\n\tlet {\n\t\tanchorValue,\n\t\tisFullscreen,\n\t\tlocale,\n\t\tonDatePicked,\n\t\tonGoToToday,\n\t\tonNavigate,\n\t\tonScaleChange,\n\t\tonToggleFullscreen,\n\t\tscale,\n\t\ttimelineTitle\n\t}: {\n\t\tanchorValue: DateValue;\n\t\tisFullscreen: boolean;\n\t\tlocale: string;\n\t\tonDatePicked: (value: DateValue | undefined) => void;\n\t\tonGoToToday: () => void;\n\t\tonNavigate: (direction: -1 | 1) => void;\n\t\tonScaleChange: (scale: GanttScale) => void;\n\t\tonToggleFullscreen: () => void;\n\t\tscale: GanttScale;\n\t\ttimelineTitle: string;\n\t} = $props();\n\n\tlet datePickerOpen = $state(false);\n\tlet scaleLabel = $derived(scales.find((item) => item.value === scale)?.label ?? 'Month');\n\n\tfunction handleDatePicked(value: DateValue | undefined): void {\n\t\tif (!value) return;\n\t\tonDatePicked(value);\n\t\tdatePickerOpen = false;\n\t}\n\n\tfunction handleScaleChange(value: string): void {\n\t\tconst selectedScale = scales.find((item) => item.value === value)?.value;\n\t\tif (selectedScale) onScaleChange(selectedScale);\n\t}\n</script>\n\n<div class=\"flex flex-wrap items-center gap-2\">\n\t<Popover.Root bind:open={datePickerOpen}>\n\t\t<Popover.Trigger>\n\t\t\t{#snippet child({ props })}\n\t\t\t\t<Button {...props} variant=\"outline\" size=\"sm\" aria-label=\"Go to date\">\n\t\t\t\t\t<CalendarDaysIcon data-icon=\"inline-start\" />\n\t\t\t\t\t{timelineTitle}\n\t\t\t\t</Button>\n\t\t\t{/snippet}\n\t\t</Popover.Trigger>\n\t\t<Popover.Content class=\"w-auto p-0\" align=\"end\">\n\t\t\t<Calendar\n\t\t\t\ttype=\"single\"\n\t\t\t\t{locale}\n\t\t\t\tvalue={anchorValue}\n\t\t\t\tonValueChange={handleDatePicked}\n\t\t\t\tcaptionLayout=\"dropdown\"\n\t\t\t/>\n\t\t</Popover.Content>\n\t</Popover.Root>\n\t<Button variant=\"outline\" size=\"sm\" onclick={onGoToToday}>Today</Button>\n\t<Select.Root type=\"single\" value={scale} onValueChange={handleScaleChange}>\n\t\t<Select.Trigger size=\"sm\" class=\"w-28\" aria-label=\"Timeline scale\">\n\t\t\t{scaleLabel}\n\t\t</Select.Trigger>\n\t\t<Select.Content>\n\t\t\t<Select.Group>\n\t\t\t\t{#each scales as item (item.value)}\n\t\t\t\t\t<Select.Item value={item.value}>{item.label}</Select.Item>\n\t\t\t\t{/each}\n\t\t\t</Select.Group>\n\t\t</Select.Content>\n\t</Select.Root>\n\t<div class=\"flex items-center gap-1\">\n\t\t<Button\n\t\t\tvariant=\"outline\"\n\t\t\tsize=\"icon-sm\"\n\t\t\taria-label=\"Previous period\"\n\t\t\tonclick={() => onNavigate(-1)}\n\t\t>\n\t\t\t<ChevronLeftIcon />\n\t\t</Button>\n\t\t<Button\n\t\t\tvariant=\"outline\"\n\t\t\tsize=\"icon-sm\"\n\t\t\taria-label=\"Next period\"\n\t\t\tonclick={() => onNavigate(1)}\n\t\t>\n\t\t\t<ChevronRightIcon />\n\t\t</Button>\n\t</div>\n\t<Button\n\t\tvariant=\"outline\"\n\t\tsize=\"icon-sm\"\n\t\taria-label={isFullscreen ? 'Exit full screen' : 'Enter full screen'}\n\t\ttitle={isFullscreen ? 'Exit full screen' : 'Enter full screen'}\n\t\tonclick={onToggleFullscreen}\n\t>\n\t\t{#if isFullscreen}\n\t\t\t<Minimize2Icon />\n\t\t{:else}\n\t\t\t<Maximize2Icon />\n\t\t{/if}\n\t</Button>\n</div>\n",
			"type": "registry:file",
			"target": "gantt/gantt-toolbar.svelte"
		},
		{
			"content": "import type { Snippet } from 'svelte';\n\nexport type GanttScale = 'day' | 'month' | 'quarter' | 'week' | 'year';\n\nexport type GanttEventTone = 'accent' | 'destructive' | 'muted' | 'primary' | 'secondary';\n\n/** A row in the Gantt tree. Nested resources render as collapsible groups. */\nexport interface GanttResource {\n\t/** Set false when child percentages cannot be meaningfully aggregated. */\n\taggregateProgress?: boolean;\n\tchildren?: GanttResource[];\n\tid: string;\n\t/** Word shown after summary progress, such as \"complete\" or \"received\". */\n\tprogressLabel?: string;\n\ttitle: string;\n}\n\n/**\n * A scheduled bar. `end` is exclusive, matching common calendar APIs.\n * For an all-day task covering July 6–10, pass July 6 as `start` and July 11 as `end`.\n */\nexport interface GanttEvent<TData = unknown> {\n\tallDay?: boolean;\n\tdata?: TData;\n\tend: Date;\n\tid: string;\n\tprogress?: number;\n\t/** Word shown after progress in the tooltip. Defaults to \"complete\". */\n\tprogressLabel?: string;\n\treadOnly?: boolean;\n\tresourceId: string;\n\tstart: Date;\n\ttitle: string;\n\ttone?: GanttEventTone;\n}\n\nexport interface GanttProps<TData = unknown> {\n\t/**\n\t * `card` wraps the chart in its own titled card. Use `bare` when the chart is\n\t * already inside a card that carries the heading, so the two do not nest.\n\t */\n\tchrome?: 'bare' | 'card';\n\tclass?: string;\n\tdefaultDate?: Date;\n\tdefaultScale?: GanttScale;\n\tdescription?: string;\n\tevents: GanttEvent<TData>[];\n\tloading?: boolean;\n\tlocale?: string;\n\tonEventClick?: (event: GanttEvent<TData>) => void;\n\t/**\n\t * Periods rendered either side of the focused one, so adjacent work can be\n\t * scrolled to rather than stepped to. Raise it for more context at the cost of\n\t * columns; `0` restores a single-period view where stepping is the only way out.\n\t */\n\tpadPeriods?: number;\n\tresources: GanttResource[];\n\tscale?: GanttScale;\n\ttitle?: string;\n\t/** Optional controls rendered at the start of the chart toolbar. */\n\ttoolbarStart?: Snippet;\n\ttreeWidth?: number;\n}\n",
			"type": "registry:file",
			"target": "gantt/gantt-types.ts"
		},
		{
			"content": "<script lang=\"ts\" generics=\"TData = unknown\">\n\timport { CalendarDate, type DateValue, getLocalTimeZone } from '@internationalized/date';\n\timport { flushSync, type Snippet, untrack } from 'svelte';\n\n\timport { Badge } from '$UI$/badge/index.js';\n\timport * as Card from '$UI$/card/index.js';\n\timport * as Tooltip from '$UI$/tooltip/index.js';\n\timport { cn } from '$UTILS$.js';\n\n\timport type { GanttProps, GanttScale } from './gantt-types.js';\n\n\timport GanttChart from './gantt-chart.svelte';\n\timport { resolveGanttDate, shiftGanttDate } from './gantt-date.js';\n\timport { flattenResources } from './gantt-resources.js';\n\timport { buildGanttRows, getTodayX } from './gantt-rows.js';\n\timport {\n\t\tbuildTimeline,\n\t\tdateToTimelineX,\n\t\tgetFocusUnitCount,\n\t\tgetOffDaySpans,\n\t\tgetPeriodIndexAtX,\n\t\tgetPeriodX,\n\t\tstretchTimeline,\n\t\ttimelineXToDate\n\t} from './gantt-timeline.js';\n\timport GanttToolbar from './gantt-toolbar.svelte';\n\n\t// Keep the bindable prop explicit here so Svelte's generated component type exposes it.\n\ttype InteractiveGanttProps<TData> = GanttProps<TData> & {\n\t\tscale?: GanttScale;\n\t\ttoolbarStart?: Snippet;\n\t};\n\n\t/** Two steps closer together than this land immediately instead of animating. */\n\tconst NAVIGATION_SETTLE_MS = 400;\n\t/** How far past a period boundary the viewport centre must sit to re-anchor. */\n\tconst AUTO_NAVIGATE_DEADBAND_RATIO = 0.15;\n\t/** Longest a smooth scroll is trusted to still be running, if it never arrives. */\n\tconst SMOOTH_SCROLL_TIMEOUT_MS = 1500;\n\t/** Sub-pixel scroll offsets differ after a round trip through the DOM. */\n\tconst SCROLL_EPSILON = 1;\n\n\tlet {\n\t\tresources,\n\t\tevents,\n\t\tchrome = 'card',\n\t\tdefaultScale = 'month',\n\t\tscale = $bindable(defaultScale),\n\t\tdefaultDate = new Date(),\n\t\ttitle = 'Project timeline',\n\t\tdescription = 'Review planned work, dates, and completion progress.',\n\t\ttoolbarStart,\n\t\tlocale = 'en-US',\n\t\ttreeWidth = 360,\n\t\tpadPeriods = 1,\n\t\tloading = false,\n\t\tclass: className,\n\t\tonEventClick\n\t}: InteractiveGanttProps<TData> = $props();\n\tlet anchorDate = $state(untrack(() => resolveGanttDate(defaultDate)));\n\tlet collapsedIds = $state.raw<string[]>([]);\n\tlet isFullscreen = $state(false);\n\tlet fullscreenTransitioning = $state(false);\n\tlet viewportWidth = $state(0);\n\tlet scrollElement = $state<HTMLDivElement | null>(null);\n\tlet navigationSeq = $state(0);\n\t/** Pending offsets are consumed by the scroll synchronization effect. */\n\tlet pendingScrollFrom: null | number = null;\n\tlet pendingScrollX: null | number = null;\n\tlet lastNavigationAt = 0;\n\tlet scrollFramePending = false;\n\t/** Offset an in-flight smooth scroll is heading for, with its progress. */\n\tlet smoothScrollX: null | number = null;\n\tlet smoothScrollDistance = Number.POSITIVE_INFINITY;\n\tlet smoothScrollExpiresAt = 0;\n\tlet scrollbarHidden = $state(false);\n\t/** True while a period step's smooth scroll is in flight. */\n\tlet navigating = $state(false);\n\tlet scrollbarRevealTimer: ReturnType<typeof setTimeout> | undefined;\n\t/** True between pointerdown on the proxy scrollbar and the pointer's release. */\n\tlet scrollbarDragging = false;\n\n\tlet collapsed = $derived(new Set(collapsedIds));\n\tlet timeline = $derived(\n\t\tstretchTimeline(buildTimeline(scale, anchorDate, locale, padPeriods), viewportWidth - treeWidth)\n\t);\n\tlet flatRows = $derived(flattenResources(resources, collapsed));\n\tlet rows = $derived.by(() => buildGanttRows(flatRows, events, timeline));\n\tlet todayX = $derived(getTodayX(timeline));\n\tlet showOffDays = $derived(scale === 'week' || scale === 'month');\n\tlet offDaySpans = $derived(showOffDays ? getOffDaySpans(timeline) : []);\n\tlet anchorValue = $derived(\n\t\tnew CalendarDate(anchorDate.getFullYear(), anchorDate.getMonth() + 1, anchorDate.getDate())\n\t);\n\tlet scrollTarget = $derived({\n\t\tseq: navigationSeq,\n\t\tx: getPeriodX(timeline, timeline.periodIndex)\n\t});\n\n\t$effect(() => {\n\t\tif (!isFullscreen) return;\n\n\t\tconst root = document.documentElement;\n\t\tconst previousRootOverflow = root.style.overflow;\n\t\tconst previousBodyOverflow = document.body.style.overflow;\n\t\troot.style.overflow = 'hidden';\n\t\tdocument.body.style.overflow = 'hidden';\n\n\t\treturn () => {\n\t\t\troot.style.overflow = previousRootOverflow;\n\t\t\tdocument.body.style.overflow = previousBodyOverflow;\n\t\t};\n\t});\n\n\t// The focused period is an offset into a wider range. Reposition after the new\n\t// columns reach the DOM, preserving a smooth transition for button navigation.\n\t$effect(() => {\n\t\tconst { x: focusX } = scrollTarget;\n\t\tconst element = scrollElement;\n\t\tif (!element) return;\n\n\t\tconst target = pendingScrollX ?? focusX;\n\t\tconst from = pendingScrollFrom;\n\t\tpendingScrollX = null;\n\t\tpendingScrollFrom = null;\n\n\t\tif (from === null || from === target) {\n\t\t\tsmoothScrollX = null;\n\t\t\telement.scrollLeft = target;\n\t\t\treturn;\n\t\t}\n\n\t\telement.scrollLeft = Math.min(from, element.scrollWidth - element.clientWidth);\n\t\tsmoothScrollX = target;\n\t\tsmoothScrollDistance = Number.POSITIVE_INFINITY;\n\t\tsmoothScrollExpiresAt = performance.now() + SMOOTH_SCROLL_TIMEOUT_MS;\n\t\thideScrollbarWhileNavigating();\n\t\telement.scrollTo({ left: target, behavior: 'smooth' });\n\t});\n\n\t$effect(() => () => clearTimeout(scrollbarRevealTimer));\n\n\t// Stepping a period slides the whole timeline window, so the thumb's offset is\n\t// discontinuous even though the content scrolls smoothly. Hide it rather than\n\t// show it teleporting, and fade it back once the new window has settled.\n\tfunction hideScrollbarWhileNavigating(): void {\n\t\tnavigating = true;\n\t\tscrollbarHidden = true;\n\t\tclearTimeout(scrollbarRevealTimer);\n\t\tscrollbarRevealTimer = setTimeout(revealScrollbar, SMOOTH_SCROLL_TIMEOUT_MS);\n\t}\n\n\tfunction revealScrollbar(): void {\n\t\tclearTimeout(scrollbarRevealTimer);\n\t\tscrollbarRevealTimer = undefined;\n\t\tscrollbarHidden = false;\n\t\tnavigating = false;\n\t}\n\n\t/**\n\t * Auto-navigation must stay out of the way until a smooth scroll settles: it\n\t * reads the outgoing period as the viewport centre for most of the animation,\n\t * and re-anchoring there would both cancel the scroll and undo the step.\n\t */\n\tfunction isSmoothScrolling(element: HTMLDivElement): boolean {\n\t\tif (smoothScrollX === null) return false;\n\n\t\t// A smooth scroll closes on its target every frame. Anything else — target\n\t\t// reached, a user scroll taking over, a dropped animation — releases the block.\n\t\tconst distance = Math.abs(element.scrollLeft - smoothScrollX);\n\t\tconst advancing = distance >= SCROLL_EPSILON && distance < smoothScrollDistance;\n\t\tif (advancing && performance.now() <= smoothScrollExpiresAt) {\n\t\t\tsmoothScrollDistance = distance;\n\t\t\treturn true;\n\t\t}\n\n\t\tsmoothScrollX = null;\n\t\trevealScrollbar();\n\t\treturn false;\n\t}\n\n\tfunction toggleResource(id: string): void {\n\t\tcollapsedIds = collapsedIds.includes(id)\n\t\t\t? collapsedIds.filter((collapsedId) => collapsedId !== id)\n\t\t\t: [...collapsedIds, id];\n\t}\n\n\tfunction setAnchor(date: Date): void {\n\t\tanchorDate = date;\n\t\tpendingScrollFrom = null;\n\t\tpendingScrollX = null;\n\t\tnavigationSeq += 1;\n\t}\n\n\t/**\n\t * Re-anchor when scrolling into padding while keeping the left-edge date fixed.\n\t * Reports whether the timeline window actually moved.\n\t */\n\tfunction syncAnchorToScroll(): boolean {\n\t\tconst element = scrollElement;\n\t\tif (!element || scrollbarDragging || isSmoothScrolling(element)) return false;\n\n\t\tconst center = element.scrollLeft + Math.max(0, element.clientWidth - treeWidth) / 2;\n\t\tconst target = getPeriodIndexAtX(timeline, center);\n\t\tif (target === timeline.periodIndex) return false;\n\n\t\tconst forward = target > timeline.periodIndex;\n\t\tconst boundary = getPeriodX(timeline, forward ? target : target + 1);\n\t\tconst margin =\n\t\t\tgetFocusUnitCount(timeline) * timeline.columnWidth * AUTO_NAVIGATE_DEADBAND_RATIO;\n\t\tif (forward ? center < boundary + margin : center > boundary - margin) return false;\n\n\t\tconst steps = target - timeline.periodIndex;\n\t\tconst edgeDate = timelineXToDate(element.scrollLeft, timeline);\n\t\tsetAnchor(shiftGanttDate(scale, anchorDate, steps));\n\t\tpendingScrollX = dateToTimelineX(edgeDate, timeline);\n\t\treturn true;\n\t}\n\n\t/*\n\t * A native scrollbar drag keeps following the pointer after the thumb moves, so\n\t * re-anchoring mid-drag teleports the thumb back to the middle underneath a still\n\t * held cursor and the browser immediately drags it away again — one grab scrolls\n\t * forever. Hold the window still for the length of the drag instead, then apply\n\t * the deferred step on release, with the thumb hidden across the jump exactly as\n\t * period stepping hides it.\n\t */\n\tfunction handleScrollbarDragStart(): void {\n\t\tscrollbarDragging = true;\n\t}\n\n\tfunction handleScrollbarDragEnd(): void {\n\t\tif (!scrollbarDragging) return;\n\t\tscrollbarDragging = false;\n\n\t\tscrollbarHidden = true;\n\t\tif (!syncAnchorToScroll()) {\n\t\t\tscrollbarHidden = false;\n\t\t\treturn;\n\t\t}\n\n\t\t// Repositioning happens in the scroll synchronization effect; fade the thumb\n\t\t// back in on the frame after it has been mirrored to its recentred offset.\n\t\trequestAnimationFrame(() => requestAnimationFrame(revealScrollbar));\n\t}\n\n\tfunction handleScroll(): void {\n\t\tif (scrollFramePending) return;\n\t\tscrollFramePending = true;\n\t\trequestAnimationFrame(() => {\n\t\t\tscrollFramePending = false;\n\t\t\tsyncAnchorToScroll();\n\t\t});\n\t}\n\n\tfunction prefersReducedMotion(): boolean {\n\t\treturn window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\t}\n\n\tfunction navigate(direction: -1 | 1): void {\n\t\tconst now = performance.now();\n\t\tconst animate = now - lastNavigationAt > NAVIGATION_SETTLE_MS && !prefersReducedMotion();\n\t\tlastNavigationAt = now;\n\t\tconst outgoing = timeline.periodIndex - direction;\n\t\tsetAnchor(shiftGanttDate(scale, anchorDate, direction));\n\t\tif (animate) pendingScrollFrom = getPeriodX(timeline, outgoing);\n\t}\n\n\tfunction goToToday(): void {\n\t\tsetAnchor(new Date());\n\t}\n\n\tfunction handleScaleChange(nextScale: GanttScale): void {\n\t\tscale = nextScale;\n\t}\n\n\tfunction setFullscreen(value: boolean): void {\n\t\tconst update = () =>\n\t\t\tflushSync(() => {\n\t\t\t\tisFullscreen = value;\n\t\t\t});\n\t\tif (prefersReducedMotion() || !('startViewTransition' in document)) {\n\t\t\tupdate();\n\t\t\treturn;\n\t\t}\n\n\t\tflushSync(() => {\n\t\t\tfullscreenTransitioning = true;\n\t\t});\n\t\tconst transition = document.startViewTransition(update);\n\t\tconst finish = () => {\n\t\t\tfullscreenTransitioning = false;\n\t\t};\n\t\tvoid transition.finished.then(finish, finish);\n\t}\n\n\tfunction toggleFullscreen(): void {\n\t\tsetFullscreen(!isFullscreen);\n\t}\n\n\tfunction handleWindowKeydown(event: KeyboardEvent): void {\n\t\tif (event.key === 'Escape' && !event.defaultPrevented && isFullscreen) setFullscreen(false);\n\t}\n\n\tfunction goToDate(date: Date): void {\n\t\tsetAnchor(resolveGanttDate(date));\n\t}\n\n\tfunction handleDatePicked(value: DateValue | undefined): void {\n\t\tif (value) goToDate(value.toDate(getLocalTimeZone()));\n\t}\n</script>\n\n<svelte:window onkeydown={handleWindowKeydown} />\n\n{#snippet toolbar()}\n\t<GanttToolbar\n\t\t{scale}\n\t\t{anchorValue}\n\t\t{locale}\n\t\t{isFullscreen}\n\t\ttimelineTitle={timeline.title}\n\t\tonDatePicked={handleDatePicked}\n\t\tonGoToToday={goToToday}\n\t\tonNavigate={navigate}\n\t\tonScaleChange={handleScaleChange}\n\t\tonToggleFullscreen={toggleFullscreen}\n\t/>\n{/snippet}\n\n{#snippet chart()}\n\t{#if resources.length === 0 && !loading}\n\t\t<div class=\"flex min-h-64 flex-col items-center justify-center gap-2 px-6 text-center\">\n\t\t\t<p class=\"font-medium\">No timeline items</p>\n\t\t\t<p class=\"max-w-sm text-sm text-muted-foreground\">\n\t\t\t\tAdd resources and scheduled events to populate this Gantt chart.\n\t\t\t</p>\n\t\t</div>\n\t{:else}\n\t\t<GanttChart\n\t\t\tbind:scrollElement\n\t\t\tbind:viewportWidth\n\t\t\t{collapsed}\n\t\t\t{isFullscreen}\n\t\t\t{loading}\n\t\t\t{locale}\n\t\t\t{navigating}\n\t\t\t{offDaySpans}\n\t\t\t{rows}\n\t\t\t{scale}\n\t\t\t{scrollbarHidden}\n\t\t\t{timeline}\n\t\t\t{title}\n\t\t\t{todayX}\n\t\t\t{treeWidth}\n\t\t\t{onEventClick}\n\t\t\tonGoToDate={goToDate}\n\t\t\tonScroll={handleScroll}\n\t\t\tonScrollbarDragEnd={handleScrollbarDragEnd}\n\t\t\tonScrollbarDragStart={handleScrollbarDragStart}\n\t\t\tonToggleResource={toggleResource}\n\t\t/>\n\t{/if}\n{/snippet}\n\n<Tooltip.Provider delayDuration={150}>\n\t{#if chrome === 'bare'}\n\t\t<!-- The surrounding card owns the heading; only the toolbar and chart belong here. -->\n\t\t<div\n\t\t\tclass={cn(\n\t\t\t\t'flex flex-col gap-3',\n\t\t\t\tclassName,\n\t\t\t\tisFullscreen && 'fixed inset-0 z-50 h-dvh w-dvw min-h-0 gap-0 bg-card'\n\t\t\t)}\n\t\t\tstyle={`view-transition-name: ${fullscreenTransitioning ? 'gantt-fullscreen' : 'none'}`}\n\t\t>\n\t\t\t<div\n\t\t\t\tclass={cn(\n\t\t\t\t\t'flex flex-wrap items-center gap-2',\n\t\t\t\t\tisFullscreen && 'border-b px-6 py-4'\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{#if toolbarStart}\n\t\t\t\t\t{@render toolbarStart()}\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"ml-auto\">{@render toolbar()}</div>\n\t\t\t</div>\n\t\t\t<div class={cn('overflow-hidden rounded-md border', isFullscreen && 'flex min-h-0 flex-1 flex-col rounded-none border-0')}>\n\t\t\t\t{@render chart()}\n\t\t\t</div>\n\t\t</div>\n\t{:else}\n\t<Card.Root\n\t\tclass={cn(\n\t\t\t'gap-0 py-0',\n\t\t\tclassName,\n\t\t\tisFullscreen && 'fixed inset-0 z-50 h-dvh w-dvw rounded-none'\n\t\t)}\n\t\tstyle={`view-transition-name: ${fullscreenTransitioning ? 'gantt-fullscreen' : 'none'}`}\n\t>\n\t\t<Card.Header class=\"grid-cols-1 border-b py-4 sm:grid-cols-[1fr_auto]\">\n\t\t\t<Card.Title class=\"flex items-center gap-2\">\n\t\t\t\t{title}\n\t\t\t\t<Badge variant=\"secondary\">{events.length}</Badge>\n\t\t\t</Card.Title>\n\t\t\t<Card.Description>{description}</Card.Description>\n\t\t\t<Card.Action\n\t\t\t\tclass=\"col-start-1 row-span-1 row-start-3 flex flex-wrap items-center gap-2 justify-self-stretch sm:col-start-2 sm:row-span-2 sm:row-start-1 sm:justify-self-end\"\n\t\t\t>\n\t\t\t\t{#if toolbarStart}\n\t\t\t\t\t{@render toolbarStart()}\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"ml-auto\">{@render toolbar()}</div>\n\t\t\t</Card.Action>\n\t\t</Card.Header>\n\n\t\t<Card.Content class={cn('p-0', isFullscreen && 'flex min-h-0 flex-1 flex-col')}>\n\t\t\t{@render chart()}\n\t\t</Card.Content>\n\t</Card.Root>\n\t{/if}\n</Tooltip.Provider>\n",
			"type": "registry:file",
			"target": "gantt/gantt.svelte"
		},
		{
			"content": "export type {\n\tGanttEvent,\n\tGanttEventTone,\n\tGanttProps,\n\tGanttResource,\n\tGanttScale\n} from './gantt-types.js';\nexport { default as Gantt } from './gantt.svelte';\n",
			"type": "registry:file",
			"target": "gantt/index.ts"
		}
	]
}