{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plug-popup",
  "title": "Plug popup",
  "description": "The subclass-plug inspect flyout — the in-game tooltip for abilities, aspects, fragments, supers, and subclasses, the counterpart to ItemPopup for non-gear plugs.",
  "dependencies": [
    "@engram/core"
  ],
  "registryDependencies": [
    "@engram/bungie-image",
    "@engram/cn",
    "@engram/element-icon",
    "@engram/key-hint",
    "@engram/panel",
    "@engram/token-utils",
    "@engram/tokens",
    "@engram/weapon-icon"
  ],
  "files": [
    {
      "path": "src/components/plug-popup.tsx",
      "content": "import {\n  type ElementType,\n  type PlugPopupProps as PlugData,\n  type PlugKeyword,\n  type PlugMetaRow,\n  weaponGlyphFromName,\n} from \"@engram/core\";\nimport { Fragment, type ReactNode } from \"react\";\nimport { cn } from \"../lib/cn.js\";\nimport { elementVar } from \"../lib/tokens.js\";\nimport { BungieImage } from \"./bungie-image.js\";\nimport { ElementIcon } from \"./element-icon.js\";\nimport { KeyHint } from \"./key-hint.js\";\nimport { Panel } from \"./panel.js\";\nimport { WeaponIcon } from \"./weapon-icon.js\";\n\nexport interface PlugPopupProps extends PlugData {\n  /** Action buttons (e.g. an Apply button). */\n  footer?: ReactNode;\n  className?: string;\n}\n\n// Element-tinted header band — the near-flat fill with a top sheen the in-game\n// subclass tooltip uses, in the plug's element color (falls back to the neutral\n// accent for element-less plugs).\nfunction bandImage(color: string): string {\n  return `linear-gradient(180deg, color-mix(in srgb, ${color} 80%, #fff 16%) 0%, ${color} 46%, color-mix(in srgb, ${color} 84%, #000 16%) 100%)`;\n}\n\n// Bungie inline element tokens (\"[Stasis]\") → the element they name.\nconst TOKEN_ELEMENT: Record<string, ElementType> = {\n  kinetic: \"kinetic\",\n  arc: \"arc\",\n  solar: \"solar\",\n  void: \"void\",\n  stasis: \"stasis\",\n  strand: \"strand\",\n  prismatic: \"prismatic\",\n};\n\n// Split `text` on the tint terms and color each match — the in-game habit of\n// coloring mechanic terms (Jolted, Bolt Charge, Stasis) each in its own color.\nfunction tintTerms(text: string, tints: Map<string, string>): ReactNode {\n  if (tints.size === 0) return text;\n  const escaped = [...tints.keys()]\n    .sort((a, b) => b.length - a.length) // match multi-word terms before their parts\n    .map((k) => k.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n  const re = new RegExp(`\\\\b(${escaped.join(\"|\")})\\\\b`, \"gi\");\n  return text.split(re).map((part, i) => {\n    const color = tints.get(part.toLowerCase());\n    return color ? (\n      // biome-ignore lint/suspicious/noArrayIndexKey: static split of fixed text\n      <span key={i} className=\"font-semibold\" style={{ color }}>\n        {part}\n      </span>\n    ) : (\n      part\n    );\n  });\n}\n\n// Rich prose: Bungie's inline \"[Stasis]\" / \"[Rocket Launcher]\" tokens become\n// inline element / weapon-type icons (exactly as the game renders them);\n// unknown tokens are dropped; the plain text between tokens gets its keyword\n// terms tinted.\nfunction richText(text: string, tints: Map<string, string>): ReactNode {\n  return text.split(/(\\[[^\\]]+\\])/g).map((part, i) => {\n    const token = /^\\[([^\\]]+)\\]$/.exec(part)?.[1];\n    if (token !== undefined) {\n      const el = TOKEN_ELEMENT[token.toLowerCase()];\n      if (el) {\n        return (\n          <ElementIcon\n            // biome-ignore lint/suspicious/noArrayIndexKey: static split of fixed text\n            key={i}\n            element={el}\n            size={14}\n            className=\"mx-0.5 inline-flex align-[-2px]\"\n          />\n        );\n      }\n      const weapon = weaponGlyphFromName(token);\n      if (weapon) {\n        return (\n          <WeaponIcon\n            // biome-ignore lint/suspicious/noArrayIndexKey: static split of fixed text\n            key={i}\n            type={weapon}\n            name={token}\n            size={13}\n            className=\"mx-0.5 inline-flex align-[-1px]\"\n          />\n        );\n      }\n      return null;\n    }\n    // biome-ignore lint/suspicious/noArrayIndexKey: static split of fixed text\n    return <Fragment key={i}>{tintTerms(part, tints)}</Fragment>;\n  });\n}\n\nfunction MetaValue({ row }: { row: PlugMetaRow }) {\n  if (row.keybind) return <KeyHint label={row.value}>{row.keybind}</KeyHint>;\n  return (\n    <span className=\"font-engram-display font-semibold text-engram-fg tabular-nums\">\n      {row.value}\n    </span>\n  );\n}\n\n// One rolled-in keyword definition: a tinted diamond glyph, the term name, and\n// its effect text (with nested keyword terms tinted too).\nfunction Definition({\n  kw,\n  tints,\n  fallbackTint,\n}: {\n  kw: PlugKeyword;\n  tints: Map<string, string>;\n  fallbackTint: string;\n}) {\n  const tint = kw.element ? elementVar(kw.element) : fallbackTint;\n  return (\n    <div className=\"flex gap-2.5\">\n      {kw.icon ? (\n        <BungieImage\n          src={kw.icon}\n          aria-hidden\n          className=\"mt-0.5 size-4 shrink-0 object-contain\"\n        />\n      ) : (\n        <span\n          aria-hidden\n          className=\"mt-1 size-2.5 shrink-0 rotate-45\"\n          style={{\n            background: tint,\n            boxShadow: `0 0 5px color-mix(in oklch, ${tint} 70%, transparent)`,\n          }}\n        />\n      )}\n      <div className=\"flex flex-col gap-0.5\">\n        <span\n          className=\"font-engram-display font-semibold text-[13px]\"\n          style={{ color: tint }}\n        >\n          {kw.name}\n        </span>\n        {kw.description ? (\n          <p className=\"text-[12.5px] text-engram-muted leading-relaxed\">\n            {richText(kw.description, tints)}\n          </p>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\n/**\n * The subclass-plug inspect flyout — the in-game tooltip for abilities, aspects,\n * fragments, supers, and subclasses, the counterpart to {@link ItemPopup} for\n * non-gear plugs. An element-tinted header (large name + category subtitle), a\n * wide banner image, an optional italic flavor line, the body description with\n * mechanic `keywords` tinted in the element color, an aspect-only\n * \"Fragment Slots (N)\" pip row, a rolled-in keyword `definitions` panel (the\n * in-game ancillary popup, folded into this one card), foot `meta` rows (keybind\n * / cooldown), and an optional action `footer`. Presentational only — pairs with\n * {@link ItemHoverCard} exactly like `ItemPopup` does.\n */\nexport function PlugPopup({\n  name,\n  subtitle,\n  element,\n  banner,\n  flavor,\n  description,\n  keywords,\n  definitions,\n  fragmentSlots,\n  meta,\n  footer,\n  className,\n}: PlugPopupProps) {\n  const tint = element ? elementVar(element) : \"var(--engram-accent)\";\n  // Prose tinting: plain keywords take the popup tint, definition names their\n  // own element color, and any element word whose \"[Token]\" appears in the\n  // prose tints in that element's color (beside its inline icon, as in-game).\n  const tints = new Map<string, string>();\n  for (const k of keywords ?? []) tints.set(k.toLowerCase(), tint);\n  for (const d of definitions ?? []) {\n    tints.set(d.name.toLowerCase(), d.element ? elementVar(d.element) : tint);\n  }\n  const prose = [\n    description,\n    flavor,\n    ...(definitions?.map((d) => d.description) ?? []),\n  ].join(\" \");\n  for (const m of prose.matchAll(/\\[([^\\]]+)\\]/g)) {\n    const el = TOKEN_ELEMENT[m[1].toLowerCase()];\n    if (el) tints.set(el, elementVar(el));\n  }\n  return (\n    <Panel\n      tone=\"glass\"\n      className={cn(\n        \"flex w-[320px] max-w-full flex-col border border-engram-border-strong\",\n        className,\n      )}\n    >\n      <header\n        className=\"flex flex-col justify-center px-4 py-3\"\n        style={{ backgroundImage: bandImage(tint), color: \"#ffffff\" }}\n      >\n        <h3 className=\"break-words font-engram-display font-bold text-2xl uppercase leading-[1.04] tracking-tight\">\n          {name}\n        </h3>\n        {subtitle ? (\n          <p className=\"mt-0.5 text-[13px] opacity-80\">{subtitle}</p>\n        ) : null}\n      </header>\n\n      {banner ? (\n        <BungieImage\n          src={banner}\n          className=\"h-[116px] w-full object-cover\"\n          aria-hidden\n        />\n      ) : null}\n\n      <div className=\"flex flex-col px-4 py-3.5\">\n        {fragmentSlots != null && fragmentSlots > 0 ? (\n          <div className=\"flex items-center gap-2.5 text-[13px]\">\n            <span className=\"font-engram-display text-engram-muted tracking-[0.02em]\">\n              Fragment Slots ({fragmentSlots})\n            </span>\n            <span className=\"flex items-center gap-1.5\" aria-hidden>\n              {Array.from({ length: fragmentSlots }, (_, i) => (\n                <span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: fixed-count pips\n                  key={i}\n                  className=\"size-2.5 rotate-45 border border-engram-border-strong bg-white/10\"\n                />\n              ))}\n            </span>\n          </div>\n        ) : null}\n\n        {flavor ? (\n          <p\n            className={cn(\n              \"text-engram-muted text-sm italic leading-relaxed\",\n              fragmentSlots ? \"mt-3\" : null,\n            )}\n          >\n            {richText(flavor, tints)}\n          </p>\n        ) : null}\n\n        {description ? (\n          <p\n            className={cn(\n              \"text-[13px] text-engram-fg/85 leading-relaxed\",\n              flavor || fragmentSlots ? \"mt-3\" : null,\n            )}\n          >\n            {richText(description, tints)}\n          </p>\n        ) : null}\n\n        {definitions && definitions.length > 0 ? (\n          <div className=\"-mx-4 mt-3.5 flex flex-col gap-3 border-engram-border border-t bg-white/[0.025] px-4 py-3\">\n            {definitions.map((kw) => (\n              <Definition\n                key={kw.name}\n                kw={kw}\n                tints={tints}\n                fallbackTint={tint}\n              />\n            ))}\n          </div>\n        ) : null}\n\n        {meta && meta.length > 0 ? (\n          <div className=\"mt-3.5 flex flex-col gap-2 border-engram-border border-t pt-3 text-[13px]\">\n            {meta.map((row) => (\n              <div\n                key={row.label}\n                className=\"flex items-center justify-between gap-3\"\n              >\n                <span className=\"text-engram-muted\">{row.label}</span>\n                <MetaValue row={row} />\n              </div>\n            ))}\n          </div>\n        ) : null}\n      </div>\n\n      {footer ? (\n        <div className=\"flex flex-wrap justify-end gap-2 border-engram-border border-t bg-white/[0.03] px-4 py-2.5\">\n          {footer}\n        </div>\n      ) : null}\n    </Panel>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/engram/plug-popup.tsx"
    }
  ],
  "meta": {
    "level": "component"
  },
  "docs": "Extend without forking: edit the copied source, use `asChild` (Radix Slot) to change the rendered element, pass the typed `annotations` prop for curated data (verdict/tags/per-plug), or use slot / render-prop props for arbitrary content. Requires the @engram/tokens theme (--engram-* CSS variables).",
  "categories": [
    "loadout"
  ],
  "type": "registry:component"
}