{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "node-trail",
  "title": "Node Trail",
  "description": "A prompt node trails dotted lines into a workflow graph: script, scenes, render, publish.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/node-trail/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeist } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\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 geist = loadGeist(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst mono = loadMono(\"normal\", { weights: [\"400\"], subsets: [\"latin\"] });\n\nexport type NodeIcon =\n  | \"doc\"\n  | \"captions\"\n  | \"frame\"\n  | \"image\"\n  | \"play\"\n  | \"spark\"\n  | \"wave\"\n  | \"music\"\n  | \"palette\"\n  | \"wide\"\n  | \"tall\"\n  | \"square\";\n\nexport type NodeTint =\n  | \"blue\"\n  | \"violet\"\n  | \"cyan\"\n  | \"green\"\n  | \"pink\"\n  | \"amber\"\n  | \"coral\";\n\nexport type NodeTrailStep = {\n  label: string;\n  icons: NodeIcon[];\n  tint?: NodeTint;\n  cols?: number;\n};\n\nexport type NodeTrailProps = {\n  theme?: \"dark\" | \"light\";\n  accent?: string;\n  prompt?: string;\n  placeholder?: string;\n  promptLabel?: string;\n  steps?: NodeTrailStep[];\n  headline?: string;\n  boardName?: string;\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n};\n\nconst DEFAULT_STEPS: NodeTrailStep[] = [\n  { label: \"Script\", icons: [\"doc\", \"captions\"], tint: \"violet\", cols: 1 },\n  {\n    label: \"Scenes\",\n    icons: [\"frame\", \"image\", \"play\", \"frame\"],\n    tint: \"cyan\",\n    cols: 2,\n  },\n  { label: \"Render\", icons: [\"spark\"], tint: \"green\", cols: 1 },\n  {\n    label: \"Publish\",\n    icons: [\"wide\", \"tall\", \"square\"],\n    tint: \"pink\",\n    cols: 3,\n  },\n];\n\nconst TINTS: Record<\"dark\" | \"light\", Record<NodeTint, string>> = {\n  dark: {\n    blue: \"#7b95ff\",\n    violet: \"#b39bff\",\n    cyan: \"#62c9ff\",\n    green: \"#5fdc8f\",\n    pink: \"#f27ad3\",\n    amber: \"#ffbc62\",\n    coral: \"#ff8a6b\",\n  },\n  light: {\n    blue: \"#3b5bfd\",\n    violet: \"#7a4df0\",\n    cyan: \"#1b86d6\",\n    green: \"#18a058\",\n    pink: \"#d23a9f\",\n    amber: \"#d6801f\",\n    coral: \"#e0553a\",\n  },\n};\n\ntype Palette = {\n  ink: string;\n  muted: string;\n  dot: string;\n  boardFill: string;\n  boardStroke: string;\n  nodeBase: string;\n  field: string;\n  handle: string;\n  shadow: string;\n  accent: string;\n  onAccent: string;\n  ground: Partial<GradientParams>;\n};\n\nconst THEMES: Record<\"dark\" | \"light\", Palette> = {\n  dark: {\n    ink: \"#eef1fa\",\n    muted: \"rgba(226,232,248,0.5)\",\n    dot: \"rgba(255,255,255,0.085)\",\n    boardFill: \"rgba(255,255,255,0.025)\",\n    boardStroke: \"rgba(255,255,255,0.18)\",\n    nodeBase: \"rgba(13,17,29,0.86)\",\n    field: \"rgba(255,255,255,0.035)\",\n    handle: \"#0b0e17\",\n    shadow:\n      \"inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 6px rgba(0,0,0,0.3), 0 14px 32px rgba(0,0,0,0.35)\",\n    accent: \"#3b5bfd\",\n    onAccent: \"#ffffff\",\n    ground: {\n      kind: \"mist\",\n      colors: [\"#3557f0\", \"#1a2a8c\", \"#0b1240\", \"#04060f\"],\n      background: \"#03050c\",\n      speed: 0.5,\n      loopSeconds: 18,\n      scale: 0.75,\n      warp: 0.35,\n      blur: 0.8,\n      grain: 0.1,\n      seed: 9,\n    },\n  },\n  light: {\n    ink: \"#16181d\",\n    muted: \"rgba(22,24,29,0.5)\",\n    dot: \"rgba(30,34,52,0.14)\",\n    boardFill: \"rgba(255,255,255,0.5)\",\n    boardStroke: \"rgba(30,34,52,0.18)\",\n    nodeBase: \"rgba(255,255,255,0.92)\",\n    field: \"#ffffff\",\n    handle: \"#ffffff\",\n    shadow:\n      \"0 1px 2px rgba(30,40,80,0.06), 0 6px 18px rgba(30,40,80,0.07), 0 20px 40px rgba(30,40,80,0.05)\",\n    accent: \"#3b5bfd\",\n    onAccent: \"#ffffff\",\n    ground: {\n      kind: \"mist\",\n      colors: [\"#c9d3ff\", \"#e6e9fb\", \"#f6efe4\", \"#9fb0f5\"],\n      background: \"#f4f1ea\",\n      speed: 0.5,\n      loopSeconds: 18,\n      scale: 0.8,\n      warp: 0.35,\n      blur: 0.75,\n      grain: 0.22,\n      seed: 4,\n    },\n  },\n};\n\nconst MOVE = Easing.bezier(0.65, 0, 0.35, 1);\nconst CAM = Easing.bezier(0.87, 0, 0.13, 1);\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst EXIT = Easing.bezier(0.55, 0, 1, 0.45);\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\n\nconst TILE = 40;\nconst TILE_GAP = 8;\nconst PAD = 12;\nconst LABEL = 15;\nconst FIELD_TEXT = 15;\n\nconst clamp01 = (v: number) => Math.min(1, Math.max(0, v));\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\nfunction alpha(hex: string, a: number) {\n  const h = hex.replace(\"#\", \"\");\n  if (h.length !== 6) return hex;\n  const n = Number.parseInt(h, 16);\n  return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`;\n}\n\nexport function NodeGlyph({\n  icon,\n  size,\n  color,\n  frame = 0,\n}: {\n  icon: NodeIcon;\n  size: number;\n  color: string;\n  frame?: number;\n}) {\n  const common = {\n    fill: \"none\",\n    stroke: color,\n    strokeWidth: 1.5,\n    strokeLinecap: \"round\" as const,\n    strokeLinejoin: \"round\" as const,\n  };\n  let body: React.ReactNode = null;\n  if (icon === \"doc\") {\n    body = (\n      <>\n        <rect x={5} y={3} width={14} height={18} rx={2.5} {...common} />\n        <path d=\"M8.5 8h7M8.5 12h7M8.5 16h4\" {...common} />\n      </>\n    );\n  } else if (icon === \"captions\") {\n    body = (\n      <>\n        <rect x={3} y={5} width={18} height={14} rx={3} {...common} />\n        <path d=\"M7 11h4M13 11h4M7 15h7\" {...common} />\n      </>\n    );\n  } else if (icon === \"frame\") {\n    body = (\n      <>\n        <rect x={3} y={5} width={18} height={14} rx={2.5} {...common} />\n        <path d=\"M7.5 5v14M16.5 5v14M3 12h4.5M16.5 12H21\" {...common} />\n      </>\n    );\n  } else if (icon === \"image\") {\n    body = (\n      <>\n        <rect x={3} y={4} width={18} height={16} rx={3} {...common} />\n        <circle cx={9} cy={10} r={1.8} {...common} />\n        <path d=\"M21 16l-5-5-9 9\" {...common} />\n      </>\n    );\n  } else if (icon === \"play\") {\n    body = (\n      <>\n        <circle cx={12} cy={12} r={9} {...common} />\n        <path d=\"M10 8.6l5.2 3.4-5.2 3.4z\" {...common} />\n      </>\n    );\n  } else if (icon === \"spark\") {\n    const step = Math.floor(frame / 3) % 8;\n    body = Array.from({ length: 8 }, (_, i) => {\n      const a = (i * Math.PI) / 4;\n      const o = 0.25 + 0.75 * (((i - step + 8) % 8) / 7);\n      return (\n        <path\n          key={`r${i}`}\n          d={`M${12 + Math.cos(a) * 4} ${12 + Math.sin(a) * 4}L${12 + Math.cos(a) * 8.5} ${12 + Math.sin(a) * 8.5}`}\n          {...common}\n          strokeOpacity={o}\n        />\n      );\n    });\n  } else if (icon === \"wave\") {\n    body = <path d=\"M4 10v4M8 7v10M12 4v16M16 8v8M20 10.5v3\" {...common} />;\n  } else if (icon === \"music\") {\n    body = (\n      <>\n        <path d=\"M9 18V6l10-2v12\" {...common} />\n        <circle cx={6.5} cy={18} r={2.5} {...common} />\n        <circle cx={16.5} cy={16} r={2.5} {...common} />\n      </>\n    );\n  } else if (icon === \"palette\") {\n    body = (\n      <>\n        <path\n          d=\"M12 3a9 9 0 1 0 0 18c1.2 0 1.8-.9 1.8-1.8 0-1.3-1-1.6-1-2.7 0-1 .8-1.7 1.8-1.7H17a4 4 0 0 0 4-4C21 6.6 17 3 12 3z\"\n          {...common}\n        />\n        <circle cx={7.5} cy={11} r={1.2} {...common} />\n        <circle cx={10.5} cy={7.2} r={1.2} {...common} />\n        <circle cx={15} cy={7.6} r={1.2} {...common} />\n      </>\n    );\n  } else if (icon === \"wide\") {\n    body = <rect x={2.5} y={7} width={19} height={10.5} rx={2} {...common} />;\n  } else if (icon === \"tall\") {\n    body = <rect x={7} y={2.5} width={10} height={19} rx={2} {...common} />;\n  } else if (icon === \"square\") {\n    body = <rect x={5} y={5} width={14} height={14} rx={2} {...common} />;\n  }\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      style={{ display: \"block\", overflow: \"visible\" }}\n      aria-hidden\n    >\n      {body}\n    </svg>\n  );\n}\n\ntype Pt = { x: number; y: number };\n\ntype LaidNode = {\n  label: string;\n  tint: string;\n  kind: \"input\" | \"tiles\";\n  icons: NodeIcon[];\n  cols: number;\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  labelSide: \"top\" | \"left\" | \"right\";\n};\n\ntype Edge = { pts: Pt[]; cum: number[]; len: number; from: Pt; to: Pt };\n\nfunction tilesSize(n: number, cols: number) {\n  const c = Math.max(1, Math.min(cols, n));\n  const r = Math.ceil(n / c);\n  return {\n    w: c * TILE + (c - 1) * TILE_GAP + PAD * 2,\n    h: r * TILE + (r - 1) * TILE_GAP + PAD * 2,\n  };\n}\n\nfunction buildEdge(a: LaidNode, b: LaidNode, k: number, tall: boolean): Edge {\n  let p0: Pt;\n  let p3: Pt;\n  let c1: Pt;\n  let c2: Pt;\n  if (tall) {\n    p0 = { x: a.x, y: a.y + (a.h * k) / 2 };\n    p3 = { x: b.x, y: b.y - (b.h * k) / 2 };\n    const d = Math.max(40 * k, (p3.y - p0.y) * 0.55);\n    c1 = { x: p0.x, y: p0.y + d };\n    c2 = { x: p3.x, y: p3.y - d };\n  } else {\n    p0 = { x: a.x + (a.w * k) / 2, y: a.y };\n    p3 = { x: b.x - (b.w * k) / 2, y: b.y };\n    const d = Math.max(40 * k, (p3.x - p0.x) * 0.55);\n    c1 = { x: p0.x + d, y: p0.y };\n    c2 = { x: p3.x - d, y: p3.y };\n  }\n  const pts: Pt[] = [];\n  const cum: number[] = [];\n  const N = 48;\n  let len = 0;\n  for (let i = 0; i <= N; i++) {\n    const t = i / N;\n    const m = 1 - t;\n    const p = {\n      x:\n        m * m * m * p0.x +\n        3 * m * m * t * c1.x +\n        3 * m * t * t * c2.x +\n        t * t * t * p3.x,\n      y:\n        m * m * m * p0.y +\n        3 * m * m * t * c1.y +\n        3 * m * t * t * c2.y +\n        t * t * t * p3.y,\n    };\n    if (i > 0) {\n      const q = pts[i - 1];\n      len += Math.hypot(p.x - q.x, p.y - q.y);\n    }\n    pts.push(p);\n    cum.push(len);\n  }\n  return { pts, cum, len, from: p0, to: p3 };\n}\n\nfunction partialPath(e: Edge, frac: number) {\n  const target = e.len * clamp01(frac);\n  if (target <= 0.01) return { d: \"\", tip: e.from };\n  let d = `M${e.pts[0].x.toFixed(2)} ${e.pts[0].y.toFixed(2)}`;\n  let tip = e.pts[0];\n  for (let i = 1; i < e.pts.length; i++) {\n    if (e.cum[i] <= target) {\n      d += `L${e.pts[i].x.toFixed(2)} ${e.pts[i].y.toFixed(2)}`;\n      tip = e.pts[i];\n    } else {\n      const seg = e.cum[i] - e.cum[i - 1];\n      const t = seg > 0 ? (target - e.cum[i - 1]) / seg : 0;\n      tip = {\n        x: lerp(e.pts[i - 1].x, e.pts[i].x, t),\n        y: lerp(e.pts[i - 1].y, e.pts[i].y, t),\n      };\n      d += `L${tip.x.toFixed(2)} ${tip.y.toFixed(2)}`;\n      break;\n    }\n  }\n  return { d, tip };\n}\n\nexport function nodeTrailTimeline(stepCount: number, promptLength: number) {\n  const edges: { start: number; dur: number; arrive: number }[] = [];\n  let start = Math.max(30, promptLength + 9);\n  let dur = 22;\n  let hold = 10;\n  for (let i = 0; i < stepCount; i++) {\n    edges.push({ start, dur, arrive: start + dur - 3 });\n    start = start + dur + hold;\n    dur = Math.max(10, Math.round(dur * 0.8));\n    hold = Math.max(5, Math.round(hold * 0.8));\n  }\n  const last = edges[edges.length - 1];\n  const pull = last ? last.start + last.dur + 8 : 40;\n  const pullDur = 24;\n  return {\n    typeStart: 3,\n    press: (edges[0]?.start ?? 30) - 4,\n    edges,\n    pull,\n    pullDur,\n    board: pull + 10,\n    headline: pull + 13,\n    end: pull + pullDur + 42,\n  };\n}\n\nconst DEFAULT_PROMPT = \"A launch film for v2\";\n\nexport const NODE_TRAIL_DURATION = nodeTrailTimeline(\n  DEFAULT_STEPS.length,\n  DEFAULT_PROMPT.length,\n).end;\n\nfunction layoutNodes(\n  W: number,\n  H: number,\n  promptW: number,\n  promptLabel: string,\n  steps: NodeTrailStep[],\n  tints: Record<NodeTint, string>,\n  accent: string,\n) {\n  const tall = H > W * 1.1;\n  const u = Math.min(W, H) / 720;\n  const raw: Omit<LaidNode, \"x\" | \"y\" | \"labelSide\">[] = [\n    {\n      label: promptLabel,\n      tint: accent,\n      kind: \"input\",\n      icons: [],\n      cols: 1,\n      w: promptW,\n      h: 52,\n    },\n    ...steps.map((s, i) => {\n      const cols = s.cols ?? Math.min(2, s.icons.length);\n      const sz = tilesSize(s.icons.length, cols);\n      const order: NodeTint[] = [\"violet\", \"cyan\", \"green\", \"pink\", \"amber\"];\n      return {\n        label: s.label,\n        tint: tints[s.tint ?? order[i % order.length]],\n        kind: \"tiles\" as const,\n        icons: s.icons,\n        cols,\n        w: sz.w,\n        h: sz.h,\n      };\n    }),\n  ];\n  const nodes: LaidNode[] = [];\n  let k: number;\n  if (!tall) {\n    const gap = 62;\n    const total =\n      raw.reduce((a, n) => a + n.w, 0) + gap * Math.max(0, raw.length - 1);\n    k = Math.min(1.32 * u, (0.82 * W) / total);\n    const yo = [-34, 22, -10, 28, -4, 18, -18];\n    let x = W / 2 - (total * k) / 2;\n    const rowY = 0;\n    raw.forEach((n, i) => {\n      nodes.push({\n        ...n,\n        x: x + (n.w * k) / 2,\n        y: rowY + yo[i % yo.length] * k,\n        labelSide: \"top\",\n      });\n      x += (n.w + gap) * k;\n    });\n  } else {\n    const gap = 40;\n    const total =\n      raw.reduce((a, n) => a + n.h, 0) + gap * Math.max(0, raw.length - 1);\n    k = Math.min(1.6 * u, (0.64 * H) / total, (0.8 * W) / raw[0].w);\n    let y = -(total * k) / 2;\n    raw.forEach((n, i) => {\n      const s = i === 0 ? 0 : i % 2 === 1 ? -1 : 1;\n      const labelW = (n.label.length * 0.6 * 17 + 16) * k;\n      const room = 0.08 * W + labelW + (n.w * k) / 2;\n      const x =\n        s === 0\n          ? W / 2\n          : s < 0\n            ? Math.max(W / 2 - 0.2 * W, room)\n            : Math.min(W / 2 + 0.2 * W, W - room);\n      nodes.push({\n        ...n,\n        x,\n        y: y + (n.h * k) / 2,\n        labelSide: i === 0 ? \"top\" : s < 0 ? \"left\" : \"right\",\n      });\n      y += (n.h + gap) * k;\n    });\n  }\n  return { nodes, k, tall, u };\n}\n\nfunction Handles({\n  w,\n  h,\n  k,\n  color,\n  fill,\n  t,\n}: {\n  w: number;\n  h: number;\n  k: number;\n  color: string;\n  fill: string;\n  t: number;\n}) {\n  if (t <= 0.001) return null;\n  const s = 7 * k;\n  const corners = [\n    [0, 0],\n    [w, 0],\n    [0, h],\n    [w, h],\n  ];\n  return (\n    <>\n      {corners.map(([cx, cy], i) => (\n        <div\n          key={`h${cx}-${cy}`}\n          style={{\n            position: \"absolute\",\n            left: cx - s / 2,\n            top: cy - s / 2,\n            width: s,\n            height: s,\n            background: fill,\n            border: `${1.25 * k}px solid ${color}`,\n            borderRadius: 1.5 * k,\n            transform: `scale(${Math.max(0, t - i * 0.04)})`,\n            boxSizing: \"border-box\",\n          }}\n        />\n      ))}\n    </>\n  );\n}\n\nfunction TypedLabel({\n  text,\n  shown,\n  color,\n  size,\n  typing,\n}: {\n  text: string;\n  shown: number;\n  color: string;\n  size: number;\n  typing: boolean;\n}) {\n  const n = Math.max(0, Math.min(text.length, Math.floor(shown)));\n  return (\n    <span\n      style={{\n        fontFamily: mono.fontFamily,\n        fontWeight: 400,\n        fontSize: size,\n        color,\n        whiteSpace: \"pre\",\n        lineHeight: 1,\n        display: \"inline-flex\",\n        alignItems: \"center\",\n      }}\n    >\n      {text.slice(0, n)}\n      {typing ? (\n        <span\n          style={{\n            display: \"inline-block\",\n            width: size * 0.09,\n            height: size * 1.05,\n            marginLeft: size * 0.06,\n            background: color,\n          }}\n        />\n      ) : null}\n    </span>\n  );\n}\n\nexport function NodeTrail({\n  theme = \"dark\",\n  accent,\n  prompt = DEFAULT_PROMPT,\n  placeholder = \"Describe your launch\",\n  promptLabel = \"Prompt\",\n  steps = DEFAULT_STEPS,\n  headline = \"From one prompt to every screen.\",\n  boardName = \"launch-film.flow\",\n  ground,\n  groundParams,\n}: NodeTrailProps) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, fps } = useVideoConfig();\n  const uid = useId().replace(/:/g, \"\");\n  const c = THEMES[theme];\n  const tints = TINTS[theme];\n  const acc = accent ?? (theme === \"dark\" ? tints.blue : c.accent);\n  const btn = accent ?? c.accent;\n  const T = nodeTrailTimeline(steps.length, prompt.length);\n\n  const chars = Math.max(prompt.length, placeholder.length);\n  const promptW = 14 + chars * 0.6 * FIELD_TEXT + 12 + 32 + 10;\n  const laid = layoutNodes(W, H, promptW, promptLabel, steps, tints, acc);\n  const { k, tall, u } = laid;\n  const headSize = tall ? 0.076 * W : 0.068 * H;\n  const labelU = tall ? 17 : LABEL;\n  const headLines = tall ? 2 : 1;\n  const top0 = Math.min(\n    ...laid.nodes.map(\n      (n) =>\n        n.y - (n.h * k) / 2 - (n.labelSide === \"top\" ? (labelU + 12) * k : 0),\n    ),\n  );\n  const bot0 = Math.max(...laid.nodes.map((n) => n.y + (n.h * k) / 2));\n  const boardPad = (tall ? 56 : 52) * u;\n  const boardH = Math.max(\n    bot0 - top0 + boardPad * 2,\n    tall ? 0.62 * H : 0.5 * H,\n  );\n  const headBlock = headSize * 1.08 * headLines + 62 * u;\n  const groupTop = (H - (headBlock + boardH)) / 2 + (tall ? 0 : 0.01 * H);\n  const margin = 0.07 * W;\n  const board = {\n    x: margin,\n    y: groupTop + headBlock,\n    w: W - margin * 2,\n    h: boardH,\n  };\n  const dy = board.y + board.h / 2 - (top0 + bot0) / 2;\n  const nodes = laid.nodes.map((n) => ({ ...n, y: n.y + dy }));\n  const headTop = groupTop;\n  const edges = nodes\n    .slice(0, -1)\n    .map((n, i) => buildEdge(n, nodes[i + 1], k, tall));\n\n  const Z = tall ? 1.6 : 2.0;\n  const cam = (f: number) => {\n    const first = T.edges[0];\n    const n0 = nodes[0];\n    if (!first || f < first.start + 2) {\n      const lim = first ? first.start + 2 : 30;\n      return { x: n0.x, y: n0.y, z: Z * (1 + 0.025 * clamp01(f / lim)) };\n    }\n    if (f >= T.pull) {\n      const ln = nodes[nodes.length - 1];\n      const t = clamp01((f - T.pull) / T.pullDur);\n      const e = CAM(t);\n      const zEnd = 1 + 0.025 * ENTER(clamp01((f - T.pull - T.pullDur) / 48));\n      return {\n        x: lerp(ln.x, W / 2, e),\n        y: lerp(ln.y, H / 2, e),\n        z: Math.exp(lerp(Math.log(Z), Math.log(zEnd), e)),\n      };\n    }\n    for (let i = T.edges.length - 1; i >= 0; i--) {\n      const ed = T.edges[i];\n      const s0 = ed.start + 2;\n      if (f >= s0) {\n        const t = clamp01((f - s0) / ed.dur);\n        const e = MOVE(t);\n        const a = nodes[i];\n        const b = nodes[i + 1];\n        const next = T.edges[i + 1];\n        const settle = next ? next.start + 2 : T.pull;\n        const push =\n          t >= 1\n            ? 0.02 * clamp01((f - s0 - ed.dur) / (settle - s0 - ed.dur))\n            : 0;\n        return {\n          x: lerp(a.x, b.x, e),\n          y: lerp(a.y, b.y, e),\n          z: Z * (1 - 0.1 * Math.sin(Math.PI * e)) * (1 + push),\n        };\n      }\n    }\n    return { x: n0.x, y: n0.y, z: Z };\n  };\n\n  const cNow = cam(frame);\n  const cPrev = cam(Math.max(0, frame - 1));\n  const vx = (cNow.x - cPrev.x) * cNow.z;\n  const vy = (cNow.y - cPrev.y) * cNow.z;\n  const bx = Math.min(10, Math.abs(vx) * 0.07);\n  const by = Math.min(10, Math.abs(vy) * 0.07);\n  const blurOn = bx > 0.35 || by > 0.35;\n\n  const nodeArrive = (i: number) => (i === 0 ? -100 : T.edges[i - 1].arrive);\n  const selectedUntil = (i: number) => {\n    if (i === nodes.length - 1) return T.pull;\n    return T.edges[i].start + 4;\n  };\n  const dash = 5 * k;\n  const march = -frame * 0.35 * k;\n\n  const worldExt = { x: -W, y: -H, w: W * 3, h: H * 3 };\n\n  const boardT = clamp01((frame - T.board) / 12);\n  const boardE = ENTER(boardT);\n  const headT = frame - T.headline;\n  const headE = ENTER(clamp01(headT / 14));\n\n  const typed = Math.floor(\n    clamp01((frame - T.typeStart) / prompt.length) * prompt.length,\n  );\n  const pressS =\n    frame >= T.press\n      ? 1 -\n        0.12 * Math.sin(Math.PI * clamp01((frame - T.press) / 6)) +\n        0.05 *\n          (spring({ frame: frame - T.press - 5, fps, config: CHIP }) -\n            spring({ frame: frame - T.press - 9, fps, config: CHIP }))\n      : 1;\n\n  return (\n    <AbsoluteFill style={{ background: c.ground.background }}>\n      <GshGround\n        preset={ground}\n        params={\n          ground && !groundParams ? undefined : (groundParams ?? c.ground)\n        }\n        timeOffset={4}\n        timeScale={0.6}\n      />\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <filter id={`${uid}-mb`} x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n          <feGaussianBlur\n            stdDeviation={`${(bx / cNow.z).toFixed(2)} ${(by / cNow.z).toFixed(2)}`}\n          />\n        </filter>\n      </svg>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: W,\n          height: H,\n          transformOrigin: \"0 0\",\n          transform: `translate(${W / 2}px, ${H / 2}px) scale(${cNow.z}) translate(${-cNow.x}px, ${-cNow.y}px)`,\n          filter: blurOn ? `url(#${uid}-mb)` : undefined,\n        }}\n      >\n        <svg\n          width={W}\n          height={H}\n          style={{ position: \"absolute\", left: 0, top: 0, overflow: \"visible\" }}\n          aria-hidden\n        >\n          <defs>\n            <pattern\n              id={`${uid}-dots`}\n              width={24 * k}\n              height={24 * k}\n              patternUnits=\"userSpaceOnUse\"\n            >\n              <circle cx={12 * k} cy={12 * k} r={1.1 * k} fill={c.dot} />\n            </pattern>\n            {edges.map((e, i) => (\n              <linearGradient\n                key={`g${e.from.x}-${e.from.y}`}\n                id={`${uid}-eg${i}`}\n                gradientUnits=\"userSpaceOnUse\"\n                x1={e.from.x}\n                y1={e.from.y}\n                x2={e.to.x}\n                y2={e.to.y}\n              >\n                <stop offset=\"0\" stopColor={nodes[i].tint} />\n                <stop offset=\"1\" stopColor={nodes[i + 1].tint} />\n              </linearGradient>\n            ))}\n          </defs>\n          <rect\n            x={worldExt.x}\n            y={worldExt.y}\n            width={worldExt.w}\n            height={worldExt.h}\n            fill={`url(#${uid}-dots)`}\n          />\n          {boardE > 0 ? (\n            <rect\n              x={board.x}\n              y={board.y}\n              width={board.w}\n              height={board.h}\n              fill={c.boardFill}\n              stroke={c.boardStroke}\n              strokeWidth={1.25 * u}\n              opacity={boardE}\n              strokeDasharray={`${(board.w + board.h) * 2 * boardE} ${(board.w + board.h) * 2}`}\n            />\n          ) : null}\n          {edges.map((e, i) => {\n            const ed = T.edges[i];\n            const p = MOVE(clamp01((frame - ed.start) / ed.dur));\n            const { d, tip } = partialPath(e, p);\n            if (!d) return null;\n            const drawing = p > 0 && p < 1;\n            return (\n              <g key={`e${e.from.x}-${e.from.y}`}>\n                <path\n                  d={d}\n                  fill=\"none\"\n                  stroke={`url(#${uid}-eg${i})`}\n                  strokeWidth={1.6 * k}\n                  strokeDasharray={`${dash} ${dash}`}\n                  strokeDashoffset={march}\n                  strokeLinecap=\"round\"\n                />\n                {drawing ? (\n                  <circle\n                    cx={tip.x}\n                    cy={tip.y}\n                    r={3.6 * k}\n                    fill={nodes[i + 1].tint}\n                  />\n                ) : null}\n              </g>\n            );\n          })}\n        </svg>\n\n        {nodes.map((n, i) => {\n          const arrive = nodeArrive(i);\n          const local = frame - arrive;\n          if (i > 0 && local < 0) return null;\n          const pop = i === 0 ? 1 : spring({ frame: local, fps, config: CARD });\n          const appear = i === 0 ? 1 : clamp01(local / 4);\n          const scale = i === 0 ? 1 : lerp(1.14, 1, pop);\n          const selOn =\n            i === 0 ? 1 : spring({ frame: local, fps, config: CHIP });\n          const selOff = 1 - EXIT(clamp01((frame - selectedUntil(i)) / 6));\n          const sel = Math.max(0, Math.min(selOn, selOff));\n          const pw = n.w * k;\n          const ph = n.h * k;\n          const labelShown = i === 0 ? n.label.length : (local - 2) * 1;\n          const typing =\n            i > 0 && labelShown >= 0 && labelShown < n.label.length + 3;\n          const labelSize = labelU * k;\n          const outPort = i < nodes.length - 1 && frame >= T.edges[i].start;\n          const inPort = i > 0;\n          const portR = 3.4 * k;\n          const portStyle = (x: number, y: number): React.CSSProperties => ({\n            position: \"absolute\",\n            left: x - portR,\n            top: y - portR,\n            width: portR * 2,\n            height: portR * 2,\n            borderRadius: portR,\n            background: c.handle,\n            border: `${1.4 * k}px solid ${n.tint}`,\n            boxSizing: \"border-box\",\n          });\n          const labelBox: React.CSSProperties =\n            n.labelSide === \"top\"\n              ? {\n                  left: 0,\n                  width: pw,\n                  bottom: ph + 9 * k,\n                  justifyContent: \"center\",\n                }\n              : n.labelSide === \"left\"\n                ? {\n                    right: pw + 14 * k,\n                    top: ph / 2 - labelSize / 2,\n                    justifyContent: \"flex-end\",\n                  }\n                : {\n                    left: pw + 14 * k,\n                    top: ph / 2 - labelSize / 2,\n                    justifyContent: \"flex-start\",\n                  };\n          return (\n            <div\n              key={`n${n.label}-${i}`}\n              style={{\n                position: \"absolute\",\n                left: n.x - pw / 2,\n                top: n.y - ph / 2,\n                width: pw,\n                height: ph,\n                opacity: appear,\n                transform: `scale(${scale})`,\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  display: \"flex\",\n                  whiteSpace: \"nowrap\",\n                  ...labelBox,\n                }}\n              >\n                <TypedLabel\n                  text={n.label}\n                  shown={labelShown}\n                  color={n.tint}\n                  size={labelSize}\n                  typing={typing && Math.floor(frame / 4) % 2 === 0}\n                />\n              </div>\n              {n.kind === \"input\" ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    borderRadius: 10 * k,\n                    background: c.field,\n                    border: `${1.5 * k}px solid ${n.tint}`,\n                    boxShadow: c.shadow,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    paddingLeft: 14 * k,\n                    boxSizing: \"border-box\",\n                  }}\n                >\n                  <span\n                    style={{\n                      fontFamily: mono.fontFamily,\n                      fontWeight: 400,\n                      fontSize: FIELD_TEXT * k,\n                      color: typed > 0 ? c.ink : c.muted,\n                      whiteSpace: \"pre\",\n                      display: \"inline-flex\",\n                      alignItems: \"center\",\n                    }}\n                  >\n                    {typed > 0 ? prompt.slice(0, typed) : placeholder}\n                    {frame < T.press + 2 && Math.floor(frame / 5) % 2 === 0 ? (\n                      <span\n                        style={{\n                          display: \"inline-block\",\n                          width: 1.5 * k,\n                          height: FIELD_TEXT * 1.15 * k,\n                          marginLeft: 2 * k,\n                          background: n.tint,\n                        }}\n                      />\n                    ) : null}\n                  </span>\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      right: 9 * k,\n                      top: ph / 2 - 16 * k,\n                      width: 32 * k,\n                      height: 32 * k,\n                      borderRadius: 16 * k,\n                      background: btn,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      justifyContent: \"center\",\n                      transform: `scale(${pressS})`,\n                      boxShadow: `0 ${2 * k}px ${8 * k}px ${alpha(btn, 0.35)}`,\n                    }}\n                  >\n                    <svg\n                      viewBox=\"0 0 24 24\"\n                      width={17 * k}\n                      height={17 * k}\n                      aria-hidden\n                    >\n                      <path\n                        d=\"M5 12h14M13 6l6 6-6 6\"\n                        fill=\"none\"\n                        stroke={c.onAccent}\n                        strokeWidth={2}\n                        strokeLinecap=\"round\"\n                        strokeLinejoin=\"round\"\n                      />\n                    </svg>\n                  </div>\n                </div>\n              ) : (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    borderRadius: 6 * k,\n                    background: `linear-gradient(${alpha(n.tint, theme === \"dark\" ? 0.12 : 0.09)}, ${alpha(n.tint, theme === \"dark\" ? 0.12 : 0.09)}), ${c.nodeBase}`,\n                    border: `${1.5 * k}px solid ${n.tint}`,\n                    boxShadow: c.shadow,\n                    boxSizing: \"border-box\",\n                    padding: (PAD - 1.5) * k,\n                    display: \"grid\",\n                    gridTemplateColumns: `repeat(${n.cols}, ${TILE * k}px)`,\n                    gridAutoRows: TILE * k,\n                    gap: TILE_GAP * k,\n                  }}\n                >\n                  {n.icons.map((ic, j) => {\n                    const tp = spring({\n                      frame: local - 4 - j * 2,\n                      fps,\n                      config: CHIP,\n                    });\n                    return (\n                      <div\n                        key={`t${ic}-${j}`}\n                        style={{\n                          width: TILE * k,\n                          height: TILE * k,\n                          borderRadius: 7 * k,\n                          border: `${1.2 * k}px dashed ${alpha(n.tint, 0.6)}`,\n                          background: alpha(\n                            n.tint,\n                            theme === \"dark\" ? 0.08 : 0.07,\n                          ),\n                          display: \"flex\",\n                          alignItems: \"center\",\n                          justifyContent: \"center\",\n                          boxSizing: \"border-box\",\n                          transform: `scale(${Math.max(0, tp)})`,\n                        }}\n                      >\n                        <NodeGlyph\n                          icon={ic}\n                          size={21 * k}\n                          color={n.tint}\n                          frame={frame}\n                        />\n                      </div>\n                    );\n                  })}\n                </div>\n              )}\n              {inPort ? (\n                <div\n                  style={tall ? portStyle(pw / 2, 0) : portStyle(0, ph / 2)}\n                />\n              ) : null}\n              {outPort ? (\n                <div\n                  style={tall ? portStyle(pw / 2, ph) : portStyle(pw, ph / 2)}\n                />\n              ) : null}\n              <Handles\n                w={pw}\n                h={ph}\n                k={k}\n                color={n.tint}\n                fill={c.handle}\n                t={sel}\n              />\n            </div>\n          );\n        })}\n\n        {boardE > 0 ? (\n          <>\n            <div\n              style={{\n                position: \"absolute\",\n                left: board.x,\n                top: board.y,\n                width: board.w,\n                height: board.h,\n              }}\n            >\n              <Handles\n                w={board.w}\n                h={board.h}\n                k={u * 1.1}\n                color={c.boardStroke.replace(/[\\d.]+\\)$/, \"0.55)\")}\n                fill={c.handle}\n                t={spring({ frame: frame - T.board - 4, fps, config: CHIP })}\n              />\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: board.x,\n                top: board.y - (tall ? 34 : 30) * u,\n                width: board.w,\n                display: \"flex\",\n                justifyContent: \"space-between\",\n                fontFamily: mono.fontFamily,\n                fontWeight: 400,\n                fontSize: (tall ? 18 : 16) * u,\n                lineHeight: 1,\n                color: c.muted,\n                opacity: boardE,\n                transform: `translateY(${(1 - boardE) * 8 * u}px)`,\n              }}\n            >\n              <span>{boardName}</span>\n              <span>{`${nodes.length} nodes`}</span>\n            </div>\n          </>\n        ) : null}\n\n        {headT > -1 ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: board.x,\n              top: headTop,\n              width: board.w,\n              overflow: \"hidden\",\n              paddingBottom: headSize * 0.18,\n            }}\n          >\n            <div\n              style={{\n                fontFamily: geist.fontFamily,\n                fontWeight: 300,\n                fontSize: headSize,\n                letterSpacing: \"-0.035em\",\n                lineHeight: 1.08,\n                textWrap: \"balance\",\n                color: c.ink,\n                transform: `translateY(${(1 - headE) * headSize * 1.2}px)`,\n                filter: `blur(${(1 - headE) * 5}px)`,\n                opacity: 0.2 + 0.8 * headE,\n              }}\n            >\n              {headline}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/node-trail.tsx"
    }
  ],
  "type": "registry:component"
}
