{
  "name": "stacked-tower-default",
  "type": "registry:block",
  "tier": "free",
  "dependencies": [],
  "registryDependencies": [
    "https://www.craftui.space/r/craftui-base.json"
  ],
  "files": [
    {
      "path": "components/craftui/designs/stacked-tower/default.tsx",
      "content": "/**\n * StackedTower — four cylindrical units stacked into a deliberately unsteady\n * tower, drawn as one static SVG.\n *\n * One invariant carries the whole 3D read: every ellipse obeys\n * ry = camera.ryRatio * rx. No perspective transform, no rotate, no skew —\n * height is only ever vertical offset between two identical ellipses. Break the\n * ratio on a single shape and it reads as tilted rather than as higher up.\n *\n * Server component: no hooks, no context, no client dependencies. The camera is\n * threaded as a `cam` prop rather than through context so it stays that way.\n */\n\n/* ------------------------------------------------------------------ canvas */\n\nconst VIEW_W = 1230;\nconst VIEW_H = 738;\n\n/** Card interior — the tower is clipped to this, so the base drum runs off. */\nconst FRAME = 28;\nconst CARD = {\n  x: FRAME,\n  y: FRAME,\n  w: VIEW_W - FRAME * 2,\n  h: VIEW_H - FRAME * 2,\n};\n\n/** Undefined in projects that don't set the token, hence the stack after it. */\nconst FONT = \"var(--font-sans, ui-sans-serif, system-ui, sans-serif)\";\n\n/* ----------------------------------------------------------------- palette */\n\n/** Chrome only. Every unit colour is derived — see `shade`. */\nconst C = {\n  canvas: \"oklch(98.0% 0.009 84.6)\",\n  frame: \"oklch(86.4% 0.050 322.9)\",\n  ink: \"oklch(20.4% 0.012 293.0)\",\n} as const;\n\n/* ------------------------------------------------------------------ colour */\n\n/** A base colour in OKLCH. `l` is a percentage, `h` degrees. */\nexport type Lch = { l: number; c: number; h: number };\n\n/**\n * How a colour moves per shading step as it darkens. Chroma goes UP and the hue\n * rotates slightly, because lightness-only ramps go muddy in the shadows.\n */\nexport type Ramp = {\n  /** Lightness removed per step, in percentage points. */\n  lightness: number;\n  /** Chroma added per step. Positive — that is the point. */\n  chroma: number;\n  /** Hue rotation per step, in degrees. Keep it small. */\n  hue: number;\n};\n\nexport type BlendMode = NonNullable<React.CSSProperties[\"mixBlendMode\"]>;\n\n/** Shading behaviour shared by every unit in the stack. */\nexport type Palette = {\n  ramp: Ramp;\n  /** Opacity of the left/right wall falloff. */\n  edgeShade: number;\n  /** Opacity of the contact occlusion under each resting unit. */\n  contactShade: number;\n  /** Negative walks *up* the ramp, turning ribs from grooves into highlights. */\n  ribStep: number;\n  ribOpacity: number;\n  /** Blended so ribs pick up the wall's colour; `plus-lighter` blows out fast. */\n  ribBlend: BlendMode;\n};\n\n/** Where each surface sits on the ramp. Fractional steps are fine. */\nconst STEP = {\n  lidFace: 0,\n  lidWall: 1,\n  drumWall: 1.8,\n  rib: 3.4,\n  contact: 3,\n} as const;\n\nconst r1 = (n: number) => Math.round(n * 10) / 10;\nconst r3 = (n: number) => Math.round(n * 1000) / 1000;\n\n/** Walk a base colour `steps` down the ramp. */\nfunction shade(base: Lch, ramp: Ramp, steps: number): string {\n  const l = Math.max(0, Math.min(100, base.l - ramp.lightness * steps));\n  const c = Math.max(0, base.c + ramp.chroma * steps);\n  const h = (((base.h + ramp.hue * steps) % 360) + 360) % 360;\n  return `oklch(${r1(l)}% ${r3(c)} ${r1(h)})`;\n}\n\n/* ------------------------------------------------------------------- types */\n\nexport type Camera = {\n  /** ry / rx for every ellipse in the drawing. The whole illusion. */\n  ryRatio: number;\n  strokeWidth: number;\n};\n\n/**\n * A `lid` (thin, wide, smooth) sitting on a `drum` (taller, ribbed). The unit\n * above is narrower than the lid below it, so the lid shows as a ring around\n * its base — that ring is what makes the stack read as stacked.\n *\n * Position is a top edge plus two heights, never absolute bottoms, so moving\n * one unit doesn't force re-deriving every unit under it.\n */\nexport type Unit = {\n  id: string;\n  label: string;\n  /** Which side of the tower the label sits on. */\n  side: \"left\" | \"right\";\n  cx: number;\n  rx: number;\n  lidTop: number;\n  lidH: number;\n  drumH: number;\n  /** Lids are not co-axial with their drum in the reference. Override to taste. */\n  lidCx?: number;\n  lidRx?: number;\n  /** The single colour this whole unit is derived from. */\n  base: Lch;\n  ribCount?: number;\n  ribWidth?: number;\n  /** Nudge the leader anchor off the drum's vertical midpoint. */\n  labelYBias?: number;\n  /** Second line, set lighter and smaller under the label. */\n  support: string;\n  /** Reading order. \"01\" is the foundation, \"04\" the cap. */\n  ordinal: string;\n};\n\n/** The callouts: the curve, the ink, and the type. */\nexport type Annotation = {\n  /** Horizontal distance from the shape edge out to the text. */\n  lead: number;\n  /** Vertical offset of the text from the tip. Negative lifts it. */\n  bow: number;\n  /** Bezier control strength. Higher = flatter approach. */\n  curve: number;\n  opacity: number;\n  strokeWidth: number;\n  labelSize: number;\n  labelWeight: number;\n  supportSize: number;\n  supportWeight: number;\n  supportOpacity: number;\n};\n\nexport type Geometry = {\n  camera: Camera;\n  palette: Palette;\n  annotation: Annotation;\n  /** How far each lid overhangs its own drum — this is the visible ring. */\n  lidOverhang: number;\n  /** Ordered top of the tower → bottom. Painted in reverse. */\n  units: Unit[];\n};\n\nexport const DEFAULT_GEOMETRY: Geometry = {\n  camera: { ryRatio: 0.235, strokeWidth: 1.5 },\n  palette: {\n    ramp: { lightness: 5.5, chroma: 0.016, hue: -3 },\n    edgeShade: 0.16,\n    contactShade: 0.3,\n    ribStep: -0.8,\n    ribOpacity: 0.6,\n    ribBlend: \"overlay\",\n  },\n  annotation: {\n    lead: 60,\n    bow: -28,\n    curve: 54,\n    opacity: 0.55,\n    strokeWidth: 1.2,\n    labelSize: 29,\n    labelWeight: 600,\n    supportSize: 21,\n    supportWeight: 400,\n    supportOpacity: 0.6,\n  },\n  lidOverhang: 14,\n  units: [\n    {\n      id: \"rust\",\n      label: \"Target Ads\",\n      ordinal: \"04\",\n      support: \"In-market accounts\",\n      side: \"right\",\n      cx: 646,\n      rx: 176,\n      lidTop: 239,\n      lidH: 12,\n      drumH: 60,\n      lidCx: 646,\n      lidRx: 190,\n      base: { l: 86, c: 0.095, h: 62 },\n      ribCount: 30,\n      ribWidth: 1.5,\n    },\n    {\n      id: \"blue\",\n      label: \"Outreach\",\n      ordinal: \"03\",\n      support: \"Adaptive sends\",\n      side: \"left\",\n      cx: 499,\n      rx: 216,\n      lidTop: 295,\n      lidH: 12,\n      drumH: 126,\n      lidCx: 499,\n      lidRx: 226,\n      base: { l: 78, c: 0.055, h: 238 },\n      ribCount: 30,\n      ribWidth: 1.5,\n    },\n    {\n      id: \"pink\",\n      label: \"Content\",\n      ordinal: \"02\",\n      support: \"Stage-matched assets\",\n      side: \"right\",\n      cx: 586,\n      rx: 224,\n      lidTop: 417,\n      lidH: 12,\n      drumH: 126,\n      lidCx: 583,\n      lidRx: 236,\n      base: { l: 70, c: 0.075, h: 345 },\n      ribCount: 30,\n      ribWidth: 1.5,\n    },\n    {\n      id: \"green\",\n      label: \"Lookalike Audiences\",\n      ordinal: \"01\",\n      support: \"From your best accounts\",\n      side: \"left\",\n      // Runs past the bottom of the card and is clipped — the crop is what\n      // makes the tower feel taller than its frame.\n      cx: 668,\n      rx: 216,\n      lidTop: 539,\n      lidH: 12,\n      drumH: 180,\n      lidCx: 662,\n      lidRx: 237,\n      base: { l: 62, c: 0.09, h: 149 },\n      ribCount: 30,\n      ribWidth: 1.5,\n      labelYBias: 31,\n    },\n  ],\n};\n\n/* -------------------------------------------------------------- primitives */\n\ntype Cyl = { cx: number; rx: number; top: number; bottom: number };\n\n/**\n * Round every derived coordinate. Raw trig output serializes to a different\n * decimal string on the server than the client renders, which trips a hydration\n * mismatch the moment this SVG sits inside a client boundary.\n */\nconst q = (n: number) => Math.round(n * 1000) / 1000;\n\nconst ry = (rx: number, cam: Camera) => q(rx * cam.ryRatio);\n\nconst lidBottomOf = (u: Unit) => u.lidTop + u.lidH;\nconst drumBottomOf = (u: Unit) => lidBottomOf(u) + u.drumH;\n\nconst drumOf = (u: Unit): Cyl => ({\n  cx: u.cx,\n  rx: u.rx,\n  top: lidBottomOf(u),\n  bottom: drumBottomOf(u),\n});\n\nconst lidOf = (u: Unit, overhang: number): Cyl => ({\n  cx: u.lidCx ?? u.cx,\n  rx: u.lidRx ?? u.rx + overhang,\n  top: u.lidTop,\n  bottom: lidBottomOf(u),\n});\n\n/**\n * The front wall: down the left silhouette, across the near bottom arc, up the\n * right silhouette, back along the near top arc. The sweep flags are\n * load-bearing — swap them and the wall inverts into a bowtie. The far half of\n * the bottom ellipse is never drawn; the cylinder hides it.\n */\nfunction wallPath(geo: Cyl, cam: Camera) {\n  const r = ry(geo.rx, cam);\n  const l = geo.cx - geo.rx;\n  const rt = geo.cx + geo.rx;\n  return [\n    `M ${l} ${geo.top}`,\n    `V ${geo.bottom}`,\n    `A ${geo.rx} ${r} 0 0 0 ${rt} ${geo.bottom}`,\n    `V ${geo.top}`,\n    `A ${geo.rx} ${r} 0 0 1 ${l} ${geo.top}`,\n    \"Z\",\n  ].join(\" \");\n}\n\n/**\n * Ribs are spaced evenly in ANGLE and then projected, never evenly in x. That\n * is what bunches them toward the silhouette, starts each one on the curve of\n * the top ellipse, and makes them longest at the centre.\n */\nfunction ribLines(geo: Cyl, cam: Camera, count: number) {\n  const r = ry(geo.rx, cam);\n  return Array.from({ length: count }, (_, i) => {\n    const theta = (Math.PI * (i + 1)) / (count + 1);\n    const dx = -Math.cos(theta) * geo.rx;\n    const dy = Math.sin(theta) * r;\n    return { x: q(geo.cx + dx), y1: q(geo.top + dy), y2: q(geo.bottom + dy) };\n  });\n}\n\n/** The visible top ellipse of a cylinder. */\nfunction TopFace({ geo, fill, cam }: { geo: Cyl; fill: string; cam: Camera }) {\n  return (\n    <ellipse\n      cx={geo.cx}\n      cy={geo.top}\n      rx={geo.rx}\n      ry={ry(geo.rx, cam)}\n      fill={fill}\n      stroke={C.ink}\n      strokeWidth={cam.strokeWidth}\n    />\n  );\n}\n\n/** One cylinder: top face, front wall, edge falloff, optional ribs. */\nfunction Cylinder({\n  geo,\n  cam,\n  wall,\n  face,\n  rib,\n  ribOpacity = 1,\n  ribBlend = \"normal\",\n  edge,\n  edgeShade,\n  ribCount = 0,\n  ribWidth = 3,\n  id,\n}: {\n  geo: Cyl;\n  cam: Camera;\n  wall: string;\n  face: string;\n  rib?: string;\n  ribOpacity?: number;\n  ribBlend?: BlendMode;\n  /** Colour of the left/right falloff — a darker shade, never black. */\n  edge: string;\n  edgeShade: number;\n  ribCount?: number;\n  ribWidth?: number;\n  id: string;\n}) {\n  const d = wallPath(geo, cam);\n  const clipId = `${id}-clip`;\n  const edgeId = `${id}-edge`;\n\n  // isolation: without it the rib blend reaches past the drum and mixes with\n  // the canvas and whatever unit is painted underneath.\n  return (\n    <g style={{ isolation: \"isolate\" }}>\n      <TopFace geo={geo} fill={face} cam={cam} />\n      <path d={d} fill={wall} stroke={C.ink} strokeWidth={cam.strokeWidth} />\n\n      {ribCount > 0 && rib && (\n        <>\n          <clipPath id={clipId}>\n            <path d={d} />\n          </clipPath>\n          <g\n            clipPath={`url(#${clipId})`}\n            stroke={rib}\n            strokeWidth={ribWidth}\n            opacity={ribOpacity}\n            style={{ mixBlendMode: ribBlend }}\n          >\n            {ribLines(geo, cam, ribCount).map((l, i) => (\n              <line key={i} x1={l.x} y1={l.y1} x2={l.x} y2={l.y2} />\n            ))}\n          </g>\n        </>\n      )}\n\n      {/* Falloff on both sides — there is no key light here, the depth comes\n       * from geometry. Tinted with the unit's own dark shade rather than black,\n       * which would desaturate the chroma the ramp just added. */}\n      <defs>\n        <linearGradient id={edgeId} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n          <stop offset=\"0\" stopColor={edge} stopOpacity={edgeShade} />\n          <stop offset=\"0.35\" stopColor={edge} stopOpacity=\"0\" />\n          <stop offset=\"0.65\" stopColor={edge} stopOpacity=\"0\" />\n          <stop offset=\"1\" stopColor={edge} stopOpacity={edgeShade} />\n        </linearGradient>\n      </defs>\n      <path d={d} fill={`url(#${edgeId})`} />\n      <path d={d} fill=\"none\" stroke={C.ink} strokeWidth={cam.strokeWidth} />\n    </g>\n  );\n}\n\n/**\n * The unit resting on this lid darkens the ring around its own base. Drawn last\n * inside the unit, so the drum above covers the middle and only the crescent\n * survives.\n */\nfunction ContactShadow({\n  lid,\n  resting,\n  cam,\n  fill,\n  opacity,\n  id,\n}: {\n  lid: Cyl;\n  resting: Unit;\n  cam: Camera;\n  fill: string;\n  opacity: number;\n  id: string;\n}) {\n  const clipId = `${id}-clip`;\n  const rx = resting.rx + 10;\n  return (\n    <>\n      <clipPath id={clipId}>\n        <ellipse cx={lid.cx} cy={lid.top} rx={lid.rx} ry={ry(lid.rx, cam)} />\n      </clipPath>\n      <ellipse\n        clipPath={`url(#${clipId})`}\n        cx={resting.cx}\n        cy={drumBottomOf(resting) + 6}\n        rx={rx}\n        ry={ry(rx, cam)}\n        fill={fill}\n        opacity={opacity}\n      />\n    </>\n  );\n}\n\n/** A drum, its lid, and the shadow cast by whatever stands on that lid. */\nfunction StackUnit({\n  unit,\n  resting,\n  cam,\n  palette,\n  overhang,\n  prefix,\n}: {\n  unit: Unit;\n  resting?: Unit;\n  cam: Camera;\n  palette: Palette;\n  overhang: number;\n  prefix: string;\n}) {\n  const lid = lidOf(unit, overhang);\n  const { ramp } = palette;\n\n  // Every surface on this unit is the same colour walked down the ramp.\n  const tone = (steps: number) => shade(unit.base, ramp, steps);\n  const drumWall = tone(STEP.drumWall);\n\n  return (\n    <g>\n      {/* Drum first: it sits lower in space than its own lid. */}\n      <Cylinder\n        id={`${prefix}-${unit.id}-drum`}\n        geo={drumOf(unit)}\n        cam={cam}\n        wall={drumWall}\n        face={drumWall}\n        rib={tone(palette.ribStep)}\n        ribOpacity={palette.ribOpacity}\n        ribBlend={palette.ribBlend}\n        edge={tone(STEP.rib)}\n        edgeShade={palette.edgeShade}\n        ribCount={unit.ribCount}\n        ribWidth={unit.ribWidth}\n      />\n      <Cylinder\n        id={`${prefix}-${unit.id}-lid`}\n        geo={lid}\n        cam={cam}\n        wall={tone(STEP.lidWall)}\n        face={tone(STEP.lidFace)}\n        edge={tone(STEP.rib)}\n        edgeShade={palette.edgeShade}\n      />\n      {resting && (\n        <ContactShadow\n          id={`${prefix}-${resting.id}-contact`}\n          lid={lid}\n          resting={resting}\n          cam={cam}\n          fill={tone(STEP.contact)}\n          opacity={palette.contactShade}\n        />\n      )}\n    </g>\n  );\n}\n\n/** Strict painter's algorithm — bottom of the tower first, cap last. */\nfunction Tower({\n  geometry,\n  prefix,\n}: {\n  geometry: Geometry;\n  prefix: string;\n}) {\n  const bottomUp = [...geometry.units].reverse();\n  return (\n    <g clipPath={`url(#${prefix}-card-clip)`}>\n      {bottomUp.map((unit, i) => (\n        <StackUnit\n          key={unit.id}\n          unit={unit}\n          resting={bottomUp[i + 1]} // the unit standing on this one's lid\n          cam={geometry.camera}\n          palette={geometry.palette}\n          overhang={geometry.lidOverhang}\n          prefix={prefix}\n        />\n      ))}\n    </g>\n  );\n}\n\n/* -------------------------------------------------------------- annotation */\n\nconst ARROW = { len: 13, half: 7.5 };\n\n/** dir 1 points right, -1 left. The curve arrives horizontally, so no rotation. */\nfunction Arrowhead({ x, y, dir }: { x: number; y: number; dir: 1 | -1 }) {\n  const back = x - dir * ARROW.len;\n  return (\n    <path\n      d={`M ${x} ${y} L ${back} ${y - ARROW.half} L ${back} ${y + ARROW.half} Z`}\n      fill={C.ink}\n    />\n  );\n}\n\n/**\n * Derived from the unit, never authored separately, so arrows track the shapes\n * when the geometry is tuned. The tip lands on the drum silhouette at its\n * vertical midpoint — cx ± rx, where the ellipse is widest.\n */\nfunction leaderAnchor(u: Unit) {\n  const y = q((lidBottomOf(u) + drumBottomOf(u)) / 2 + (u.labelYBias ?? 0));\n  const x = u.side === \"right\" ? u.cx + u.rx : u.cx - u.rx;\n  return { x, y };\n}\n\n/**\n * Text off to the side and above, with a cubic bezier sweeping into the shape.\n * Both control points are placed horizontally from their anchors, so the curve\n * leaves the text flat and arrives at the shape flat.\n */\nfunction Leader({ unit, ann }: { unit: Unit; ann: Annotation }) {\n  const { x: tx, y: ty } = leaderAnchor(unit);\n  const out = unit.side === \"right\" ? 1 : -1; // away from the tower\n\n  const sx = q(tx + out * ann.lead); // curve start, beside the text\n  const sy = q(ty + ann.bow);\n  const ex = q(tx + out * ARROW.len); // curve end, behind the arrowhead\n  const d = [\n    `M ${sx} ${sy}`,\n    `C ${q(sx - out * ann.curve)} ${sy},`,\n    `${q(ex + out * ann.curve)} ${ty},`,\n    `${ex} ${ty}`,\n  ].join(\" \");\n\n  const textX = q(sx + out * 14);\n  const anchor = unit.side === \"right\" ? \"start\" : \"end\";\n\n  return (\n    <g>\n      <g opacity={ann.opacity}>\n        <path\n          d={d}\n          fill=\"none\"\n          stroke={C.ink}\n          strokeWidth={ann.strokeWidth}\n          strokeLinecap=\"round\"\n        />\n        <Arrowhead x={tx} y={ty} dir={(out === 1 ? -1 : 1) as 1 | -1} />\n      </g>\n\n      {/* Weight carries the hierarchy, not size alone — the support line steps\n       * down in both, plus a drop in opacity. */}\n      <text\n        x={textX}\n        y={q(sy - 5)}\n        fill={C.ink}\n        fontSize={ann.labelSize}\n        fontWeight={ann.labelWeight}\n        fontFamily={FONT}\n        textAnchor={anchor}\n        dominantBaseline=\"middle\"\n      >\n        <tspan fontSize={ann.supportSize} fontWeight={700} fillOpacity={0.42}>\n          {unit.ordinal}\n        </tspan>\n        <tspan dx={11}>{unit.label}</tspan>\n      </text>\n      <text\n        x={textX}\n        y={q(sy + ann.supportSize + 5)}\n        fill={C.ink}\n        fillOpacity={ann.supportOpacity}\n        fontSize={ann.supportSize}\n        fontWeight={ann.supportWeight}\n        fontFamily={FONT}\n        textAnchor={anchor}\n        dominantBaseline=\"middle\"\n      >\n        {unit.support}\n      </text>\n    </g>\n  );\n}\n\n/* ------------------------------------------------------------------ chrome */\n\n/** The one piece that has to be a hand-authored path. */\nfunction BirdMark({ x, y, size }: { x: number; y: number; size: number }) {\n  return (\n    <g transform={`translate(${x} ${y}) scale(${size / 24})`}>\n      <path\n        d=\"M2 10.2 6.4 8.2c-.4-2.8.8-5.6 3.2-6.8 0 2-.2 3.4-.6 4.6 3-.8 6 .8 7.4 3.6 1.4 2.8 1.2 5.8-.2 8.2l6.2 4.8-8.2-2.2c-3.2.6-6.2-1.2-7.4-4.2-.8-2.2-.8-4.4-.2-6z\"\n        fill={C.ink}\n      />\n    </g>\n  );\n}\n\n/** Wordmark and headline above the tower. */\nfunction Masthead({ brand, headline }: { brand: string; headline: string }) {\n  return (\n    <g>\n      <BirdMark x={548} y={62} size={30} />\n      <text\n        x={586}\n        y={88}\n        fill={C.ink}\n        fontSize={30}\n        fontWeight={600}\n        fontFamily={FONT}\n      >\n        {brand}\n      </text>\n      <text\n        x={VIEW_W / 2}\n        y={172}\n        fill={C.ink}\n        fontSize={54}\n        fontWeight={400}\n        fontFamily={FONT}\n        textAnchor=\"middle\"\n      >\n        {headline}\n      </text>\n    </g>\n  );\n}\n\n/** Outer bleed plus the inset card the tower is clipped to. */\nfunction Frame() {\n  return (\n    <>\n      <rect width={VIEW_W} height={VIEW_H} fill={C.frame} />\n      <rect\n        x={CARD.x}\n        y={CARD.y}\n        width={CARD.w}\n        height={CARD.h}\n        fill={C.canvas}\n      />\n    </>\n  );\n}\n\n/* ------------------------------------------------------------------ export */\n\nexport interface StackedTowerProps {\n  className?: string;\n  /** Every measurement, colour and label in the drawing. */\n  geometry?: Geometry;\n  brand?: string;\n  headline?: string;\n  /**\n   * Prefix for every generated SVG id. Ids are document-global, so two\n   * instances on one page need two prefixes or the second steals the first's\n   * clip paths and gradients.\n   */\n  id?: string;\n}\n\nexport default function StackedTower({\n  className,\n  geometry = DEFAULT_GEOMETRY,\n  brand = \"Cardinal\",\n  headline = \"Your sales motion, built by you\",\n  id = \"stacked-tower\",\n}: StackedTowerProps) {\n  const titleId = `${id}-title`;\n  const descId = `${id}-desc`;\n\n  return (\n    <svg\n      viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}\n      preserveAspectRatio=\"xMidYMid meet\"\n      role=\"img\"\n      aria-labelledby={`${titleId} ${descId}`}\n      className={className}\n    >\n      <title id={titleId}>{headline}</title>\n      <desc id={descId}>\n        Four stacked cylinders labelled, from top to bottom,{\" \"}\n        {geometry.units.map((u) => u.label).join(\", \")}.\n      </desc>\n\n      <defs>\n        <clipPath id={`${id}-card-clip`}>\n          <rect x={CARD.x} y={CARD.y} width={CARD.w} height={CARD.h} />\n        </clipPath>\n      </defs>\n\n      <Frame />\n      <Masthead brand={brand} headline={headline} />\n      <Tower geometry={geometry} prefix={id} />\n\n      {/* Annotation always sits on top and is never occluded. */}\n      {geometry.units.map((u) => (\n        <Leader key={u.id} unit={u} ann={geometry.annotation} />\n      ))}\n    </svg>\n  );\n}\n",
      "type": "registry:component",
      "target": "~/components/craftui/designs/stacked-tower/default.tsx"
    }
  ],
  "meta": {
    "displayName": "Stacked Tower - Stacked Tower",
    "description": "Four cylindrical units stacked into a deliberately unsteady tower. Every ellipse obeys one camera ratio, so height reads as vertical offset rather than as tilt; every surface on a unit is derived from one OKLCH base walked down a ramp that gains chroma as it darkens; ribs are spaced in angle and projected, so they bunch toward the silhouette. The base drum runs off the bottom of the card, which is what makes the tower feel taller than its frame. Static — no hooks, no motion, zero JavaScript.",
    "category": "illustration",
    "tags": [
      "design",
      "static",
      "illustration",
      "svg",
      "3d",
      "cylinder",
      "oklch",
      "annotation",
      "marketing"
    ],
    "animationType": "none"
  }
}