{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "gantt",
  "title": "Gantt",
  "type": "registry:ui",
  "description": "Interactive Gantt chart primitive with multi-scale timeline (day/week/month/year), collapsible task tree, progress fill, milestone markers, dependency lines, and right-click GanttContextMenu.",
  "categories": [
    "data-display"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/gantt/gantt.ts",
      "content": "import { LitElement, css, html, nothing, svg, type PropertyDeclarations } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { Calendar, Clock, Copy, Edit2, Flag, Layers, Trash2 } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport '../button/button'\nimport '../button-group/button-group'\nimport '../context-menu/context-menu'\nimport type { GanttScale, GanttTask, GanttTaskPriority, GanttTaskStatus } from './types'\n\nexport type * from './types'\n\n// React's class strings, verbatim (packages/registry-react/components/gantt).\nconst statusColors: Record<string, string> = {\n  done: 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300 border-emerald-500/40',\n  'in-progress': 'bg-primary/20 text-primary border-primary/40',\n  'at-risk': 'bg-amber-500/20 text-amber-700 dark:text-amber-300 border-amber-500/40',\n  todo: 'bg-muted/80 text-muted-foreground border-border',\n  blocked: 'bg-destructive/20 text-destructive border-destructive/40',\n}\n\nconst progressColors: Record<string, string> = {\n  done: 'bg-emerald-500/40',\n  'in-progress': 'bg-primary/40',\n  'at-risk': 'bg-amber-500/40',\n  todo: 'bg-muted-foreground/20',\n  blocked: 'bg-destructive/40',\n}\n\nconst priorityColors: Record<string, string> = {\n  urgent: 'text-destructive',\n  high: 'text-amber-500',\n  medium: 'text-primary',\n  low: 'text-muted-foreground/60',\n}\n\nconst ONE_DAY = 1000 * 60 * 60 * 24\n\nfunction calculateDays(startDate: string, endDate: string) {\n  const diff = new Date(endDate).getTime() - new Date(startDate).getTime()\n  const days = Math.max(1, Math.ceil(diff / ONE_DAY))\n  return `${days}d`\n}\n\nconst statusItems: { value: GanttTaskStatus; label: string; dot: string }[] = [\n  { value: 'done', label: 'Completed', dot: 'mr-2 size-2 rounded-full bg-emerald-500' },\n  { value: 'in-progress', label: 'In Progress', dot: 'bg-primary mr-2 size-2 rounded-full' },\n  { value: 'at-risk', label: 'At Risk', dot: 'mr-2 size-2 rounded-full bg-amber-500' },\n  { value: 'blocked', label: 'Blocked', dot: 'bg-destructive mr-2 size-2 rounded-full' },\n  { value: 'todo', label: 'To Do', dot: 'bg-muted-foreground/40 mr-2 size-2 rounded-full' },\n]\n\nconst priorityItems: { value: GanttTaskPriority; label: string; flag: string }[] = [\n  { value: 'urgent', label: 'Urgent', flag: 'text-destructive mr-2 size-3' },\n  { value: 'high', label: 'High', flag: 'mr-2 size-3 text-amber-500' },\n  { value: 'medium', label: 'Medium', flag: 'text-primary mr-2 size-3' },\n  { value: 'low', label: 'Low', flag: 'text-muted-foreground mr-2 size-3' },\n]\n\nconst boolDefaultTrue = { type: Boolean, converter: { fromAttribute: (v: string | null) => v !== null && v !== 'false' } }\n\n/**\n * <uip-gantt> — the registry Gantt (Gantt + GanttHeader + GanttTree +\n * GanttTimeline, and optionally GanttContextMenu) as ONE web component, laid\n * out like the React demo: header on top, tree on the left, horizontally\n * scrolling timeline on the right.\n *\n *   <uip-gantt class=\"h-96\" scale=\"day\" header-title=\"Product Engineering Q3\"\n *     tasks='[{\"id\":\"t-1\",\"name\":\"Discovery\",\"startDate\":\"2026-08-01\",\"endDate\":\"2026-08-08\"}]'>\n *     <uip-button slot=\"actions\" size=\"xs\" variant=\"outline\">Export</uip-button>\n *   </uip-gantt>\n *\n * Properties / attributes (React prop → attribute):\n *   tasks (JSON), scale (`day` | `week` | `month` | `year`), startDate → `start-date`,\n *   endDate → `end-date`, rowHeight → `row-height` (40), headerHeight → `header-height` (48),\n *   treeWidth → `tree-width` (280).\n *   GanttHeader: title → `header-title` (\"Project Timeline\"), showScaleSwitcher →\n *   `show-scale-switcher` (true; `=\"false\"` turns it off), `actions` → `slot=\"actions\"`.\n *   GanttTree: showPriority → `show-priority` (true), showAssignee → `show-assignee` (true, unused — as in React).\n *   GanttTimeline: showTodayLine → `show-today-line` (true), showDependencies → `show-dependencies` (true).\n *   `context-menu`: wrap every task row and bar in React's GanttContextMenu.\n *\n * Events (bubble, composed; payload in `detail`):\n *   `task-click` { task } (onTaskClick), `scale-change` { scale } (onScaleChange —\n *   the scale switcher also updates `scale` itself; preventDefault to keep it controlled),\n *   and with `context-menu`: `edit` { task }, `status-change` { task, status },\n *   `priority-change` { task, priority }, `duplicate` { task }, `delete` { task }\n *   (GanttContextMenu's onEdit / onStatusChange / onPriorityChange / onDuplicate / onDelete).\n *\n * Styling: `class` on the host (e.g. `h-96`) sizes it; `::part(base)` is the\n * card, plus parts `header`, `body`, `tree`, `timeline`.\n *\n * Intentionally data-driven: React's composable GanttHeader/Tree/Timeline/Bar\n * parts render here from `tasks` + props (`actions` slot + parts cover the\n * common customizations); custom row/bar renderers are niche next to the main\n * props and stay out of the web-component API.\n */\nexport class UipGantt extends LitElement {\n  // Flex host so the card stretches to a height set on the host (`h-96`).\n  static styles = [tailwind, css`:host { display: flex; width: 100%; }`]\n\n  static properties: PropertyDeclarations = {\n    tasks: { type: Array },\n    scale: { reflect: true },\n    startDate: { attribute: 'start-date' },\n    endDate: { attribute: 'end-date' },\n    rowHeight: { type: Number, attribute: 'row-height' },\n    headerHeight: { type: Number, attribute: 'header-height' },\n    treeWidth: { type: Number, attribute: 'tree-width' },\n    headerTitle: { attribute: 'header-title' },\n    showScaleSwitcher: { ...boolDefaultTrue, attribute: 'show-scale-switcher' },\n    showPriority: { ...boolDefaultTrue, attribute: 'show-priority' },\n    showAssignee: { ...boolDefaultTrue, attribute: 'show-assignee' },\n    showTodayLine: { ...boolDefaultTrue, attribute: 'show-today-line' },\n    showDependencies: { ...boolDefaultTrue, attribute: 'show-dependencies' },\n    contextMenu: { type: Boolean, attribute: 'context-menu' },\n    menuTask: { state: true },\n  }\n\n  tasks: GanttTask[] = []\n  scale: GanttScale = 'day'\n  startDate?: string\n  endDate?: string\n  rowHeight = 40\n  headerHeight = 48\n  treeWidth = 280\n  headerTitle = 'Project Timeline'\n  showScaleSwitcher = true\n  showPriority = true\n  showAssignee = true\n  showTodayLine = true\n  showDependencies = true\n  contextMenu = false\n  private menuTask: GanttTask | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'gantt')\n  }\n\n  // --- derived (Gantt.tsx) -------------------------------------------------------\n  private get resolvedStartDate() {\n    const tasks = this.tasks ?? []\n    if (this.startDate) return new Date(this.startDate)\n    if (tasks.length === 0) return new Date()\n    const min = Math.min(...tasks.map((t) => new Date(t.startDate).getTime()))\n    const d = new Date(min)\n    d.setDate(d.getDate() - 3)\n    return d\n  }\n\n  private get resolvedEndDate() {\n    const tasks = this.tasks ?? []\n    if (this.endDate) return new Date(this.endDate)\n    if (tasks.length === 0) {\n      const d = new Date()\n      d.setDate(d.getDate() + 30)\n      return d\n    }\n    const max = Math.max(...tasks.map((t) => new Date(t.endDate).getTime()))\n    const d = new Date(max)\n    d.setDate(d.getDate() + 7)\n    return d\n  }\n\n  private get columnWidth() {\n    switch (this.scale) {\n      case 'day':\n        return 44\n      case 'week':\n        return 120\n      case 'month':\n        return 180\n      case 'year':\n        return 240\n      default:\n        return 44\n    }\n  }\n\n  private setScale(scale: GanttScale) {\n    const ok = this.dispatchEvent(\n      new CustomEvent('scale-change', { detail: { scale }, bubbles: true, composed: true, cancelable: true }),\n    )\n    if (ok) this.scale = scale\n  }\n\n  private emit(type: string, detail: Record<string, unknown>) {\n    this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }))\n  }\n\n  private onTaskClick(task: GanttTask) {\n    this.emit('task-click', { task })\n  }\n\n  // --- context menu ----------------------------------------------------------------\n  /** Right-click on a task row/bar picks the menu's task; anywhere else keeps the native menu. */\n  private onBodyContextMenu(e: MouseEvent) {\n    const el = e\n      .composedPath()\n      .find((n): n is HTMLElement => n instanceof HTMLElement && n.hasAttribute('data-task-id'))\n    const task = el ? this.tasks.find((t) => t.id === el.getAttribute('data-task-id')) : undefined\n    if (!task) {\n      e.stopPropagation()\n      return\n    }\n    this.menuTask = task\n  }\n\n  private copyTaskId(task: GanttTask) {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) navigator.clipboard.writeText(task.id)\n  }\n\n  private renderMenu() {\n    const task = this.menuTask\n    return html`<uip-context-menu-content>\n      ${task\n        ? html`<uip-context-menu-label part=\"task-label\"\n              ><span class=\"truncate font-semibold\">${task.name}</span\n              ><span class=\"text-muted-foreground font-mono text-xs\">${task.id}</span></uip-context-menu-label\n            >\n            <uip-context-menu-separator></uip-context-menu-separator>\n            <uip-context-menu-item @select=${() => this.emit('edit', { task })}\n              >${icon(Edit2, 'pen-line', 'mr-2 size-3.5')}<span>View Details</span\n              ><uip-context-menu-shortcut>↵</uip-context-menu-shortcut></uip-context-menu-item\n            >\n            <uip-context-menu-sub>\n              <uip-context-menu-sub-trigger\n                >${icon(Clock, 'clock', 'text-primary mr-2 size-3.5')}<span>Change Status</span></uip-context-menu-sub-trigger\n              >\n              <uip-context-menu-sub-content part=\"status-menu\">\n                <uip-context-menu-radio-group\n                  value=${task.status ?? 'todo'}\n                  @value-change=${(e: Event) => e.preventDefault()}\n                >\n                  ${statusItems.map(\n                    (s) =>\n                      html`<uip-context-menu-radio-item\n                        value=${s.value}\n                        @select=${() => this.emit('status-change', { task, status: s.value })}\n                        ><span class=${s.dot}></span><span>${s.label}</span></uip-context-menu-radio-item\n                      >`,\n                  )}\n                </uip-context-menu-radio-group>\n              </uip-context-menu-sub-content>\n            </uip-context-menu-sub>\n            <uip-context-menu-sub>\n              <uip-context-menu-sub-trigger\n                >${icon(Flag, 'flag', 'mr-2 size-3.5 text-amber-500')}<span>Set Priority</span></uip-context-menu-sub-trigger\n              >\n              <uip-context-menu-sub-content part=\"priority-menu\">\n                <uip-context-menu-radio-group\n                  value=${task.priority ?? 'medium'}\n                  @value-change=${(e: Event) => e.preventDefault()}\n                >\n                  ${priorityItems.map(\n                    (p) =>\n                      html`<uip-context-menu-radio-item\n                        value=${p.value}\n                        @select=${() => this.emit('priority-change', { task, priority: p.value })}\n                        >${icon(Flag, 'flag', p.flag)}<span>${p.label}</span></uip-context-menu-radio-item\n                      >`,\n                  )}\n                </uip-context-menu-radio-group>\n              </uip-context-menu-sub-content>\n            </uip-context-menu-sub>\n            <uip-context-menu-separator></uip-context-menu-separator>\n            <uip-context-menu-item @select=${() => this.copyTaskId(task)}\n              >${icon(Copy, 'copy', 'mr-2 size-3.5')}<span>Copy Task ID</span\n              ><uip-context-menu-shortcut>⌘C</uip-context-menu-shortcut></uip-context-menu-item\n            >\n            <uip-context-menu-item @select=${() => this.emit('duplicate', { task })}\n              >${icon(Layers, 'layers', 'mr-2 size-3.5')}<span>Duplicate</span\n              ><uip-context-menu-shortcut>⌘D</uip-context-menu-shortcut></uip-context-menu-item\n            >\n            <uip-context-menu-separator></uip-context-menu-separator>\n            <uip-context-menu-item\n              class=\"text-destructive focus:text-destructive\"\n              @select=${() => this.emit('delete', { task })}\n              >${icon(Trash2, 'trash-2', 'mr-2 size-3.5')}<span>Delete Deliverable</span\n              ><uip-context-menu-shortcut>⌫</uip-context-menu-shortcut></uip-context-menu-item\n            >`\n        : nothing}\n    </uip-context-menu-content>`\n  }\n\n  // --- GanttHeader -----------------------------------------------------------------\n  private renderHeader() {\n    const scales: [GanttScale, string][] = [\n      ['day', 'Day'],\n      ['week', 'Week'],\n      ['month', 'Month'],\n      ['year', 'Year'],\n    ]\n    return html`<div\n      part=\"header\"\n      data-uipkge=\"\"\n      data-slot=\"gantt-header\"\n      class=\"border-border bg-muted/30 flex items-center justify-between border-b px-4 py-2.5\"\n    >\n      <div class=\"flex items-center gap-2\">\n        ${icon(Calendar, 'calendar', 'text-primary size-4')}\n        <span class=\"text-foreground text-sm font-semibold\">${this.headerTitle}</span>\n      </div>\n\n      <div class=\"flex items-center gap-3\">\n        <slot name=\"actions\"></slot>\n        ${this.showScaleSwitcher\n          ? html`<uip-button-group>\n              ${scales.map(\n                ([s, label]) =>\n                  html`<uip-button\n                    size=\"xs\"\n                    variant=${this.scale === s ? 'default' : 'outline'}\n                    @click=${() => this.setScale(s)}\n                    >${label}</uip-button\n                  >`,\n              )}\n            </uip-button-group>`\n          : nothing}\n      </div>\n    </div>`\n  }\n\n  // --- GanttTree -------------------------------------------------------------------\n  private renderTree() {\n    const tasks = this.tasks ?? []\n    return html`<div\n      part=\"tree\"\n      data-uipkge=\"\"\n      data-slot=\"gantt-tree\"\n      style=${styleMap({ width: `${this.treeWidth}px` })}\n      class=\"border-border bg-card flex shrink-0 flex-col border-r transition-[width] select-none\"\n    >\n      <div\n        style=${styleMap({ height: `${this.headerHeight}px` })}\n        class=\"border-border bg-muted/20 text-muted-foreground flex items-center justify-between border-b px-3 text-xs font-semibold tracking-wider uppercase\"\n      >\n        <span class=\"flex-1 truncate\">Deliverable</span>\n        ${this.showPriority ? html`<span class=\"w-12 shrink-0 text-center\">Pri</span>` : nothing}\n        <span class=\"w-16 shrink-0 text-right\">Duration</span>\n      </div>\n\n      <div class=\"divide-border/40 flex-1 divide-y overflow-y-auto\">\n        ${tasks.map(\n          (task) => html`<div\n            data-task-id=${task.id}\n            style=${styleMap({ height: `${this.rowHeight}px` })}\n            class=${cn(\n              'group/row text-foreground hover:bg-muted/40 flex cursor-pointer items-center justify-between px-3 text-xs transition-colors',\n              task.isGroup && 'bg-muted/10 font-semibold',\n            )}\n            @click=${() => this.onTaskClick(task)}\n          >\n            <div class=\"flex min-w-0 flex-1 items-center gap-1.5 pr-2\">\n              ${task.parentId ? html`<span class=\"w-4 shrink-0\"></span>` : nothing}\n              ${task.status\n                ? html`<span\n                    class=${cn(\n                      'size-2 shrink-0 rounded-full',\n                      task.status === 'done' && 'bg-emerald-500 ring-2 ring-emerald-500/20',\n                      task.status === 'in-progress' && 'bg-primary ring-primary/20 ring-2',\n                      task.status === 'at-risk' && 'bg-amber-500 ring-2 ring-amber-500/20',\n                      task.status === 'todo' && 'bg-muted-foreground/40',\n                      task.status === 'blocked' && 'bg-destructive ring-destructive/20 ring-2',\n                    )}\n                  ></span>`\n                : nothing}\n              <span class=\"truncate font-medium\">${task.name}</span>\n            </div>\n\n            ${this.showPriority\n              ? html`<div class=\"flex w-12 shrink-0 items-center justify-center\">\n                  ${task.priority ? icon(Flag, 'flag', cn('size-3', priorityColors[task.priority])) : nothing}\n                </div>`\n              : nothing}\n\n            <div class=\"text-muted-foreground w-16 shrink-0 text-right font-mono text-xs\">\n              ${task.isMilestone\n                ? html`<span class=\"text-xs font-semibold text-amber-500\">Milestone</span>`\n                : html`<span>${calculateDays(task.startDate, task.endDate)}</span>`}\n            </div>\n          </div>`,\n        )}\n      </div>\n    </div>`\n  }\n\n  // --- GanttTimeline -----------------------------------------------------------------\n  private taskCoordinates(task: GanttTask, index: number, start: number) {\n    const taskStart = new Date(task.startDate).getTime()\n    const taskEnd = new Date(task.endDate).getTime()\n    const startDiffDays = Math.max(0, (taskStart - start) / ONE_DAY)\n    const durationDays = Math.max(1, (taskEnd - taskStart) / ONE_DAY)\n    const left = startDiffDays * this.columnWidth\n    const width = durationDays * this.columnWidth\n    const top = index * this.rowHeight + (this.rowHeight - 28) / 2\n    return { left, width, top, height: 28 }\n  }\n\n  private renderBar(task: GanttTask, left: number, width: number, top: number, height: number) {\n    if (task.isGroup) {\n      return html`<div\n        data-uipkge=\"\"\n        data-slot=\"gantt-group-bar\"\n        data-task-id=${task.id}\n        style=${styleMap({\n          left: `${left}px`,\n          width: `${Math.max(24, width)}px`,\n          top: `${top + 4}px`,\n          height: `${height - 8}px`,\n        })}\n        class=\"group/bar bg-foreground/80 text-background hover:bg-foreground absolute z-10 flex cursor-pointer items-center justify-between rounded-xs px-2 text-xs font-semibold shadow-xs select-none\"\n        @click=${() => this.onTaskClick(task)}\n      >\n        <span class=\"truncate\">${task.name}</span>\n        ${task.progress != null ? html`<span class=\"font-mono text-xs opacity-80\">${task.progress}%</span>` : nothing}\n      </div>`\n    }\n    return html`<div\n      data-uipkge=\"\"\n      data-slot=\"gantt-bar\"\n      data-task-id=${task.id}\n      style=${styleMap({ left: `${left}px`, width: `${Math.max(24, width)}px`, top: `${top}px`, height: `${height}px` })}\n      class=${cn(\n        'group/bar absolute z-10 flex cursor-pointer items-center overflow-hidden rounded-md border text-xs font-medium shadow-xs transition-[box-shadow,transform] select-none hover:scale-[1.01] hover:shadow-md',\n        task.color ? task.color : statusColors[task.status ?? 'in-progress'],\n      )}\n      @click=${() => this.onTaskClick(task)}\n    >\n      ${task.progress != null && task.progress > 0\n        ? html`<div\n            style=${styleMap({ width: `${task.progress}%` })}\n            class=${cn('absolute inset-y-0 left-0 transition-[left]', progressColors[task.status ?? 'in-progress'])}\n          ></div>`\n        : nothing}\n\n      <div class=\"relative z-10 flex w-full min-w-0 items-center justify-between px-2\">\n        <span class=\"truncate font-medium\">${task.name}</span>\n        ${task.progress != null\n          ? html`<span class=\"ml-1 shrink-0 font-mono text-xs opacity-80\">${task.progress}%</span>`\n          : nothing}\n      </div>\n\n      <div\n        aria-hidden=\"true\"\n        class=\"bg-foreground/20 absolute inset-y-0 left-0 w-1.5 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100\"\n      ></div>\n      <div\n        aria-hidden=\"true\"\n        class=\"bg-foreground/20 absolute inset-y-0 right-0 w-1.5 cursor-ew-resize opacity-0 transition-opacity group-hover/bar:opacity-100\"\n      ></div>\n    </div>`\n  }\n\n  private renderMilestone(task: GanttTask, left: number, top: number, size = 16) {\n    return html`<div\n      data-uipkge=\"\"\n      data-slot=\"gantt-milestone\"\n      data-task-id=${task.id}\n      style=${styleMap({\n        left: `${left - size / 2}px`,\n        top: `${top - size / 2}px`,\n        width: `${size}px`,\n        height: `${size}px`,\n      })}\n      title=${`${task.name} (${task.startDate})`}\n      class=\"border-primary bg-primary absolute z-20 rotate-45 cursor-pointer rounded-xs border-2 shadow-sm transition-transform hover:scale-125\"\n      @click=${() => this.onTaskClick(task)}\n    ></div>`\n  }\n\n  private renderTimeline() {\n    const tasks = this.tasks ?? []\n    const startDate = this.resolvedStartDate\n    const endDate = this.resolvedEndDate\n    const totalDays = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / ONE_DAY))\n    const columnWidth = this.columnWidth\n    const rowHeight = this.rowHeight\n    const start = startDate.getTime()\n\n    const columns: { label: string; subLabel: string; isWeekend: boolean }[] = []\n    for (let i = 0; i < totalDays; i++) {\n      const d = new Date(startDate)\n      d.setDate(d.getDate() + i)\n      const dayOfWeek = d.getDay()\n      columns.push({\n        label: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),\n        subLabel: d.toLocaleDateString(undefined, { weekday: 'narrow' }),\n        isWeekend: dayOfWeek === 0 || dayOfWeek === 6,\n      })\n    }\n    const timelineWidth = columns.length * columnWidth\n\n    const diffDays = (new Date().setHours(0, 0, 0, 0) - start) / ONE_DAY\n    const todayPosition = diffDays < 0 || diffDays > totalDays ? null : diffDays * columnWidth + columnWidth / 2\n\n    const paths: string[] = []\n    if (this.showDependencies) {\n      const taskMap = new Map<string, { task: GanttTask; index: number }>()\n      tasks.forEach((t, i) => taskMap.set(t.id, { task: t, index: i }))\n      tasks.forEach((toTask, toIdx) => {\n        toTask.dependencies?.forEach((fromId) => {\n          const fromEntry = taskMap.get(fromId)\n          if (!fromEntry) return\n          const fromCoords = this.taskCoordinates(fromEntry.task, fromEntry.index, start)\n          const toCoords = this.taskCoordinates(toTask, toIdx, start)\n          const startX = fromEntry.task.isMilestone ? fromCoords.left : fromCoords.left + fromCoords.width\n          const startY = fromCoords.top + 14\n          const endX = toCoords.left\n          const endY = toCoords.top + 14\n          const deltaX = Math.max(16, (endX - startX) / 2)\n          paths.push(`M ${startX} ${startY} C ${startX + deltaX} ${startY}, ${endX - deltaX} ${endY}, ${endX} ${endY}`)\n        })\n      })\n    }\n\n    return html`<div\n      part=\"timeline\"\n      data-uipkge=\"\"\n      data-slot=\"gantt-timeline\"\n      class=\"bg-background relative flex-1 overflow-x-auto overflow-y-hidden select-none\"\n    >\n      <div style=${styleMap({ width: `${timelineWidth}px` })} class=\"relative\">\n        <div\n          style=${styleMap({ height: `${this.headerHeight}px` })}\n          class=\"border-border bg-muted/10 sticky top-0 z-20 flex border-b\"\n        >\n          ${columns.map(\n            (col) => html`<div\n              style=${styleMap({ width: `${columnWidth}px` })}\n              class=${cn(\n                'border-border/50 text-muted-foreground flex flex-col items-center justify-center border-r text-xs',\n                col.isWeekend && 'bg-muted/20 text-muted-foreground/60',\n              )}\n            >\n              <span class=\"text-foreground font-medium\">${col.label}</span>\n              <span class=\"text-xs\">${col.subLabel}</span>\n            </div>`,\n          )}\n        </div>\n\n        <div class=\"relative\">\n          <div class=\"pointer-events-none absolute inset-0 flex\">\n            ${columns.map(\n              (col) => html`<div\n                style=${styleMap({ width: `${columnWidth}px` })}\n                class=${cn('border-border/30 h-full border-r', col.isWeekend && 'bg-muted/15')}\n              ></div>`,\n            )}\n          </div>\n\n          ${this.showTodayLine && todayPosition != null\n            ? html`<div\n                style=${styleMap({ left: `${todayPosition}px` })}\n                class=\"pointer-events-none absolute inset-y-0 z-30 flex flex-col items-center\"\n              >\n                <div\n                  class=\"bg-destructive text-destructive-foreground rounded-full px-1.5 py-0.5 text-xs font-bold shadow-xs\"\n                >\n                  Today\n                </div>\n                <div class=\"bg-destructive/60 h-full w-[1.5px] border-r border-dashed\"></div>\n              </div>`\n            : nothing}\n          ${paths.length > 0\n            ? html`<svg\n                width=${timelineWidth}\n                height=${tasks.length * rowHeight}\n                class=\"pointer-events-none absolute inset-0 z-10\"\n              >\n                <defs>\n                  <marker\n                    id=\"gantt-arrow\"\n                    viewBox=\"0 0 6 6\"\n                    refX=\"5\"\n                    refY=\"3\"\n                    markerWidth=\"6\"\n                    markerHeight=\"6\"\n                    orient=\"auto\"\n                  >\n                    <path d=\"M 0 0 L 6 3 L 0 6 z\" class=\"fill-primary/60\"></path>\n                  </marker>\n                </defs>\n                ${paths.map(\n                  (d) =>\n                    svg`<path d=${d} fill=\"none\" class=\"stroke-primary/50\" stroke-width=\"1.5\" stroke-dasharray=\"3,3\" marker-end=\"url(#gantt-arrow)\"></path>`,\n                )}\n              </svg>`\n            : nothing}\n          ${tasks.map((task, idx) => {\n            const coords = this.taskCoordinates(task, idx, start)\n            return html`<div\n              style=${styleMap({ height: `${rowHeight}px` })}\n              class=\"border-border/40 hover:bg-muted/10 relative border-b transition-colors\"\n            >\n              ${task.isMilestone\n                ? this.renderMilestone(task, coords.left, rowHeight / 2)\n                : this.renderBar(task, coords.left, coords.width, (coords.height - 28) / 2 + 6, 28)}\n            </div>`\n          })}\n        </div>\n      </div>\n    </div>`\n  }\n\n  render() {\n    const body = html`<div\n      part=\"body\"\n      slot=${this.contextMenu ? 'trigger' : nothing}\n      class=\"flex min-h-0 flex-1 overflow-hidden\"\n      @contextmenu=${this.contextMenu ? this.onBodyContextMenu : nothing}\n    >\n      ${this.renderTree()}${this.renderTimeline()}\n    </div>`\n\n    return html`<div\n      part=\"base\"\n      class=\"border-border bg-card text-card-foreground relative flex w-full flex-col overflow-hidden rounded-xl border shadow-xs\"\n    >\n      ${this.renderHeader()}\n      ${this.contextMenu ? html`<uip-context-menu\n            class=\"[&::part(content)]:w-56 [&::part(task-label)]:flex [&::part(task-label)]:items-center [&::part(task-label)]:justify-between [&::part(task-label)]:text-xs [&::part(status-menu)]:w-44 [&::part(priority-menu)]:w-40\"\n            >${body}${this.renderMenu()}</uip-context-menu\n          >` : body}\n    </div>`\n  }\n}\n\ncustomElements.get('uip-gantt') || customElements.define('uip-gantt', UipGantt)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-gantt': UipGantt\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/gantt/gantt.ts"
    },
    {
      "path": "packages/elements-lit/src/components/gantt/types.ts",
      "content": "// Copy of packages/registry-react/components/gantt/types.ts.\nexport type GanttScale = 'day' | 'week' | 'month' | 'quarter' | 'year'\n\nexport type GanttTaskStatus = 'todo' | 'in-progress' | 'done' | 'blocked' | 'at-risk'\n\nexport type GanttTaskPriority = 'low' | 'medium' | 'high' | 'urgent'\n\nexport interface GanttDependency {\n  fromId: string\n  toId: string\n  type?: 'finish-to-start' | 'start-to-start' | 'finish-to-finish'\n}\n\nexport interface GanttAssignee {\n  name: string\n  avatar?: string\n  initials?: string\n  role?: string\n}\n\nexport interface GanttTask {\n  id: string\n  name: string\n  startDate: string // YYYY-MM-DD\n  endDate: string // YYYY-MM-DD\n  progress?: number // 0 to 100\n  color?: string\n  status?: GanttTaskStatus\n  priority?: GanttTaskPriority\n  assignee?: GanttAssignee\n  isMilestone?: boolean\n  isGroup?: boolean\n  parentId?: string | null\n  isExpanded?: boolean\n  dependencies?: string[] // task IDs\n  children?: GanttTask[]\n}\n\nexport interface GanttColumn {\n  key: string\n  label: string\n  width?: string\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/gantt/types.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/context-menu.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
