{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "roadmap-timeline",
  "title": "Roadmap Timeline",
  "description": "A project roadmap timeline drags bars and shows a reschedule toast.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/roadmap-timeline/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/InterTight\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\n\nexport type RoadmapStatus = \"done\" | \"active\" | \"planned\";\n\nexport interface RoadmapProject {\n  name: string;\n  /** Start date, e.g. \"Aug 10\". */\n  start: string;\n  /** Target date (inclusive), e.g. \"Sep 18\". */\n  end: string;\n  status: RoadmapStatus;\n  /** 0–1, drawn as the darker part of the bar and as the status pie. */\n  progress?: number;\n  lead: string;\n  color: string;\n  /** Name of the project this one is blocked by. */\n  dependsOn?: string;\n}\n\nexport interface RoadmapTimelineProps {\n  projects?: RoadmapProject[];\n  milestone?: { name: string; date: string };\n  /** The \"today\" marker, e.g. \"Aug 26\". */\n  today?: string;\n  /** Name of the project the cursor drags. */\n  dragProject?: string;\n  /** How many days the dragged project moves (positive = later). */\n  dragDays?: number;\n  /** First visible month, 0-based (6 = July). */\n  startMonth?: number;\n  monthCount?: number;\n  year?: number;\n  title?: string;\n  workspace?: string;\n  theme?: \"light\" | \"dark\";\n  accent?: string;\n  speed?: number;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}\n\nconst DEFAULT_PROJECTS: RoadmapProject[] = [\n  {\n    name: \"Billing v2\",\n    start: \"Jul 6\",\n    end: \"Aug 7\",\n    status: \"done\",\n    progress: 1,\n    lead: \"Maya Chen\",\n    color: \"#3FA27B\",\n  },\n  {\n    name: \"Public API\",\n    start: \"Aug 10\",\n    end: \"Sep 18\",\n    status: \"active\",\n    progress: 0.4,\n    lead: \"Priya Raman\",\n    color: \"#3B5BFD\",\n    dependsOn: \"Billing v2\",\n  },\n  {\n    name: \"Mobile app 1.0\",\n    start: \"Sep 24\",\n    end: \"Oct 29\",\n    status: \"planned\",\n    progress: 0,\n    lead: \"Ana Souza\",\n    color: \"#E5795B\",\n    dependsOn: \"Public API\",\n  },\n  {\n    name: \"Onboarding\",\n    start: \"Jul 16\",\n    end: \"Aug 28\",\n    status: \"active\",\n    progress: 0.8,\n    lead: \"Leo Park\",\n    color: \"#D9A33A\",\n  },\n  {\n    name: \"Usage analytics\",\n    start: \"Aug 19\",\n    end: \"Oct 2\",\n    status: \"active\",\n    progress: 0.25,\n    lead: \"Sam Ortiz\",\n    color: \"#2A9FBF\",\n  },\n  {\n    name: \"Docs site\",\n    start: \"Oct 12\",\n    end: \"Nov 13\",\n    status: \"planned\",\n    progress: 0,\n    lead: \"Maya Chen\",\n    color: \"#D55C9A\",\n  },\n];\n\nconst MONTHS = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\nconst monthLen = (m: number, y: number) =>\n  [31, y % 4 === 0 ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m % 12];\nfunction weekday(y: number, m: number, d: number) {\n  const t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];\n  const yy = m < 2 ? y - 1 : y;\n  return (\n    (yy +\n      Math.floor(yy / 4) -\n      Math.floor(yy / 100) +\n      Math.floor(yy / 400) +\n      t[m] +\n      d) %\n    7\n  );\n}\nfunction dayIndex(label: string, startMonth: number, year: number) {\n  const [mon, d] = label.trim().split(/\\s+/);\n  const m = MONTHS.indexOf(mon);\n  let idx = 0;\n  for (let k = startMonth; k < (m < startMonth ? m + 12 : m); k++)\n    idx += monthLen(k, year);\n  return idx + Number(d) - 1;\n}\nfunction dayLabel(idx: number, startMonth: number, year: number) {\n  let m = startMonth;\n  let d = Math.round(idx);\n  while (d >= monthLen(m, year)) {\n    d -= monthLen(m, year);\n    m++;\n  }\n  while (d < 0) {\n    m--;\n    d += monthLen(m, year);\n  }\n  return `${MONTHS[m % 12]} ${d + 1}`;\n}\n\nconst HEAD_H = 52;\nconst TOOL_H = 48;\nconst THEAD_H = 52;\nconst MIN_GAP = 3;\n\nconst CL = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\nconst easeOut = Easing.bezier(0.22, 1, 0.36, 1);\nconst inOut = Easing.bezier(0.65, 0, 0.35, 1);\nconst moveEase = Easing.bezier(0.65, 0, 0.35, 1);\nconst exitEase = Easing.bezier(0.55, 0, 1, 0.45);\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst tween = (f: number, a: number, b: number, easing = easeOut) =>\n  interpolate(f, [a, b], [0, 1], { ...CL, easing });\nconst clamp = (v: number, lo: number, hi: number) =>\n  Math.min(hi, Math.max(lo, v));\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\nfunction withAlpha(color: string, alpha: number) {\n  const hex = color.replace(\"#\", \"\");\n  const full = hex.length === 3 ? hex.replace(/(.)/g, \"$1$1\") : hex;\n  if (!/^[0-9a-fA-F]{6}$/.test(full)) return color;\n  const n = Number.parseInt(full, 16);\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n}\n\ninterface Pal {\n  stage: string;\n  win: string;\n  panel: string;\n  ring: string;\n  line: string;\n  grid: string;\n  text: string;\n  muted: string;\n  faint: string;\n  active: string;\n  barText: string;\n  bar: string;\n  barFill: string;\n  toast: string;\n  dark: boolean;\n}\n\nfunction palette(theme: \"light\" | \"dark\"): Pal {\n  return theme === \"dark\"\n    ? {\n        stage: \"rgba(4,4,10,0.34)\",\n        win: \"#0F1011\",\n        panel: \"#111214\",\n        ring: \"rgba(255,255,255,0.08)\",\n        line: \"rgba(255,255,255,0.06)\",\n        grid: \"rgba(255,255,255,0.05)\",\n        text: \"#EDEDEF\",\n        muted: \"#9C9DA4\",\n        faint: \"#6E6F76\",\n        active: \"rgba(255,255,255,0.06)\",\n        barText: \"#EDEDEF\",\n        bar: \"#1A1B1E\",\n        barFill: \"rgba(255,255,255,0.06)\",\n        toast: \"#202124\",\n        dark: true,\n      }\n    : {\n        stage: \"rgba(245,245,247,0.28)\",\n        win: \"#F4F4F5\",\n        panel: \"#FFFFFF\",\n        ring: \"rgba(17,19,24,0.08)\",\n        line: \"rgba(17,19,24,0.06)\",\n        grid: \"rgba(17,19,24,0.05)\",\n        text: \"#18181B\",\n        muted: \"#5F6168\",\n        faint: \"#8E9096\",\n        active: \"rgba(17,19,24,0.05)\",\n        barText: \"#1C1D21\",\n        bar: \"#F6F6F7\",\n        barFill: \"rgba(17,19,24,0.05)\",\n        toast: \"#FFFFFF\",\n        dark: false,\n      };\n}\n\nconst ICONS = {\n  inbox: \"M2.75 9.25h3l.75 1.5h3l.75-1.5h3M4.4 3.25h7.2l1.65 6v3.5h-10.5v-3.5z\",\n  issues:\n    \"M8 13.25A5.25 5.25 0 1 0 8 2.75a5.25 5.25 0 0 0 0 10.5zM8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\",\n  target:\n    \"M8 13.25A5.25 5.25 0 1 0 8 2.75a5.25 5.25 0 0 0 0 10.5zM8 10.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5z\",\n  box: \"M8 2.5l5 2.5v6L8 13.5 3 11V5zM3 5l5 2.5L13 5M8 7.5v6\",\n  layers: \"M8 2.75l5.25 2.75L8 8.25 2.75 5.5zM2.75 8.25 8 11l5.25-2.75\",\n  cycle: \"M13 8a5 5 0 1 1-1.46-3.54M13 3v2.5h-2.5\",\n  search:\n    \"M7.25 11.75a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM13.25 13.25l-2.8-2.8\",\n  compose:\n    \"M8.5 3H4.25A1.25 1.25 0 0 0 3 4.25v7.5A1.25 1.25 0 0 0 4.25 13h7.5A1.25 1.25 0 0 0 13 11.75V7.5M11.6 2.4a1.2 1.2 0 0 1 1.7 1.7L8.5 8.9 6.5 9.5l.6-2z\",\n  chevDown: \"M4.75 6.5 8 9.75l3.25-3.25\",\n  chevRight: \"M6.5 4.75 9.75 8 6.5 11.25\",\n  star: \"M8 2.75l1.6 3.3 3.6.5-2.6 2.5.6 3.6L8 10.95l-3.2 1.7.6-3.6-2.6-2.5 3.6-.5z\",\n  filter: \"M2.5 4.5h11M4.5 8h7M6.5 11.5h3\",\n  sliders:\n    \"M2.75 5h5.5M11.25 5h2M2.75 11h2M7.75 11h5.5M9.75 3.4v3.2M6.25 9.4v3.2\",\n  plus: \"M8 3.5v9M3.5 8h9\",\n  link: \"M6.75 9.25l2.5-2.5M7.25 4.75l.9-.9a2.5 2.5 0 0 1 3.5 3.5l-.9.9M8.75 11.25l-.9.9a2.5 2.5 0 0 1-3.5-3.5l.9-.9\",\n  check: \"M4.5 8.25 6.9 10.5 11.5 5.5\",\n  undo: \"M5.5 4.25 3 6.75l2.5 2.5M3 6.75h6.25a3.25 3.25 0 0 1 0 6.5H7.5\",\n} as const;\ntype IconName = keyof typeof ICONS;\n\nfunction Icon({\n  name,\n  color,\n  size = 16,\n  width = 1.4,\n}: {\n  name: IconName;\n  color: string;\n  size?: number;\n  width?: number;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 16 16\"\n      style={{ flexShrink: 0, display: \"block\" }}\n    >\n      <path\n        d={ICONS[name]}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={width}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nconst AVATAR_COLORS = [\"#E5795B\", \"#4F8BE8\", \"#9B6BDF\", \"#3FA27B\", \"#D9A33A\"];\nfunction Avatar({\n  name,\n  size = 18,\n  ring,\n}: {\n  name: string;\n  size?: number;\n  ring?: string;\n}) {\n  const idx =\n    [...name].reduce((s, ch) => s + ch.charCodeAt(0), 0) % AVATAR_COLORS.length;\n  return (\n    <div\n      style={{\n        width: size,\n        height: size,\n        borderRadius: 999,\n        background: `radial-gradient(circle at 50% 34%, rgba(255,255,255,0.42) 0 22%, transparent 23%), radial-gradient(circle at 50% 108%, rgba(255,255,255,0.32) 0 42%, transparent 43%), ${AVATAR_COLORS[idx]}`,\n        flexShrink: 0,\n        boxShadow: ring ? `0 0 0 1.5px ${ring}` : undefined,\n      }}\n    />\n  );\n}\n\nfunction StatusIcon({\n  status,\n  progress,\n  accent,\n  pal,\n}: {\n  status: RoadmapStatus;\n  progress: number;\n  accent: string;\n  pal: Pal;\n}) {\n  if (status === \"done\") {\n    return (\n      <svg width={14} height={14} viewBox=\"0 0 14 14\" style={{ flexShrink: 0 }}>\n        <circle cx={7} cy={7} r={6.5} fill={accent} />\n        <path\n          d=\"M4.3 7.2 6.2 9 9.8 5.2\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeWidth={1.5}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    );\n  }\n  const color = status === \"active\" ? \"#E2A93B\" : pal.faint;\n  const a = clamp(progress, 0, 0.999) * Math.PI * 2;\n  const pie =\n    progress > 0\n      ? `M7 7 L7 3.25 A3.75 3.75 0 ${a > Math.PI ? 1 : 0} 1 ${7 + 3.75 * Math.sin(a)} ${7 - 3.75 * Math.cos(a)} Z`\n      : \"\";\n  return (\n    <svg width={14} height={14} viewBox=\"0 0 14 14\" style={{ flexShrink: 0 }}>\n      <circle\n        cx={7}\n        cy={7}\n        r={5.9}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={1.5}\n        strokeDasharray={status === \"planned\" ? \"2.2 1.9\" : undefined}\n      />\n      {pie ? <path d={pie} fill={color} /> : null}\n    </svg>\n  );\n}\n\ninterface Pt {\n  x: number;\n  y: number;\n}\ninterface Key extends Pt {\n  t: number;\n}\nfunction pathAt(keys: Key[], f: number): Pt {\n  if (f <= keys[0].t) return keys[0];\n  for (let i = 1; i < keys.length; i++) {\n    const a = keys[i - 1];\n    const b = keys[i];\n    if (f <= b.t) {\n      const p = moveEase(clamp((f - a.t) / Math.max(1, b.t - a.t), 0, 1));\n      const dx = b.x - a.x;\n      const dy = b.y - a.y;\n      const d = Math.hypot(dx, dy) || 1;\n      const arc = Math.sin(p * Math.PI) * Math.min(28, d * 0.12);\n      return {\n        x: lerp(a.x, b.x, p) + (-dy / d) * arc,\n        y: lerp(a.y, b.y, p) + (dx / d) * arc,\n      };\n    }\n  }\n  return keys[keys.length - 1];\n}\n\nfunction Cursor({ x, y, press }: { x: number; y: number; press: number }) {\n  return (\n    <svg\n      width={22}\n      height={26}\n      viewBox=\"0 0 20 24\"\n      style={{\n        position: \"absolute\",\n        left: x - 2,\n        top: y - 2,\n        transformOrigin: \"2px 2px\",\n        transform: `scale(${1 - 0.1 * press})`,\n        filter: \"drop-shadow(0 2px 3px rgba(0,0,0,0.28))\",\n        overflow: \"visible\",\n      }}\n    >\n      <path\n        d=\"M2 2 L2 19.5 L6.6 15.4 L9.8 22.4 L12.9 21 L9.8 14.2 L16 14.2 Z\"\n        fill=\"#FFFFFF\"\n        stroke=\"#16171A\"\n        strokeWidth={1.1}\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction connectorPath(sx: number, sy: number, x2: number, y2: number) {\n  const r = clamp(Math.min(x2 - 5 - sx, y2 - sy) * 0.5, 2, 9);\n  return `M${sx} ${sy} V${y2 - r} Q${sx} ${y2} ${sx + r} ${y2} H${x2 - 5}`;\n}\n\nexport function RoadmapTimeline({\n  projects = DEFAULT_PROJECTS,\n  milestone = { name: \"Public launch\", date: \"Nov 18\" },\n  today = \"Aug 26\",\n  dragProject = \"Public API\",\n  dragDays = 14,\n  startMonth = 6,\n  monthCount = 5,\n  year = 2026,\n  title = \"Roadmap · H2 2026\",\n  workspace = \"remocn\",\n  theme = \"light\",\n  accent = \"#3B5BFD\",\n  speed = 1,\n  ground,\n  groundParams,\n}: RoadmapTimelineProps) {\n  const frame = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const tall = H > W;\n  const DW = tall ? 720 : 1280;\n  const DH = tall ? 1280 : 720;\n  const u = Math.min(W / DW, H / DH);\n  const pal = palette(theme);\n  const LIST_W = tall ? 0 : 260;\n  const ROW_H = tall ? 96 : 56;\n  const BAR_H = tall ? 40 : 32;\n  const ROWS_Y = HEAD_H + TOOL_H + THEAD_H;\n  const panelW = tall ? 672 : 1000;\n  const panelH = ROWS_Y + (projects.length + 1) * ROW_H + 56;\n  const PANEL = {\n    x: (DW - panelW) / 2,\n    y: (DH - panelH) / 2,\n    w: panelW,\n    h: panelH,\n  };\n  const GRID_W = PANEL.w - LIST_W;\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n  const monthStarts: number[] = [];\n  let totalDays = 0;\n  for (let k = 0; k < monthCount; k++) {\n    monthStarts.push(totalDays);\n    totalDays += monthLen(startMonth + k, year);\n  }\n  const DAY = GRID_W / totalDays;\n  const firstWd = weekday(\n    year + Math.floor(startMonth / 12),\n    startMonth % 12,\n    1,\n  );\n  const firstMonday = (8 - firstWd) % 7;\n  const mondays: number[] = [];\n  for (let d = firstMonday; d < totalDays; d += 7) mondays.push(d);\n  const di = (label: string) => dayIndex(label, startMonth, year);\n  const lbl = (idx: number) => dayLabel(idx, startMonth, year);\n\n  const T = {\n    rows: 8,\n    bars: 12,\n    links: 40,\n    milestone: 50,\n    sweep: 56,\n    cursorIn: 76,\n    grab: 99,\n    dragStart: 102,\n    dragEnd: 128,\n    toast: 136,\n  };\n\n  const parsed = projects.map((p) => ({\n    ...p,\n    s: di(p.start),\n    e: di(p.end),\n  }));\n  const dragIdx = Math.max(\n    0,\n    parsed.findIndex((p) => p.name === dragProject),\n  );\n  const dragAt = (f: number) =>\n    dragDays * tween(f, T.dragStart, T.dragEnd, inOut);\n\n  const shifts: number[] = parsed.map(() => 0);\n  const shiftAt = (f: number) => {\n    const out = parsed.map((_, i) => (i === dragIdx ? dragAt(f) : 0));\n    for (let pass = 0; pass < 3; pass++) {\n      parsed.forEach((p, i) => {\n        if (!p.dependsOn || i === dragIdx) return;\n        const b = parsed.findIndex((q) => q.name === p.dependsOn);\n        if (b < 0) return;\n        const need = parsed[b].e + out[b] + MIN_GAP - p.s;\n        out[i] = Math.max(0, need);\n      });\n    }\n    return out;\n  };\n  const lagged = shiftAt(frame - 3);\n  const live = shiftAt(frame);\n  parsed.forEach((_, i) => {\n    shifts[i] = i === dragIdx ? live[i] : lagged[i];\n  });\n  const finalShifts = shiftAt(9999);\n  const moved = parsed\n    .map((_p, i) => (finalShifts[i] > 0.5 && i !== dragIdx ? i : -1))\n    .filter((i) => i >= 0);\n\n  const rowY = (i: number) => ROWS_Y + i * ROW_H;\n  const barX = (i: number) => LIST_W + (parsed[i].s + shifts[i]) * DAY;\n  const barW = (i: number) => (parsed[i].e - parsed[i].s + 1) * DAY;\n  const barMidY = (i: number) => rowY(i) + ROW_H / 2;\n\n  const gx = (x: number) => PANEL.x + x;\n  const gy = (y: number) => PANEL.y + y;\n  const grabOffset = Math.min(barW(dragIdx) * 0.42, 90);\n  const grab0 = {\n    x: gx(LIST_W + parsed[dragIdx].s * DAY + grabOffset),\n    y: gy(barMidY(dragIdx) + 4),\n  };\n  const dragPx = dragAt(frame) * DAY;\n  const keys: Key[] = [\n    { t: T.cursorIn, x: DW - 90, y: DH - 50 },\n    { t: T.grab - 4, ...grab0 },\n  ];\n  let cur = pathAt(keys, frame);\n  if (frame > T.grab - 4) cur = { x: grab0.x + dragPx, y: grab0.y };\n  if (frame > T.dragEnd + 4) {\n    const away = tween(frame, T.dragEnd + 6, T.dragEnd + 26, moveEase);\n    cur = {\n      x: grab0.x + dragDays * DAY + 46 * away,\n      y: grab0.y + 64 * away + Math.sin(away * Math.PI) * 6,\n    };\n  }\n  const pressDown = spring({ frame: frame - T.grab, fps, config: CHIP });\n  const pressUp = tween(frame, T.dragEnd + 1, T.dragEnd + 5, exitEase);\n  const press = clamp(pressDown, 0, 1.2) * (1 - pressUp);\n  const cursorIn = tween(frame, T.cursorIn, T.cursorIn + 6);\n  const hover = tween(frame, T.grab - 6, T.grab - 1);\n  const lifted = press;\n  const tipIn = spring({ frame: frame - T.dragStart, fps, config: CHIP });\n  const tipOut = tween(frame, T.dragEnd + 2, T.dragEnd + 6, exitEase);\n  const tip = clamp(tipIn, 0, 1) * (1 - tipOut);\n  const flash = (at: number) =>\n    tween(frame, at, at + 4) * (1 - tween(frame, at + 10, at + 34, inOut));\n\n  const enter = spring({ frame, fps, config: HEAVY });\n  const camT = tween(frame, 66, 98, inOut);\n  const sweep = tween(frame, T.sweep, T.sweep + 20, inOut);\n  const todayIdx = di(today);\n  const todayDay = sweep * todayIdx;\n  const todayX = LIST_W + (todayDay + 0.5) * DAY;\n  const toast = spring({ frame: frame - T.toast, fps, config: CARD });\n  const ms = spring({ frame: frame - T.milestone, fps, config: CHIP });\n  const msIdx = di(milestone.date);\n  const msRow = parsed.length;\n  const msLabelLeft = (msIdx + 0.5) * DAY + 120 > GRID_W;\n\n  const links = parsed\n    .map((p, i) => {\n      if (!p.dependsOn) return null;\n      const b = parsed.findIndex((q) => q.name === p.dependsOn);\n      return b < 0 ? null : { from: b, to: i };\n    })\n    .filter((l): l is { from: number; to: number } => l !== null);\n\n  const dates = (i: number) =>\n    `${lbl(parsed[i].s + shifts[i])} – ${lbl(parsed[i].e + shifts[i])}`;\n\n  const btn = (label: React.ReactNode, icon?: IconName) => (\n    <div\n      style={{\n        height: 32,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: 6,\n        padding: icon && !label ? \"0 7px\" : \"0 12px\",\n        borderRadius: 8,\n        boxShadow: `inset 0 0 0 1px ${pal.ring}`,\n        fontSize: 16,\n        color: pal.text,\n        background: pal.panel,\n      }}\n    >\n      {icon ? <Icon name={icon} color={pal.muted} size={15} /> : null}\n      {label}\n    </div>\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        fontFamily,\n        fontWeight: 400,\n        overflow: \"hidden\",\n        WebkitFontSmoothing: \"antialiased\",\n      }}\n    >\n      <GshGround\n        preset={ground ?? (pal.dark ? \"europa\" : \"paper-crease\")}\n        params={groundParams}\n        timeOffset={pal.dark ? 4 : 2}\n        timeScale={0.3}\n      />\n      <AbsoluteFill style={{ background: pal.stage }} />\n      <div\n        style={{\n          position: \"absolute\",\n          left: (W - DW * u) / 2,\n          top: (H - DH * u) / 2,\n          width: DW,\n          height: DH,\n          transformOrigin: \"0 0\",\n          transform: `scale(${u})`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            transformOrigin: `${DW / 2}px ${DH / 2}px`,\n            transform: `scale(${0.95 + 0.05 * camT}) translateY(${(1 - enter) * 48}px)`,\n            opacity: clamp(enter * 1.6, 0, 1),\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: PANEL.x,\n              top: PANEL.y,\n              width: PANEL.w,\n              height: PANEL.h,\n              borderRadius: 16,\n              background: pal.panel,\n              boxShadow: pal.dark\n                ? \"0 0 0 1px rgba(255,255,255,0.08), inset 0 1px 0 rgba(255,255,255,0.05), 0 30px 80px -24px rgba(0,0,0,0.7)\"\n                : \"0 0 0 1px rgba(17,19,24,0.06), 0 1px 3px rgba(17,19,24,0.05), 0 16px 36px -16px rgba(17,19,24,0.16), 0 44px 90px -44px rgba(17,19,24,0.24)\",\n              overflow: \"hidden\",\n            }}\n          >\n            <div\n              style={{\n                height: HEAD_H,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 8,\n                padding: \"0 16px 0 20px\",\n                borderBottom: `1px solid ${pal.line}`,\n                fontSize: 16,\n              }}\n            >\n              <span style={{ color: pal.muted }}>{workspace}</span>\n              <Icon name=\"chevRight\" color={pal.faint} size={14} />\n              <span style={{ color: pal.text }}>{title}</span>\n              <div style={{ flex: 1 }} />\n              <div style={{ display: \"flex\" }}>\n                {[\"Maya Chen\", \"Leo Park\", \"Priya Raman\"].map((n, i) => (\n                  <div key={n} style={{ marginLeft: i ? -5 : 0 }}>\n                    <Avatar name={n} size={26} ring={pal.panel} />\n                  </div>\n                ))}\n              </div>\n              <div style={{ width: 4 }} />\n              {btn(null, \"link\")}\n            </div>\n            <div\n              style={{\n                height: TOOL_H,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 6,\n                padding: \"0 14px\",\n                borderBottom: `1px solid ${pal.line}`,\n              }}\n            >\n              {[\"All\", \"Timeline\", \"Board\"].map((t) => (\n                <div\n                  key={t}\n                  style={{\n                    height: 32,\n                    padding: \"0 12px\",\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    borderRadius: 8,\n                    fontSize: 16,\n                    color: t === \"Timeline\" ? pal.text : pal.muted,\n                    background: t === \"Timeline\" ? pal.active : \"transparent\",\n                    boxShadow:\n                      t === \"Timeline\" ? `inset 0 0 0 1px ${pal.line}` : \"none\",\n                  }}\n                >\n                  {t}\n                </div>\n              ))}\n              <div style={{ flex: 1 }} />\n              {btn(\"Today\")}\n              {btn(\n                <span style={{ display: \"flex\", alignItems: \"center\", gap: 4 }}>\n                  Months\n                  <Icon name=\"chevDown\" color={pal.muted} size={12} />\n                </span>,\n              )}\n            </div>\n\n            <div\n              style={{\n                position: \"absolute\",\n                left: LIST_W,\n                top: HEAD_H + TOOL_H,\n                width: GRID_W,\n                bottom: 0,\n                borderLeft: `1px solid ${pal.line}`,\n              }}\n            >\n              {monthStarts.map((m, k) => (\n                <div\n                  key={`m${m}`}\n                  style={{\n                    position: \"absolute\",\n                    left: m * DAY,\n                    top: 0,\n                    bottom: 0,\n                    borderLeft: k ? `1px solid ${pal.line}` : \"none\",\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: 10,\n                      top: 6,\n                      fontSize: 16,\n                      fontWeight: 400,\n                      color: pal.muted,\n                      whiteSpace: \"nowrap\",\n                    }}\n                  >\n                    {MONTHS[(startMonth + k) % 12]}\n                    {k === 0 ? (\n                      <span style={{ color: pal.faint }}> {year}</span>\n                    ) : null}\n                  </div>\n                </div>\n              ))}\n              {mondays.map((d) => (\n                <div\n                  key={`d${d}`}\n                  style={{\n                    position: \"absolute\",\n                    left: d * DAY,\n                    top: THEAD_H - 7,\n                    height: 6,\n                    borderLeft: `1px solid ${pal.ring}`,\n                  }}\n                />\n              ))}\n              {mondays.map((d) => (\n                <div\n                  key={`g${d}`}\n                  style={{\n                    position: \"absolute\",\n                    left: d * DAY,\n                    top: THEAD_H,\n                    bottom: 0,\n                    borderLeft: `1px dashed ${pal.grid}`,\n                  }}\n                />\n              ))}\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  top: THEAD_H - 1,\n                  borderTop: `1px solid ${pal.line}`,\n                }}\n              />\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: HEAD_H + TOOL_H,\n                width: LIST_W,\n                height: THEAD_H,\n                borderBottom: `1px solid ${pal.line}`,\n                display: LIST_W ? \"flex\" : \"none\",\n                alignItems: \"center\",\n                gap: 6,\n                padding: \"0 16px\",\n                boxSizing: \"border-box\",\n                fontSize: 16,\n                fontWeight: 400,\n                color: pal.muted,\n              }}\n            >\n              Projects\n              <span\n                style={{\n                  color: pal.faint,\n                  fontVariantNumeric: \"tabular-nums\",\n                }}\n              >\n                {parsed.length}\n              </span>\n              <div style={{ flex: 1 }} />\n              Target\n            </div>\n\n            {parsed.map((p, i) => {\n              const a = tween(frame, T.rows + i * 2, T.rows + i * 2 + 10);\n              const changed = i === dragIdx || moved.includes(i);\n              const swap = changed\n                ? tween(frame, T.dragEnd + 2, T.dragEnd + 8)\n                : 0;\n              const hl = changed\n                ? flash(T.dragEnd + 2 + (i === dragIdx ? 0 : 3))\n                : 0;\n              const oldEnd = lbl(p.e);\n              const newEnd = lbl(p.e + finalShifts[i]);\n              return (\n                <div\n                  key={p.name}\n                  style={{\n                    position: \"absolute\",\n                    left: 0,\n                    top: rowY(i),\n                    width: PANEL.w,\n                    height: ROW_H,\n                    borderBottom: `1px solid ${pal.line}`,\n                    background: withAlpha(accent, (pal.dark ? 0.1 : 0.05) * hl),\n                    opacity: a,\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: 0,\n                      width: LIST_W,\n                      height: ROW_H,\n                      display: LIST_W ? \"flex\" : \"none\",\n                      alignItems: \"center\",\n                      gap: 10,\n                      padding: \"0 14px 0 16px\",\n                      boxSizing: \"border-box\",\n                      transform: `translateY(${(1 - a) * 6}px)`,\n                    }}\n                  >\n                    <StatusIcon\n                      status={p.status}\n                      progress={p.progress ?? 0}\n                      accent={accent}\n                      pal={pal}\n                    />\n                    <span\n                      style={{\n                        fontSize: 16,\n                        fontWeight: 400,\n                        color: pal.text,\n                        whiteSpace: \"nowrap\",\n                        overflow: \"hidden\",\n                        textOverflow: \"ellipsis\",\n                        flex: 1,\n                      }}\n                    >\n                      {p.name}\n                    </span>\n                    <span\n                      style={{\n                        position: \"relative\",\n                        width: 62,\n                        height: 22,\n                        fontSize: 16,\n                        color: changed && swap > 0.5 ? pal.text : pal.muted,\n                        fontVariantNumeric: \"tabular-nums\",\n                        textAlign: \"right\",\n                      }}\n                    >\n                      <span\n                        style={{\n                          position: \"absolute\",\n                          right: 0,\n                          opacity: 1 - swap,\n                          transform: `translateY(${-5 * swap}px)`,\n                        }}\n                      >\n                        {oldEnd}\n                      </span>\n                      {changed ? (\n                        <span\n                          style={{\n                            position: \"absolute\",\n                            right: 0,\n                            opacity: swap,\n                            transform: `translateY(${5 * (1 - swap)}px)`,\n                          }}\n                        >\n                          {newEnd}\n                        </span>\n                      ) : null}\n                    </span>\n                  </div>\n                </div>\n              );\n            })}\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: rowY(msRow),\n                width: LIST_W,\n                height: ROW_H,\n                display: LIST_W ? \"flex\" : \"none\",\n                alignItems: \"center\",\n                gap: 10,\n                padding: \"0 14px 0 16px\",\n                boxSizing: \"border-box\",\n                opacity: tween(\n                  frame,\n                  T.rows + msRow * 2,\n                  T.rows + msRow * 2 + 10,\n                ),\n              }}\n            >\n              <svg width={14} height={14} viewBox=\"0 0 14 14\">\n                <rect\n                  x={3}\n                  y={3}\n                  width={8}\n                  height={8}\n                  rx={1.5}\n                  transform=\"rotate(45 7 7)\"\n                  fill=\"none\"\n                  stroke={pal.muted}\n                  strokeWidth={1.4}\n                />\n              </svg>\n              <span\n                style={{\n                  fontSize: 16,\n                  fontWeight: 400,\n                  color: pal.text,\n                  flex: 1,\n                }}\n              >\n                {milestone.name}\n              </span>\n              <span\n                style={{\n                  fontSize: 16,\n                  color: pal.muted,\n                  fontVariantNumeric: \"tabular-nums\",\n                }}\n              >\n                {milestone.date}\n              </span>\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: todayX,\n                top: HEAD_H + TOOL_H + 28,\n                bottom: 0,\n                width: 1,\n                background: withAlpha(accent, 0.7),\n                opacity: tween(frame, T.sweep - 2, T.sweep + 3),\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  top: -1,\n                  transform: \"translateX(-50%)\",\n                  background: accent,\n                  color: \"#fff\",\n                  fontSize: 16,\n                  fontWeight: 400,\n                  lineHeight: \"20px\",\n                  padding: \"0 7px\",\n                  borderRadius: 5,\n                  whiteSpace: \"nowrap\",\n                  fontVariantNumeric: \"tabular-nums\",\n                }}\n              >\n                {lbl(todayDay)}\n              </div>\n            </div>\n\n            <svg\n              width={PANEL.w}\n              height={PANEL.h}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                overflow: \"visible\",\n              }}\n            >\n              <defs>\n                <clipPath id={`${uid}-grid`}>\n                  <rect\n                    x={LIST_W}\n                    y={ROWS_Y}\n                    width={GRID_W}\n                    height={PANEL.h - ROWS_Y}\n                  />\n                </clipPath>\n              </defs>\n              <g clipPath={`url(#${uid}-grid)`}>\n                {links.map((l, k) => {\n                  const draw = tween(\n                    frame,\n                    T.links + k * 5,\n                    T.links + k * 5 + 12,\n                    inOut,\n                  );\n                  const x2 = barX(l.to);\n                  const y2 = barMidY(l.to);\n                  const x1 = Math.min(\n                    barX(l.from) + barW(l.from) - 12,\n                    x2 - 18,\n                  );\n                  const y1 = barMidY(l.from) + BAR_H / 2;\n                  const col = pal.dark\n                    ? \"rgba(255,255,255,0.4)\"\n                    : \"rgba(17,19,24,0.38)\";\n                  return (\n                    <g key={`${l.from}-${l.to}`} opacity={draw > 0 ? 1 : 0}>\n                      <circle cx={x1} cy={y1} r={2.2} fill={col} />\n                      <path\n                        d={connectorPath(x1, y1, x2, y2)}\n                        fill=\"none\"\n                        stroke={col}\n                        strokeWidth={1.5}\n                        strokeLinecap=\"round\"\n                        pathLength={1}\n                        strokeDasharray=\"1 1\"\n                        strokeDashoffset={1 - draw}\n                      />\n                      <path\n                        d={`M${x2 - 8} ${y2 - 3.5} L${x2 - 4} ${y2} L${x2 - 8} ${y2 + 3.5}`}\n                        fill=\"none\"\n                        stroke={col}\n                        strokeWidth={1.5}\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                        opacity={tween(\n                          frame,\n                          T.links + k * 5 + 9,\n                          T.links + k * 5 + 12,\n                        )}\n                      />\n                    </g>\n                  );\n                })}\n              </g>\n            </svg>\n\n            {parsed.map((p, i) => {\n              const grow = tween(frame, T.bars + i * 3, T.bars + i * 3 + 14);\n              const isDrag = i === dragIdx;\n              const lift = isDrag ? lifted : 0;\n              const hov = isDrag\n                ? hover * (1 - tween(frame, T.dragEnd + 8, T.dragEnd + 14))\n                : 0;\n              const hl = moved.includes(i) ? flash(T.dragEnd + 5) : 0;\n              const x = barX(i);\n              const w = barW(i);\n              const prog = clamp(p.progress ?? 0, 0, 1);\n              return (\n                <div\n                  key={p.name}\n                  style={{\n                    position: \"absolute\",\n                    left: x,\n                    top: barMidY(i) - BAR_H / 2,\n                    width: Math.max(12, w * grow),\n                    height: BAR_H,\n                    borderRadius: 8,\n                    overflow: \"hidden\",\n                    opacity: clamp(grow * 4, 0, 1),\n                    transform: `translateY(${-1.5 * lift}px)`,\n                    zIndex: isDrag ? 3 : 2,\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: 0,\n                      top: 0,\n                      width: w,\n                      height: BAR_H,\n                      borderRadius: 8,\n                      background: pal.bar,\n                      boxShadow: [\n                        `inset 0 0 0 1px ${Math.max(hov, hl) > 0.01 ? withAlpha(accent, 0.25 + 0.5 * Math.max(hov, hl)) : pal.ring}`,\n                        lift > 0\n                          ? `0 ${6 * lift}px ${14 * lift}px -4px ${pal.dark ? \"rgba(0,0,0,0.6)\" : \"rgba(17,19,24,0.18)\"}`\n                          : \"0 0 0 0 transparent\",\n                      ].join(\", \"),\n                      overflow: \"hidden\",\n                    }}\n                  >\n                    <div\n                      style={{\n                        position: \"absolute\",\n                        left: 0,\n                        top: 0,\n                        bottom: 0,\n                        width: `${prog * 100}%`,\n                        background: pal.barFill,\n                      }}\n                    />\n                  </div>\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: 0,\n                      top: 0,\n                      width: w,\n                      height: BAR_H,\n                      boxSizing: \"border-box\",\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      gap: 8,\n                      padding: \"0 6px 0 10px\",\n                      fontSize: 16,\n                      fontWeight: 400,\n                      color: pal.barText,\n                      whiteSpace: \"nowrap\",\n                    }}\n                  >\n                    <div\n                      style={{\n                        width: 10,\n                        height: 10,\n                        borderRadius: 3,\n                        background: p.color,\n                        flexShrink: 0,\n                      }}\n                    />\n                    <span\n                      style={{\n                        overflow: \"hidden\",\n                        textOverflow: \"ellipsis\",\n                        flex: 1,\n                      }}\n                    >\n                      {p.name}\n                    </span>\n                    <Avatar name={p.lead} size={18} />\n                  </div>\n                  {[0, 1].map((side) => (\n                    <div\n                      key={side}\n                      style={{\n                        position: \"absolute\",\n                        top: 7,\n                        left: side ? w - 6 : 3,\n                        width: 3,\n                        height: BAR_H - 14,\n                        borderRadius: 2,\n                        background: withAlpha(accent, 0.9),\n                        opacity: hov,\n                      }}\n                    />\n                  ))}\n                </div>\n              );\n            })}\n\n            <div\n              style={{\n                position: \"absolute\",\n                left: LIST_W + (msIdx + 0.5) * DAY - 8,\n                top: barMidY(msRow) - 8,\n                width: 16,\n                height: 16,\n                opacity: clamp(ms * 3, 0, 1),\n                transform: `translateY(${(1 - Math.min(ms, 1)) * -12}px) scale(${0.7 + 0.3 * ms})`,\n              }}\n            >\n              <svg width={16} height={16} viewBox=\"0 0 16 16\">\n                <rect\n                  x={3.5}\n                  y={3.5}\n                  width={9}\n                  height={9}\n                  rx={1.6}\n                  transform=\"rotate(45 8 8)\"\n                  fill={pal.dark ? \"#EDEDEF\" : \"#1C1D21\"}\n                />\n              </svg>\n              <div\n                style={{\n                  position: \"absolute\",\n                  ...(msLabelLeft ? { right: 22 } : { left: 22 }),\n                  top: -2,\n                  fontSize: 16,\n                  fontWeight: 400,\n                  color: pal.text,\n                  whiteSpace: \"nowrap\",\n                  opacity: tween(frame, T.milestone + 4, T.milestone + 10),\n                }}\n              >\n                {milestone.name}\n              </div>\n            </div>\n\n            <div\n              style={{\n                position: \"absolute\",\n                left: barX(dragIdx) + barW(dragIdx) / 2,\n                top: barMidY(dragIdx) - BAR_H / 2 - 34,\n                transform: `translateX(-50%) translateY(${(1 - tipIn) * 4}px) scale(${0.96 + 0.04 * tipIn})`,\n                transformOrigin: \"50% 100%\",\n                opacity: tip,\n                background: pal.dark ? \"#2A2B2F\" : \"#1C1D21\",\n                color: \"#F4F4F5\",\n                fontSize: 16,\n                fontWeight: 400,\n                lineHeight: \"28px\",\n                padding: \"0 10px\",\n                borderRadius: 7,\n                whiteSpace: \"nowrap\",\n                fontVariantNumeric: \"tabular-nums\",\n                boxShadow: \"0 4px 12px rgba(0,0,0,0.18)\",\n                zIndex: 4,\n              }}\n            >\n              {dates(dragIdx)}\n              <span style={{ color: \"#9C9DA4\" }}>\n                {\" \"}\n                · {parsed[dragIdx].e - parsed[dragIdx].s + 1}d\n              </span>\n            </div>\n\n            <div\n              style={{\n                position: \"absolute\",\n                left: PANEL.w / 2,\n                bottom: 22,\n                transform: `translateX(-50%) translateY(${(1 - toast) * 14}px)`,\n                opacity: clamp(toast * 1.5, 0, 1),\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 10,\n                height: 42,\n                padding: \"0 8px 0 12px\",\n                borderRadius: 10,\n                background: pal.toast,\n                boxShadow: pal.dark\n                  ? \"0 0 0 1px rgba(255,255,255,0.09), 0 12px 30px rgba(0,0,0,0.5)\"\n                  : \"0 0 0 1px rgba(17,19,24,0.08), 0 12px 30px rgba(17,19,24,0.14), 0 2px 6px rgba(17,19,24,0.05)\",\n                fontSize: 16,\n                color: pal.text,\n                whiteSpace: \"nowrap\",\n                zIndex: 5,\n              }}\n            >\n              <div\n                style={{\n                  width: 18,\n                  height: 18,\n                  borderRadius: 99,\n                  background: accent,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                }}\n              >\n                <Icon name=\"check\" color=\"#fff\" size={14} width={1.8} />\n              </div>\n              <span style={{ fontWeight: 400 }}>\n                Rescheduled {1 + moved.length} projects\n              </span>\n              <span\n                style={{\n                  color: pal.muted,\n                  fontVariantNumeric: \"tabular-nums\",\n                }}\n              >\n                {moved.length\n                  ? `${parsed[moved[0]].name} now starts ${lbl(parsed[moved[0]].s + finalShifts[moved[0]])}`\n                  : `${parsed[dragIdx].name} moved`}\n              </span>\n              <div\n                style={{\n                  height: 26,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  gap: 5,\n                  padding: \"0 8px\",\n                  borderRadius: 6,\n                  boxShadow: `inset 0 0 0 1px ${pal.ring}`,\n                  fontSize: 16,\n                  fontWeight: 400,\n                  color: pal.text,\n                  marginLeft: 4,\n                }}\n              >\n                <Icon name=\"undo\" color={pal.muted} size={13} />\n                Undo\n              </div>\n            </div>\n          </div>\n          <div style={{ opacity: cursorIn }}>\n            <Cursor x={cur.x} y={cur.y} press={press} />\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/roadmap-timeline.tsx"
    }
  ],
  "type": "registry:component"
}
