{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "project-roadmap",
  "title": "Project Roadmap",
  "type": "registry:block",
  "description": "Full interactive project roadmap and deliverables surface: Gantt timeline with multi-scale zooming, work breakdown task tree, overall progress metrics, search and status/assignee filters, task detail slide-over Sheet, and Add Task modal dialog.",
  "categories": [
    "productivity",
    "dashboard",
    "data"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/project-roadmap/project-roadmap.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { AlertCircle, Calendar, CheckCircle2, Clock, Download, Milestone, Plus, Search } from 'lucide'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect imports: register the composed elements for standalone use\n// (consumers who install only this block still get working primitives).\nimport '../../components/badge/badge'\nimport '../../components/button/button'\nimport '../../components/card/card'\nimport '../../components/checkbox/checkbox'\nimport '../../components/dialog/dialog'\nimport '../../components/gantt/gantt'\nimport '../../components/input/input'\nimport '../../components/label/label'\nimport '../../components/select/select'\nimport '../../components/sheet/sheet'\nimport type { GanttScale, GanttTask, GanttTaskStatus } from '../../components/gantt/types'\nimport type { UipInput } from '../../components/input/input'\nimport type { UipSelect } from '../../components/select/select'\n\nfunction seedTasks(): GanttTask[] {\n  return [\n    { id: 'task-1', name: 'System Architecture & Database Schema', startDate: '2026-08-01', endDate: '2026-08-08', progress: 100, status: 'done', priority: 'high', assignee: { name: 'Sarah Connor', initials: 'SC' } },\n    { id: 'task-2', name: 'Auth, RBAC & Multi-Tenant Session Engine', startDate: '2026-08-06', endDate: '2026-08-16', progress: 85, status: 'in-progress', priority: 'urgent', assignee: { name: 'Marcus Rivera', initials: 'MR' } },\n    { id: 'task-3', name: 'Real-time WebSocket Notification Bus', startDate: '2026-08-12', endDate: '2026-08-22', progress: 50, status: 'in-progress', priority: 'high', assignee: { name: 'Priya Nair', initials: 'PN' } },\n    { id: 'task-4', name: 'Alpha Core API Release Milestone', startDate: '2026-08-23', endDate: '2026-08-23', isMilestone: true, status: 'todo', priority: 'urgent' },\n    { id: 'task-5', name: 'Stripe Billing & Invoicing Integration', startDate: '2026-08-24', endDate: '2026-09-04', progress: 15, status: 'todo', priority: 'medium', assignee: { name: 'Sundar Krishnan', initials: 'SK' } },\n    { id: 'task-6', name: 'End-to-End Playwright Automated Testing', startDate: '2026-08-28', endDate: '2026-09-08', progress: 0, status: 'todo', priority: 'low', assignee: { name: 'Diane Cho', initials: 'DC' } },\n    { id: 'task-7', name: 'Production Cloudflare Pages Deployment', startDate: '2026-09-10', endDate: '2026-09-10', isMilestone: true, status: 'todo' },\n  ]\n}\n\n/**\n * <uip-project-roadmap> — the registry ProjectRoadmap block as a web component.\n *\n * Ports the full surface: header with Export/Add Task, metrics row,\n * search + status + assignee filters with a live count, the Gantt timeline,\n * a task detail sheet, and the Add Task dialog. The RoadmapMetrics /\n * RoadmapTaskDetail / RoadmapAddTaskDialog decomposition is folded into this\n * single file (multi-file blocks ship together, but one file suffices here);\n * class strings follow React's files. `tasks` is a settable property\n * (defaults to the demo seed). The timeline itself renders through the\n * existing <uip-gantt> primitive (`task-click` / `scale-change` events).\n */\nexport class UipProjectRoadmap extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    tasks: { attribute: false },\n    heading: {},\n    description: {},\n    searchQuery: { state: true },\n    statusFilter: { state: true },\n    assigneeFilter: { state: true },\n    scale: { state: true },\n    sheetOpen: { state: true },\n    selectedTaskId: { state: true },\n    addOpen: { state: true },\n    addName: { state: true },\n    addStart: { state: true },\n    addEnd: { state: true },\n    addStatus: { state: true },\n    addMilestone: { state: true },\n    addAssignee: { state: true },\n  }\n\n  tasks: GanttTask[] = []\n  /** React's `title` (`title` is taken by HTMLElement). */\n  heading = 'Engineering Q3 Deliverables'\n  description = 'Track roadmap work breakdown, sprint milestones, and cross-team dependencies.'\n\n  private searchQuery = ''\n  private statusFilter = 'all'\n  private assigneeFilter = 'all'\n  private scale: GanttScale = 'day'\n  private sheetOpen = false\n  private selectedTaskId: string | null = null\n  private addOpen = false\n  private addName = ''\n  private addStart = '2026-08-20'\n  private addEnd = '2026-08-28'\n  private addStatus: 'todo' | 'in-progress' | 'done' = 'todo'\n  private addMilestone = false\n  private addAssignee = 'Marcus Rivera'\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', 'project-roadmap')\n    if (this.tasks.length === 0) this.tasks = seedTasks()\n  }\n\n  private get filteredTasks(): GanttTask[] {\n    return this.tasks.filter((t) => {\n      const matchesSearch = !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase())\n      const matchesStatus = this.statusFilter === 'all' || t.status === this.statusFilter\n      const matchesAssignee = this.assigneeFilter === 'all' || t.assignee?.name === this.assigneeFilter\n      return matchesSearch && matchesStatus && matchesAssignee\n    })\n  }\n\n  private get metrics(): { total: number; completed: number; inProgress: number; milestones: number; progress: number } {\n    const total = this.tasks.length\n    const completed = this.tasks.filter((t) => t.status === 'done').length\n    const inProgress = this.tasks.filter((t) => t.status === 'in-progress').length\n    const milestones = this.tasks.filter((t) => t.isMilestone).length\n    const progress = total === 0 ? 0 : Math.round(this.tasks.reduce((acc, t) => acc + (t.progress ?? 0), 0) / total)\n    return { total, completed, inProgress, milestones, progress }\n  }\n\n  private get selectedTask(): GanttTask | null {\n    return this.tasks.find((t) => t.id === this.selectedTaskId) ?? null\n  }\n\n  private statusBadge(task: GanttTask): { label: string; variant: string; cls: string } {\n    switch (task.status) {\n      case 'done':\n        return { label: 'Completed', variant: 'outline', cls: 'text-success border-success/30' }\n      case 'in-progress':\n        return { label: 'In Progress', variant: 'default', cls: '' }\n      case 'blocked':\n        return { label: 'Blocked', variant: 'destructive', cls: '' }\n      default:\n        return { label: 'To Do', variant: 'secondary', cls: '' }\n    }\n  }\n\n  private submitAdd(e: Event) {\n    e.preventDefault()\n    if (!this.addName.trim()) return\n    const task: GanttTask = {\n      id: `task-${Date.now()}`,\n      name: this.addName.trim(),\n      startDate: this.addStart,\n      endDate: this.addMilestone ? this.addStart : this.addEnd,\n      status: this.addStatus as GanttTaskStatus,\n      progress: this.addMilestone ? 100 : 0,\n      isMilestone: this.addMilestone,\n      assignee: {\n        name: this.addAssignee,\n        initials: this.addAssignee\n          .split(' ')\n          .map((p) => p[0])\n          .join(''),\n      },\n    }\n    this.tasks = [...this.tasks, task]\n    this.dispatchEvent(new CustomEvent('task-add', { detail: task, bubbles: true, composed: true }))\n    this.addOpen = false\n    this.addName = ''\n    this.addMilestone = false\n  }\n\n  render() {\n    const m = this.metrics\n    const filtered = this.filteredTasks\n    const selected = this.selectedTask\n    const badge = selected ? this.statusBadge(selected) : null\n    return html`\n      <div data-uipkge=\"\" data-slot=\"project-roadmap\" class=\"space-y-6\">\n        <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n            <h2 class=\"text-foreground text-xl font-bold tracking-tight sm:text-2xl\">${this.heading}</h2>\n            <p class=\"text-muted-foreground mt-0.5 text-sm\">${this.description}</p>\n          </div>\n          <div class=\"flex items-center gap-2\">\n            <uip-button aria-label=\"Download attachment\" variant=\"outline\" size=\"sm\">\n              ${icon(Download, 'download', 'mr-1 size-3.5')} Export\n            </uip-button>\n            <uip-button\n              size=\"sm\"\n              @click=${() => {\n                this.addOpen = true\n              }}\n            >\n              ${icon(Plus, 'plus', 'mr-1 size-3.5')} Add Task\n            </uip-button>\n          </div>\n        </div>\n\n        <div data-slot=\"roadmap-metrics\" class=\"grid grid-cols-2 gap-3 sm:grid-cols-4\">\n          <uip-card class=\"bg-card/50 border-border/80 shadow-none\">\n            <uip-card-content class=\"flex items-center justify-between p-3.5\">\n              <div>\n                <p class=\"text-muted-foreground text-xs font-medium\">Overall Progress</p>\n                <p class=\"text-foreground mt-0.5 font-mono text-xl font-bold\">${m.progress}%</p>\n              </div>\n              <div class=\"bg-primary/10 text-primary flex size-8 items-center justify-center rounded-full\">\n                ${icon(Clock, 'clock', 'size-4')}\n              </div>\n            </uip-card-content>\n          </uip-card>\n          <uip-card class=\"bg-card/50 border-border/80 shadow-none\">\n            <uip-card-content class=\"flex items-center justify-between p-3.5\">\n              <div>\n                <p class=\"text-muted-foreground text-xs font-medium\">In Progress</p>\n                <p class=\"text-foreground mt-0.5 font-mono text-xl font-bold\">${m.inProgress}</p>\n              </div>\n              <div class=\"bg-info/10 text-info flex size-8 items-center justify-center rounded-full\">\n                ${icon(AlertCircle, 'circle-alert', 'size-4')}\n              </div>\n            </uip-card-content>\n          </uip-card>\n          <uip-card class=\"bg-card/50 border-border/80 shadow-none\">\n            <uip-card-content class=\"flex items-center justify-between p-3.5\">\n              <div>\n                <p class=\"text-muted-foreground text-xs font-medium\">Completed</p>\n                <p class=\"text-foreground mt-0.5 font-mono text-xl font-bold\">${m.completed}</p>\n              </div>\n              <div class=\"bg-success/10 text-success flex size-8 items-center justify-center rounded-full\">\n                ${icon(CheckCircle2, 'circle-check-big', 'size-4')}\n              </div>\n            </uip-card-content>\n          </uip-card>\n          <uip-card class=\"bg-card/50 border-border/80 shadow-none\">\n            <uip-card-content class=\"flex items-center justify-between p-3.5\">\n              <div>\n                <p class=\"text-muted-foreground text-xs font-medium\">Milestones</p>\n                <p class=\"text-foreground mt-0.5 font-mono text-xl font-bold\">${m.milestones}</p>\n              </div>\n              <div class=\"bg-warning/10 text-warning flex size-8 items-center justify-center rounded-full\">\n                ${icon(Milestone, 'milestone', 'size-4')}\n              </div>\n            </uip-card-content>\n          </uip-card>\n        </div>\n\n        <div class=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n          <div class=\"flex max-w-lg flex-1 flex-wrap items-center gap-2\">\n            <uip-input\n              .value=${this.searchQuery}\n              placeholder=\"Filter tasks by name...\"\n              class=\"w-56 text-xs\"\n              @input=${(e: Event) => {\n                this.searchQuery = (e.target as UipInput).value\n              }}\n            >\n              <span slot=\"prefix\">${icon(Search, 'search', 'size-3.5')}</span>\n            </uip-input>\n            <uip-select\n              value=${this.statusFilter}\n              class=\"h-8 w-32 text-xs\"\n              @change=${(e: Event) => {\n                this.statusFilter = (e.target as UipSelect).value\n              }}\n            >\n              <option value=\"all\">All Status</option>\n              <option value=\"done\">Completed</option>\n              <option value=\"in-progress\">In Progress</option>\n              <option value=\"todo\">To Do</option>\n            </uip-select>\n            <uip-select\n              value=${this.assigneeFilter}\n              class=\"h-8 w-36 text-xs\"\n              @change=${(e: Event) => {\n                this.assigneeFilter = (e.target as UipSelect).value\n              }}\n            >\n              <option value=\"all\">All Assignees</option>\n              <option value=\"Sarah Connor\">Sarah Connor</option>\n              <option value=\"Marcus Rivera\">Marcus Rivera</option>\n              <option value=\"Priya Nair\">Priya Nair</option>\n              <option value=\"Sundar Krishnan\">Sundar Krishnan</option>\n              <option value=\"Diane Cho\">Diane Cho</option>\n            </uip-select>\n          </div>\n          <p class=\"text-muted-foreground font-mono text-xs\">\n            Showing <span class=\"text-foreground font-bold\">${filtered.length}</span> of ${this.tasks.length}\n            deliverables\n            <span class=\"text-muted-foreground/60 ml-2\">(Right-click task for quick actions)</span>\n          </p>\n        </div>\n\n        <uip-gantt\n          .tasks=${filtered}\n          scale=${this.scale}\n          header-title=${this.heading}\n          class=\"h-[480px]\"\n          @task-click=${(e: CustomEvent<{ task: GanttTask }>) => {\n            this.selectedTaskId = e.detail.task.id\n            this.sheetOpen = true\n          }}\n          @scale-change=${(e: CustomEvent<{ scale: GanttScale }>) => {\n            this.scale = e.detail.scale\n          }}\n        ></uip-gantt>\n\n        <uip-sheet\n          ?open=${this.sheetOpen}\n          heading=${selected?.name ?? ''}\n          description=${selected ? `Task ID: ${selected.id}` : ''}\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.sheetOpen = e.detail.open\n          }}\n        >\n          ${selected && badge\n            ? html`\n                <div class=\"space-y-2 border-b pb-4\">\n                  <div class=\"flex items-center gap-2\">\n                    <uip-badge variant=${badge.variant} class=${badge.cls}>${badge.label}</uip-badge>\n                    ${selected.isMilestone\n                      ? html`<uip-badge variant=\"outline\" class=\"border-warning/30 text-warning\">Milestone</uip-badge>`\n                      : ''}\n                  </div>\n                </div>\n                <div class=\"space-y-5 py-5 text-sm\">\n                  <div class=\"space-y-1.5\">\n                    <span class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Schedule</span>\n                    <div class=\"text-foreground flex items-center gap-2 font-mono text-xs\">\n                      ${icon(Calendar, 'calendar', 'text-muted-foreground size-4')}\n                      <span>${selected.startDate}</span>\n                      <span>&rarr;</span>\n                      <span>${selected.endDate}</span>\n                    </div>\n                  </div>\n                  ${selected.progress != null\n                    ? html`\n                        <div class=\"space-y-1.5\">\n                          <div class=\"flex items-center justify-between text-xs\">\n                            <span class=\"text-muted-foreground font-medium tracking-wider uppercase\">Completion</span>\n                            <span class=\"font-mono font-bold\">${selected.progress}%</span>\n                          </div>\n                          <div class=\"bg-muted h-2 w-full overflow-hidden rounded-full\">\n                            <div\n                              style=\"width: ${selected.progress}%\"\n                              class=\"bg-primary h-full rounded-full transition-[width] duration-300\"\n                            ></div>\n                          </div>\n                        </div>\n                      `\n                    : ''}\n                  ${selected.assignee\n                    ? html`\n                        <div class=\"space-y-1.5\">\n                          <span class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Owner</span>\n                          <div class=\"flex items-center gap-2.5\">\n                            <div\n                              class=\"bg-primary/10 text-primary flex size-7 items-center justify-center rounded-full text-xs font-medium\"\n                            >\n                              ${selected.assignee.initials || selected.assignee.name.charAt(0)}\n                            </div>\n                            <span class=\"text-foreground text-sm font-medium\">${selected.assignee.name}</span>\n                          </div>\n                        </div>\n                      `\n                    : ''}\n                </div>\n              `\n            : ''}\n        </uip-sheet>\n\n        <uip-dialog\n          ?open=${this.addOpen}\n          heading=\"Add Roadmap Task\"\n          description=\"Create a new scheduled deliverable or critical milestone in the project Gantt timeline.\"\n          @open-change=${(e: CustomEvent<{ open: boolean }>) => {\n            this.addOpen = e.detail.open\n          }}\n        >\n          <form @submit=${this.submitAdd} class=\"space-y-4 py-2\">\n            <div class=\"space-y-1.5\">\n              <uip-label for=\"roadmap-task-name\">Task Name</uip-label>\n              <uip-input\n                id=\"roadmap-task-name\"\n                .value=${this.addName}\n                placeholder=\"e.g. Design Token Hierarchy & Specs\"\n                required\n                @input=${(e: Event) => {\n                  this.addName = (e.target as UipInput).value\n                }}\n              ></uip-input>\n            </div>\n            <div class=\"grid grid-cols-2 gap-3\">\n              <div class=\"space-y-1.5\">\n                <uip-label for=\"roadmap-start-date\">Start Date</uip-label>\n                <uip-input\n                  id=\"roadmap-start-date\"\n                  type=\"date\"\n                  .value=${this.addStart}\n                  required\n                  @input=${(e: Event) => {\n                    this.addStart = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n              <div class=\"space-y-1.5\">\n                <uip-label for=\"roadmap-end-date\">End Date</uip-label>\n                <uip-input\n                  id=\"roadmap-end-date\"\n                  type=\"date\"\n                  .value=${this.addEnd}\n                  ?disabled=${this.addMilestone}\n                  required\n                  @input=${(e: Event) => {\n                    this.addEnd = (e.target as UipInput).value\n                  }}\n                ></uip-input>\n              </div>\n            </div>\n            <div class=\"border-border/80 bg-muted/20 flex items-center space-x-2 rounded-md border p-3\">\n              <uip-checkbox\n                id=\"roadmap-milestone-toggle\"\n                .checked=${this.addMilestone}\n                @checked-change=${(e: CustomEvent<{ checked: boolean }>) => {\n                  this.addMilestone = e.detail.checked\n                }}\n              ></uip-checkbox>\n              <div class=\"grid gap-0.5 leading-none\">\n                <label for=\"roadmap-milestone-toggle\" class=\"text-foreground cursor-pointer text-xs font-semibold select-none\">\n                  Key Project Milestone\n                </label>\n                <p class=\"text-muted-foreground text-xs\">\n                  Marks a zero-duration target deliverable (e.g. Beta Release, Audit sign-off)\n                </p>\n              </div>\n            </div>\n            <div class=\"grid grid-cols-2 gap-3\">\n              <div class=\"space-y-1.5\">\n                <uip-label>Initial Status</uip-label>\n                <uip-select\n                  value=${this.addStatus}\n                  @change=${(e: Event) => {\n                    this.addStatus = (e.target as UipSelect).value as 'todo' | 'in-progress' | 'done'\n                  }}\n                >\n                  <option value=\"todo\">To Do</option>\n                  <option value=\"in-progress\">In Progress</option>\n                  <option value=\"done\">Completed</option>\n                </uip-select>\n              </div>\n              <div class=\"space-y-1.5\">\n                <uip-label>Assignee</uip-label>\n                <uip-select\n                  value=${this.addAssignee}\n                  @change=${(e: Event) => {\n                    this.addAssignee = (e.target as UipSelect).value\n                  }}\n                >\n                  <option value=\"Marcus Rivera\">Marcus Rivera</option>\n                  <option value=\"Sarah Connor\">Sarah Connor</option>\n                  <option value=\"Priya Nair\">Priya Nair</option>\n                  <option value=\"Sundar Krishnan\">Sundar Krishnan</option>\n                  <option value=\"Diane Cho\">Diane Cho</option>\n                </uip-select>\n              </div>\n            </div>\n          </form>\n          <div slot=\"footer\">\n            <uip-button\n              variant=\"outline\"\n              @click=${() => {\n                this.addOpen = false\n              }}\n              >Cancel</uip-button\n            >\n            <uip-button\n              @click=${this.submitAdd}\n              ?disabled=${!this.addName.trim()}\n            >\n              ${icon(Plus, 'plus', 'mr-1.5 size-3.5')} Create Task\n            </uip-button>\n          </div>\n        </uip-dialog>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-project-roadmap') || customElements.define('uip-project-roadmap', UipProjectRoadmap)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-project-roadmap': UipProjectRoadmap\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/project-roadmap/project-roadmap.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/button.json",
    "https://uipkge.dev/r/lit/card.json",
    "https://uipkge.dev/r/lit/checkbox.json",
    "https://uipkge.dev/r/lit/dialog.json",
    "https://uipkge.dev/r/lit/gantt.json",
    "https://uipkge.dev/r/lit/input.json",
    "https://uipkge.dev/r/lit/label.json",
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/select.json",
    "https://uipkge.dev/r/lit/sheet.json"
  ]
}
