{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "gantt",
  "title": "Gantt",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/gantt/gantt.component.ts",
      "content": "import {\n  Component,\n  EventEmitter,\n  Input,\n  Output,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport { UiButtonGroupComponent } from '@/ui/button/button-group.component'\nimport {\n  UiContextMenuComponent,\n  UiContextMenuTriggerComponent,\n  UiContextMenuContentComponent,\n  UiContextMenuItemComponent,\n  UiContextMenuLabelComponent,\n  UiContextMenuRadioGroupComponent,\n  UiContextMenuRadioItemComponent,\n  UiContextMenuSeparatorComponent,\n  UiContextMenuShortcutComponent,\n  UiContextMenuSubComponent,\n  UiContextMenuSubContentComponent,\n  UiContextMenuSubTriggerComponent,\n} from '@/ui/context-menu/context-menu.component'\nimport type { GanttTask, GanttScale, GanttTaskStatus, GanttTaskPriority } from './types'\n\n/* ------------------------------------------------------------------ */\n/* Helpers & Color Lookups                                            */\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\nfunction calculateDays(startDate: string, endDate: string): string {\n  const diff = new Date(endDate).getTime() - new Date(startDate).getTime()\n  const days = Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24)))\n  return `${days}d`\n}\n\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\n/* ------------------------------------------------------------------ */\n/* UiGanttComponent (Root Provider)                                   */\n/* ------------------------------------------------------------------ */\n\n/**\n * Angular port of UIPKGE Gantt. Multi-scale timeline, collapsible tree,\n * progress fill, milestones, dependency lines. 1:1 React parity.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt, [ui-gantt]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt\"',\n    '[attr.data-scale]': 'currentScale',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiGanttComponent {\n  @Input() tasks: GanttTask[] = []\n  @Input() scale?: GanttScale\n  @Input() startDate?: string\n  @Input() endDate?: string\n  @Input() rowHeight = 40\n  @Input() headerHeight = 48\n  @Input() treeWidth = 280\n  @Input('class') className?: string\n  @Output() scaleChange = new EventEmitter<GanttScale>()\n  @Output() taskClick = new EventEmitter<GanttTask>()\n  @Output() taskChange = new EventEmitter<GanttTask>()\n\n  readonly internalScale = signal<GanttScale>('day')\n\n  get currentScale(): GanttScale {\n    return this.scale ?? this.internalScale()\n  }\n\n  get hostClass(): string {\n    return cn(\n      'block border-border bg-card text-card-foreground relative w-full overflow-hidden rounded-xl border shadow-xs',\n      this.className,\n    )\n  }\n\n  setScale(scale: GanttScale): void {\n    this.internalScale.set(scale)\n    this.scale = scale\n    this.scaleChange.emit(scale)\n  }\n\n  onTaskClick(task: GanttTask): void {\n    this.taskClick.emit(task)\n  }\n\n  get resolvedStartDate(): Date {\n    if (this.startDate) return new Date(this.startDate)\n    if (this.tasks.length === 0) return new Date()\n    const dates = this.tasks.map((t) => new Date(t.startDate).getTime())\n    const min = Math.min(...dates)\n    const d = new Date(min)\n    d.setDate(d.getDate() - 3)\n    return d\n  }\n\n  get resolvedEndDate(): Date {\n    if (this.endDate) return new Date(this.endDate)\n    if (this.tasks.length === 0) {\n      const d = new Date()\n      d.setDate(d.getDate() + 30)\n      return d\n    }\n    const dates = this.tasks.map((t) => new Date(t.endDate).getTime())\n    const max = Math.max(...dates)\n    const d = new Date(max)\n    d.setDate(d.getDate() + 7)\n    return d\n  }\n\n  get totalDays(): number {\n    const diff = this.resolvedEndDate.getTime() - this.resolvedStartDate.getTime()\n    return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24)))\n  }\n\n  get columnWidth(): number {\n    switch (this.currentScale) {\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  barLeft(task: GanttTask): number {\n    const range = this.range()\n    if (!range) return 0\n    const start = new Date(task.startDate ?? new Date()).getTime()\n    return Math.max(0, Math.min(100, ((start - range.min) / (range.max - range.min)) * 100))\n  }\n\n  barWidth(task: GanttTask): number {\n    const range = this.range()\n    if (!range) return 10\n    const start = new Date(task.startDate ?? new Date()).getTime()\n    const end = new Date(task.endDate ?? new Date(start + 86400000)).getTime()\n    return Math.max(2, Math.min(100, ((end - start) / (range.max - range.min)) * 100))\n  }\n\n  private range(): { min: number; max: number } | null {\n    if (this.startDate && this.endDate) {\n      return { min: new Date(this.startDate).getTime(), max: new Date(this.endDate).getTime() }\n    }\n    return { min: this.resolvedStartDate.getTime(), max: this.resolvedEndDate.getTime() }\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttHeaderComponent                                             */\n/* ------------------------------------------------------------------ */\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-header, [ui-gantt-header]',\n  standalone: true,\n  imports: [UiButtonComponent, UiButtonGroupComponent],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt-header\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div class=\"flex items-center gap-2\">\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        stroke-width=\"2\"\n        stroke-linecap=\"round\"\n        stroke-linejoin=\"round\"\n        class=\"lucide lucide-calendar text-primary size-4\"\n        aria-hidden=\"true\"\n      >\n        <path d=\"M8 2v4\" />\n        <path d=\"M16 2v4\" />\n        <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\" />\n        <path d=\"M3 10h18\" />\n      </svg>\n      <span class=\"text-foreground text-sm font-semibold\">{{ title }}</span>\n    </div>\n\n    <div class=\"flex items-center gap-3\">\n      <ng-content />\n\n      @if (showScaleSwitcher && gantt) {\n        <ui-button-group>\n          <button\n            ui-button\n            size=\"xs\"\n            [variant]=\"gantt.currentScale === 'day' ? 'default' : 'outline'\"\n            (click)=\"gantt.setScale('day')\"\n          >\n            Day\n          </button>\n          <button\n            ui-button\n            size=\"xs\"\n            [variant]=\"gantt.currentScale === 'week' ? 'default' : 'outline'\"\n            (click)=\"gantt.setScale('week')\"\n          >\n            Week\n          </button>\n          <button\n            ui-button\n            size=\"xs\"\n            [variant]=\"gantt.currentScale === 'month' ? 'default' : 'outline'\"\n            (click)=\"gantt.setScale('month')\"\n          >\n            Month\n          </button>\n          <button\n            ui-button\n            size=\"xs\"\n            [variant]=\"gantt.currentScale === 'year' ? 'default' : 'outline'\"\n            (click)=\"gantt.setScale('year')\"\n          >\n            Year\n          </button>\n        </ui-button-group>\n      }\n    </div>\n  `,\n})\nexport class UiGanttHeaderComponent {\n  readonly gantt = inject(UiGanttComponent, { optional: true })\n\n  @Input() title = 'Project Timeline'\n  @Input({ transform: booleanAttribute }) showScaleSwitcher = true\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('border-border bg-muted/30 flex items-center justify-between border-b px-4 py-2.5', this.className)\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttTreeComponent                                               */\n/* ------------------------------------------------------------------ */\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-tree, [ui-gantt-tree]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt-tree\"',\n    '[style.width.px]': 'treeWidth',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div\n      [style.height.px]=\"headerHeight\"\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      @if (showPriority) {\n        <span class=\"w-12 shrink-0 text-center\">Pri</span>\n      }\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      @for (task of tasks; track task.id) {\n        <div\n          [style.height.px]=\"rowHeight\"\n          class=\"group/row text-foreground hover:bg-muted/40 flex cursor-pointer items-center justify-between px-3 text-xs transition-colors\"\n          [class.bg-muted/10]=\"task.isGroup\"\n          [class.font-semibold]=\"task.isGroup\"\n          (click)=\"onRowClick(task)\"\n        >\n          <div class=\"flex min-w-0 flex-1 items-center gap-1.5 pr-2\">\n            @if (task.parentId) {\n              <span class=\"w-4 shrink-0\"></span>\n            }\n\n            @if (task.status) {\n              <span\n                class=\"size-2 shrink-0 rounded-full\"\n                [class.bg-emerald-500]=\"task.status === 'done'\"\n                [class.ring-2]=\"\n                  task.status === 'done' ||\n                  task.status === 'in-progress' ||\n                  task.status === 'at-risk' ||\n                  task.status === 'blocked'\n                \"\n                [class.ring-emerald-500/20]=\"task.status === 'done'\"\n                [class.bg-primary]=\"task.status === 'in-progress'\"\n                [class.ring-primary/20]=\"task.status === 'in-progress'\"\n                [class.bg-amber-500]=\"task.status === 'at-risk'\"\n                [class.ring-amber-500/20]=\"task.status === 'at-risk'\"\n                [class.bg-muted-foreground/40]=\"task.status === 'todo'\"\n                [class.bg-destructive]=\"task.status === 'blocked'\"\n                [class.ring-destructive/20]=\"task.status === 'blocked'\"\n              ></span>\n            }\n\n            <span class=\"truncate font-medium\">{{ task.name }}</span>\n          </div>\n\n          @if (showPriority) {\n            <div class=\"flex w-12 shrink-0 items-center justify-center\">\n              @if (task.priority) {\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-flag size-3\"\n                  [class]=\"priorityColors[task.priority]\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n                  <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n                </svg>\n              }\n            </div>\n          }\n\n          <div class=\"text-muted-foreground w-16 shrink-0 text-right font-mono text-xs\">\n            @if (task.isMilestone) {\n              <span class=\"text-xs font-semibold text-amber-500\">Milestone</span>\n            } @else {\n              <span>{{ calculateDays(task.startDate, task.endDate) }}</span>\n            }\n          </div>\n        </div>\n      }\n    </div>\n  `,\n})\nexport class UiGanttTreeComponent {\n  readonly gantt = inject(UiGanttComponent)\n\n  @Input({ transform: booleanAttribute }) showAssignee = true\n  @Input({ transform: booleanAttribute }) showPriority = true\n  @Input('class') className?: string\n  @Output() taskClick = new EventEmitter<GanttTask>()\n\n  readonly priorityColors = priorityColors\n  readonly calculateDays = calculateDays\n\n  get treeWidth() {\n    return this.gantt.treeWidth\n  }\n  get headerHeight() {\n    return this.gantt.headerHeight\n  }\n  get rowHeight() {\n    return this.gantt.rowHeight\n  }\n  get tasks() {\n    return this.gantt.tasks\n  }\n\n  get hostClass(): string {\n    return cn('border-border bg-card flex shrink-0 flex-col border-r transition-[width] select-none', this.className)\n  }\n\n  onRowClick(task: GanttTask): void {\n    this.taskClick.emit(task)\n    this.gantt.taskClick.emit(task)\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttMilestoneComponent                                          */\n/* ------------------------------------------------------------------ */\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-milestone, [ui-gantt-milestone]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt-milestone\"',\n    '[style.left.px]': 'left - size / 2',\n    '[style.top.px]': 'top - size / 2',\n    '[style.width.px]': 'size',\n    '[style.height.px]': 'size',\n    '[attr.title]': 'task ? task.name + \" (\" + task.startDate + \")\" : null',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n  },\n  template: ``,\n})\nexport class UiGanttMilestoneComponent {\n  @Input({ required: true }) task!: GanttTask\n  @Input({ required: true }) left!: number\n  @Input({ required: true }) top!: number\n  @Input() size = 16\n  @Input('class') className?: string\n  @Output() taskClick = new EventEmitter<GanttTask>()\n\n  get hostClass(): string {\n    return cn(\n      'border-primary bg-primary absolute z-20 rotate-45 cursor-pointer rounded-xs border-2 shadow-sm transition-transform hover:scale-125',\n      this.className,\n    )\n  }\n\n  onClick(): void {\n    this.taskClick.emit(this.task)\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttBarComponent                                                */\n/* ------------------------------------------------------------------ */\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-bar, [ui-gantt-bar]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': 'task?.isGroup ? \"gantt-group-bar\" : \"gantt-bar\"',\n    '[style.left.px]': 'left',\n    '[style.width.px]': 'mathMax(24, width)',\n    '[style.top.px]': 'task?.isGroup ? top + 4 : top',\n    '[style.height.px]': 'task?.isGroup ? height - 8 : height',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n  },\n  template: `\n    @if (task) {\n      @if (task.isGroup) {\n        <span class=\"truncate\">{{ task.name }}</span>\n        @if (task.progress != null) {\n          <span class=\"font-mono text-xs opacity-80\">{{ task.progress }}%</span>\n        }\n      } @else {\n        @if (task.progress != null && task.progress > 0) {\n          <div\n            [style.width.%]=\"task.progress\"\n            [class]=\"'absolute inset-y-0 left-0 transition-[left] ' + progressColors[task.status ?? 'in-progress']\"\n          ></div>\n        }\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          @if (task.progress != null) {\n            <span class=\"ml-1 shrink-0 font-mono text-xs opacity-80\">{{ task.progress }}%</span>\n          }\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      }\n    }\n  `,\n})\nexport class UiGanttBarComponent {\n  @Input({ required: true }) task!: GanttTask\n  @Input({ required: true }) left!: number\n  @Input({ required: true }) width!: number\n  @Input({ required: true }) top!: number\n  @Input({ required: true }) height!: number\n  @Input('class') className?: string\n  @Output() taskClick = new EventEmitter<GanttTask>()\n\n  readonly mathMax = Math.max\n  readonly progressColors = progressColors\n\n  get hostClass(): string {\n    if (this.task?.isGroup) {\n      return cn(\n        '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        this.className,\n      )\n    }\n    return 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      this.task?.color ? this.task.color : statusColors[this.task?.status ?? 'in-progress'],\n      this.className,\n    )\n  }\n\n  onClick(): void {\n    this.taskClick.emit(this.task)\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttTimelineComponent                                           */\n/* ------------------------------------------------------------------ */\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-timeline, [ui-gantt-timeline]',\n  standalone: true,\n  imports: [UiGanttBarComponent, UiGanttMilestoneComponent],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt-timeline\"',\n    '[class]': 'hostClass',\n  },\n  template: `\n    <div [style.width.px]=\"timelineWidth\" class=\"relative\">\n      <div [style.height.px]=\"headerHeight\" class=\"border-border bg-muted/10 sticky top-0 z-20 flex border-b\">\n        @for (col of columns; track $index) {\n          <div\n            [style.width.px]=\"columnWidth\"\n            class=\"border-border/50 text-muted-foreground flex flex-col items-center justify-center border-r text-xs\"\n            [class.bg-muted/20]=\"col.isWeekend\"\n            [class.text-muted-foreground/60]=\"col.isWeekend\"\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          @for (col of columns; track $index) {\n            <div\n              [style.width.px]=\"columnWidth\"\n              class=\"border-border/30 h-full border-r\"\n              [class.bg-muted/15]=\"col.isWeekend\"\n            ></div>\n          }\n        </div>\n\n        @if (showTodayLine && todayPosition != null) {\n          <div\n            [style.left.px]=\"todayPosition\"\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        }\n\n        @if (dependencyPaths.length > 0) {\n          <svg\n            [attr.width]=\"timelineWidth\"\n            [attr.height]=\"tasks.length * rowHeight\"\n            class=\"pointer-events-none absolute inset-0 z-10\"\n          >\n            <defs>\n              <marker\n                id=\"gantt-arrow-angular\"\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\" />\n              </marker>\n            </defs>\n            @for (p of dependencyPaths; track $index) {\n              <path\n                [attr.d]=\"p.d\"\n                fill=\"none\"\n                class=\"stroke-primary/50\"\n                stroke-width=\"1.5\"\n                stroke-dasharray=\"3,3\"\n                marker-end=\"url(#gantt-arrow-angular)\"\n              />\n            }\n          </svg>\n        }\n\n        @for (task of tasks; track task.id; let idx = $index) {\n          @let coords = getTaskCoordinates(task, idx);\n          <div\n            [style.height.px]=\"rowHeight\"\n            class=\"border-border/40 hover:bg-muted/10 relative border-b transition-colors\"\n          >\n            @if (task.isMilestone) {\n              <ui-gantt-milestone\n                [task]=\"task\"\n                [left]=\"coords.left\"\n                [top]=\"rowHeight / 2\"\n                (taskClick)=\"onTaskClick(task)\"\n              />\n            } @else {\n              <ui-gantt-bar\n                [task]=\"task\"\n                [left]=\"coords.left\"\n                [width]=\"coords.width\"\n                [top]=\"(coords.height - 28) / 2 + 6\"\n                [height]=\"28\"\n                (taskClick)=\"onTaskClick(task)\"\n              />\n            }\n          </div>\n        }\n      </div>\n    </div>\n  `,\n})\nexport class UiGanttTimelineComponent {\n  readonly gantt = inject(UiGanttComponent)\n\n  @Input({ transform: booleanAttribute }) showTodayLine = true\n  @Input({ transform: booleanAttribute }) showDependencies = true\n  @Input('class') className?: string\n  @Output() taskClick = new EventEmitter<GanttTask>()\n\n  get headerHeight() {\n    return this.gantt.headerHeight\n  }\n  get rowHeight() {\n    return this.gantt.rowHeight\n  }\n  get columnWidth() {\n    return this.gantt.columnWidth\n  }\n  get tasks() {\n    return this.gantt.tasks\n  }\n\n  get hostClass(): string {\n    return cn('bg-background relative flex-1 overflow-x-auto overflow-y-hidden select-none', this.className)\n  }\n\n  get columns(): { date: Date; label: string; subLabel: string; isWeekend: boolean }[] {\n    const list: { date: Date; label: string; subLabel: string; isWeekend: boolean }[] = []\n    const start = new Date(this.gantt.resolvedStartDate)\n    const totalDays = this.gantt.totalDays\n\n    for (let i = 0; i < totalDays; i++) {\n      const d = new Date(start)\n      d.setDate(d.getDate() + i)\n      const dayOfWeek = d.getDay()\n      const isWeekend = dayOfWeek === 0 || dayOfWeek === 6\n\n      list.push({\n        date: d,\n        label: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),\n        subLabel: d.toLocaleDateString(undefined, { weekday: 'narrow' }),\n        isWeekend,\n      })\n    }\n    return list\n  }\n\n  get timelineWidth(): number {\n    return this.columns.length * this.columnWidth\n  }\n\n  getTaskCoordinates(task: GanttTask, index: number): { left: number; width: number; top: number; height: number } {\n    const start = new Date(this.gantt.resolvedStartDate).getTime()\n    const taskStart = new Date(task.startDate).getTime()\n    const taskEnd = new Date(task.endDate).getTime()\n    const oneDay = 1000 * 60 * 60 * 24\n\n    const startDiffDays = Math.max(0, (taskStart - start) / oneDay)\n    const durationDays = Math.max(1, (taskEnd - taskStart) / oneDay)\n\n    const left = startDiffDays * this.columnWidth\n    const width = durationDays * this.columnWidth\n    const top = index * this.rowHeight + (this.rowHeight - 28) / 2\n\n    return { left, width, top, height: 28 }\n  }\n\n  get todayPosition(): number | null {\n    const start = new Date(this.gantt.resolvedStartDate).getTime()\n    const today = new Date().setHours(0, 0, 0, 0)\n    const oneDay = 1000 * 60 * 60 * 24\n    const diffDays = (today - start) / oneDay\n\n    if (diffDays < 0 || diffDays > this.gantt.totalDays) return null\n    return diffDays * this.columnWidth + this.columnWidth / 2\n  }\n\n  get dependencyPaths(): { d: string; fromId: string; toId: string }[] {\n    if (!this.showDependencies) return []\n    const taskMap = new Map<string, { task: GanttTask; index: number }>()\n    this.tasks.forEach((t, i) => taskMap.set(t.id, { task: t, index: i }))\n\n    const paths: { d: string; fromId: string; toId: string }[] = []\n\n    this.tasks.forEach((toTask, toIdx) => {\n      if (!toTask.dependencies || toTask.dependencies.length === 0) return\n      toTask.dependencies.forEach((fromId) => {\n        const fromEntry = taskMap.get(fromId)\n        if (!fromEntry) return\n\n        const fromCoords = this.getTaskCoordinates(fromEntry.task, fromEntry.index)\n        const toCoords = this.getTaskCoordinates(toTask, toIdx)\n\n        const startX = fromEntry.task.isMilestone ? fromCoords.left : fromCoords.left + fromCoords.width\n        const startY = fromCoords.top + 14\n\n        const endX = toCoords.left\n        const endY = toCoords.top + 14\n\n        const deltaX = Math.max(16, (endX - startX) / 2)\n        const d = `M ${startX} ${startY} C ${startX + deltaX} ${startY}, ${endX - deltaX} ${endY}, ${endX} ${endY}`\n        paths.push({ d, fromId, toId: toTask.id })\n      })\n    })\n\n    return paths\n  }\n\n  onTaskClick(task: GanttTask): void {\n    this.taskClick.emit(task)\n    this.gantt.taskClick.emit(task)\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* UiGanttContextMenuComponent                                        */\n/* ------------------------------------------------------------------ */\n\n/**\n * React `GanttContextMenu`: right-click menu for a task — task label header, View Details,\n * Change Status / Set Priority submenus (radio groups), Copy Task ID, Duplicate, Delete.\n * React's `asChild` trigger becomes the projected content inside the trigger element.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-gantt-context-menu, [ui-gantt-context-menu]',\n  standalone: true,\n  imports: [\n    UiContextMenuComponent,\n    UiContextMenuTriggerComponent,\n    UiContextMenuContentComponent,\n    UiContextMenuItemComponent,\n    UiContextMenuLabelComponent,\n    UiContextMenuRadioGroupComponent,\n    UiContextMenuRadioItemComponent,\n    UiContextMenuSeparatorComponent,\n    UiContextMenuShortcutComponent,\n    UiContextMenuSubComponent,\n    UiContextMenuSubContentComponent,\n    UiContextMenuSubTriggerComponent,\n  ],\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"gantt-context-menu\"',\n    '[attr.class]': '\"contents\"',\n  },\n  template: `\n    <ui-context-menu>\n      <ui-context-menu-trigger><ng-content /></ui-context-menu-trigger>\n      <ui-context-menu-content class=\"w-56\">\n        <ui-context-menu-label class=\"flex items-center justify-between text-xs\">\n          <span class=\"truncate font-semibold\">{{ task?.name }}</span>\n          <span class=\"text-muted-foreground font-mono text-xs\">{{ task?.id }}</span>\n        </ui-context-menu-label>\n        <ui-context-menu-separator />\n\n        <ui-context-menu-item (select)=\"edit.emit(task)\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-pen mr-2 size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <path\n              d=\"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z\"\n            />\n          </svg>\n          <span>View Details</span>\n          <ui-context-menu-shortcut>↵</ui-context-menu-shortcut>\n        </ui-context-menu-item>\n\n        <ui-context-menu-sub>\n          <ui-context-menu-sub-trigger>\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-clock text-primary mr-2 size-3.5\"\n              aria-hidden=\"true\"\n            >\n              <circle cx=\"12\" cy=\"12\" r=\"10\" />\n              <path d=\"M12 6v6l4 2\" />\n            </svg>\n            <span>Change Status</span>\n          </ui-context-menu-sub-trigger>\n          <ui-context-menu-sub-content class=\"w-44\">\n            <ui-context-menu-radio-group [value]=\"task?.status ?? 'todo'\">\n              <ui-context-menu-radio-item value=\"done\" (select)=\"statusChange.emit({ task, status: 'done' })\">\n                <span class=\"mr-2 size-2 rounded-full bg-emerald-500\"></span>\n                <span>Completed</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item\n                value=\"in-progress\"\n                (select)=\"statusChange.emit({ task, status: 'in-progress' })\"\n              >\n                <span class=\"bg-primary mr-2 size-2 rounded-full\"></span>\n                <span>In Progress</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item value=\"at-risk\" (select)=\"statusChange.emit({ task, status: 'at-risk' })\">\n                <span class=\"mr-2 size-2 rounded-full bg-amber-500\"></span>\n                <span>At Risk</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item value=\"blocked\" (select)=\"statusChange.emit({ task, status: 'blocked' })\">\n                <span class=\"bg-destructive mr-2 size-2 rounded-full\"></span>\n                <span>Blocked</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item value=\"todo\" (select)=\"statusChange.emit({ task, status: 'todo' })\">\n                <span class=\"bg-muted-foreground/40 mr-2 size-2 rounded-full\"></span>\n                <span>To Do</span>\n              </ui-context-menu-radio-item>\n            </ui-context-menu-radio-group>\n          </ui-context-menu-sub-content>\n        </ui-context-menu-sub>\n\n        <ui-context-menu-sub>\n          <ui-context-menu-sub-trigger>\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-flag mr-2 size-3.5 text-amber-500\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n              <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n            </svg>\n            <span>Set Priority</span>\n          </ui-context-menu-sub-trigger>\n          <ui-context-menu-sub-content class=\"w-40\">\n            <ui-context-menu-radio-group [value]=\"task?.priority ?? 'medium'\">\n              <ui-context-menu-radio-item\n                value=\"urgent\"\n                (select)=\"priorityChange.emit({ task, priority: 'urgent' })\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-flag text-destructive mr-2 size-3\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n                  <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n                </svg>\n                <span>Urgent</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item value=\"high\" (select)=\"priorityChange.emit({ task, priority: 'high' })\">\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-flag mr-2 size-3 text-amber-500\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n                  <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n                </svg>\n                <span>High</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item\n                value=\"medium\"\n                (select)=\"priorityChange.emit({ task, priority: 'medium' })\"\n              >\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-flag text-primary mr-2 size-3\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n                  <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n                </svg>\n                <span>Medium</span>\n              </ui-context-menu-radio-item>\n              <ui-context-menu-radio-item value=\"low\" (select)=\"priorityChange.emit({ task, priority: 'low' })\">\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  width=\"24\"\n                  height=\"24\"\n                  viewBox=\"0 0 24 24\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  stroke-width=\"2\"\n                  stroke-linecap=\"round\"\n                  stroke-linejoin=\"round\"\n                  class=\"lucide lucide-flag text-muted-foreground mr-2 size-3\"\n                  aria-hidden=\"true\"\n                >\n                  <path d=\"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z\" />\n                  <line x1=\"4\" x2=\"4\" y1=\"22\" y2=\"15\" />\n                </svg>\n                <span>Low</span>\n              </ui-context-menu-radio-item>\n            </ui-context-menu-radio-group>\n          </ui-context-menu-sub-content>\n        </ui-context-menu-sub>\n\n        <ui-context-menu-separator />\n\n        <ui-context-menu-item (select)=\"copyTaskId()\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-copy mr-2 size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\n            <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n          </svg>\n          <span>Copy Task ID</span>\n          <ui-context-menu-shortcut>⌘C</ui-context-menu-shortcut>\n        </ui-context-menu-item>\n\n        <ui-context-menu-item (select)=\"duplicate.emit(task)\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-layers mr-2 size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <path\n              d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"\n            />\n            <path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\" />\n            <path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\" />\n          </svg>\n          <span>Duplicate</span>\n          <ui-context-menu-shortcut>⌘D</ui-context-menu-shortcut>\n        </ui-context-menu-item>\n\n        <ui-context-menu-separator />\n\n        <ui-context-menu-item class=\"text-destructive focus:text-destructive\" (select)=\"deleteTask.emit(task)\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-trash-2 mr-2 size-3.5\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M10 11v6\" />\n            <path d=\"M14 11v6\" />\n            <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n            <path d=\"M3 6h18\" />\n            <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n          </svg>\n          <span>Delete Deliverable</span>\n          <ui-context-menu-shortcut>⌫</ui-context-menu-shortcut>\n        </ui-context-menu-item>\n      </ui-context-menu-content>\n    </ui-context-menu>\n  `,\n})\nexport class UiGanttContextMenuComponent {\n  @Input() task?: GanttTask\n  /** React `onEdit`. */\n  @Output() edit = new EventEmitter<GanttTask>()\n  /** React `onStatusChange(task, status)`. */\n  @Output() statusChange = new EventEmitter<{ task: GanttTask; status: GanttTaskStatus }>()\n  /** React `onPriorityChange(task, priority)`. */\n  @Output() priorityChange = new EventEmitter<{ task: GanttTask; priority: GanttTaskPriority }>()\n  /** React `onDuplicate`. */\n  @Output() duplicate = new EventEmitter<GanttTask>()\n  /** React `onDelete` (aliased: `delete` is a reserved word in template expressions). */\n  @Output('delete') deleteTask = new EventEmitter<GanttTask>()\n\n  copyTaskId(): void {\n    if (this.task?.id && typeof navigator !== 'undefined' && navigator.clipboard) {\n      navigator.clipboard.writeText(this.task.id)\n    }\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/gantt/gantt.component.ts"
    },
    {
      "path": "packages/registry-angular/components/gantt/types.ts",
      "content": "export 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:ui",
      "target": "~/src/app/components/ui/gantt/types.ts"
    },
    {
      "path": "packages/registry-angular/components/gantt/index.ts",
      "content": "export {\n  UiGanttComponent,\n  UiGanttHeaderComponent,\n  UiGanttTreeComponent,\n  UiGanttTimelineComponent,\n  UiGanttBarComponent,\n  UiGanttMilestoneComponent,\n  UiGanttContextMenuComponent,\n} from './gantt.component'\nexport type {\n  GanttTask,\n  GanttScale,\n  GanttTaskStatus,\n  GanttTaskPriority,\n  GanttDependency,\n  GanttAssignee,\n  GanttColumn,\n} from './types'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/gantt/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/context-menu.json"
  ],
  "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"
  ]
}