{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "game-grid",
  "title": "Game grid",
  "description": "The pokerdata.io game picker: one card per game with its table, stack depths, pack counts, data size and bundle price. Pass the cards from pokerdata.io's partner library.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://www.pokerstudy.ai/r/pack-table.json"
  ],
  "files": [
    {
      "path": "src/components/pokerdata/game-grid.tsx",
      "content": "import Link from \"next/link\";\nimport { ArrowUpRight } from \"lucide-react\";\nimport { PackTableViz } from \"@/components/pokerdata/pack-table-viz\";\n\n/**\n * The pokerdata.io game picker: one card per game type, each a link, with\n * what the game holds and what the whole thing costs. Shared by pokerdata.io's\n * catalog home and pokerstudy.ai's /downloads.\n *\n * Pure: the cards come in as plain data. pokerdata.io builds them from its\n * catalog (`gameCard` in its `@/lib/game-summary`) and serves the same shape\n * to pokerstudy.ai from `GET /api/partner/library` (`games`), so prices and\n * pack counts are only ever worked out in one place.\n */\n\nexport type GameCard = {\n  /** URL slug and bundle id — `nlh-6max`. */\n  slug: string;\n  /** \"NLHE 6-max\". */\n  title: string;\n  /** One line on what the game holds. */\n  blurb: string;\n  /** Where the card goes. */\n  href: string;\n  /** Table size, for the drawn table: 2, 6, 8 or 9. */\n  seats: number;\n  preflop: {\n    count: number;\n    bytes: number;\n    stacks: number[];\n    /** One preflop pack's price: \"$50\", or \"from $50\" when packs differ. */\n    priceLabel: string;\n  };\n  postflop: { count: number; boards: number; stacks: number[] };\n  /**\n   * The whole-game bundle, when it's on sale for less than its packs one at a\n   * time. `label` is \"All of it\", or what it covers when that's not everything.\n   */\n  bundle: { priceCents: number; singlesCents: number; label: string } | null;\n};\n\nconst cx = (...c: (string | false | null | undefined)[]) =>\n  c.filter(Boolean).join(\" \");\n\n/** 5000 → \"$50\", 4999 → \"$49.99\". */\nfunction formatPrice(cents: number): string {\n  const digits = cents % 100 === 0 ? 0 : 2;\n  return `$${(cents / 100).toLocaleString(\"en-US\", {\n    minimumFractionDigits: digits,\n    maximumFractionDigits: digits,\n  })}`;\n}\n\nfunction percentOff(priceCents: number, compareAtCents: number): string {\n  return `${Math.round((1 - priceCents / compareAtCents) * 100)}% off`;\n}\n\nfunction formatSize(bytes: number): string {\n  if (bytes < 1024 ** 2) return `${Math.round(bytes / 1024)} KB`;\n  if (bytes < 1024 ** 3) return `${Math.round(bytes / 1024 ** 2)} MB`;\n  return `${(bytes / 1024 ** 3).toFixed(1)} GB`;\n}\n\nconst focus =\n  \"focus-visible:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--rb-accent,oklch(100%_0_0))]\";\n\nconst frame =\n  \"group flex h-full flex-col gap-1 rounded-[var(--rb-r-2xl,14px)] border border-neutral-800 bg-neutral-900 p-1 transition-[border-color,transform] duration-150 ease-out hover:border-neutral-700 active:scale-[0.995]\";\n\nconst panel =\n  \"flex flex-1 flex-col gap-4 rounded-[var(--rb-r-lg,10px)] border border-neutral-800 bg-neutral-950 p-5\";\n\nfunction Stat({ label, value }: { label: string; value: string }) {\n  return (\n    <div className=\"flex flex-col gap-0.5\">\n      <span className=\"text-[13px] font-medium text-neutral-100\">{value}</span>\n      <span className=\"text-xs text-neutral-500\">{label}</span>\n    </div>\n  );\n}\n\nexport function GameGrid({\n  games,\n  newTab = false,\n}: {\n  games: GameCard[];\n  /** Open each card in a new tab, for links that leave the site. */\n  newTab?: boolean;\n}) {\n  return (\n    <ul className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n      {games.map((g) => {\n        const { preflop, postflop, bundle } = g;\n        const total = preflop.count + postflop.count;\n        return (\n          <li key={g.slug} id={g.slug}>\n            <Link\n              href={g.href}\n              className={cx(frame, focus)}\n              {...(newTab ? { target: \"_blank\", rel: \"noopener\" } : {})}\n            >\n              <div className={panel}>\n                <div className=\"flex items-start justify-between gap-4\">\n                  <div className=\"flex min-w-0 flex-col gap-1\">\n                    <h2 className=\"text-lg font-medium tracking-[-0.01em] text-neutral-100\">\n                      {g.title}\n                    </h2>\n                    <p className=\"text-[13px] leading-relaxed text-neutral-400\">\n                      {g.blurb}\n                    </p>\n                  </div>\n                  <ArrowUpRight\n                    aria-hidden=\"true\"\n                    className=\"h-4 w-4 shrink-0 text-neutral-500 transition-colors duration-150 group-hover:text-neutral-100\"\n                  />\n                </div>\n\n                <div className=\"-mx-2 -my-1 hidden sm:block\">\n                  <PackTableViz\n                    seats={g.seats}\n                    stackBbs={preflop.count > 0 ? preflop.stacks : postflop.stacks}\n                    label={preflop.count > 0 ? \"Preflop\" : \"Postflop\"}\n                    compact\n                  />\n                </div>\n\n                <div className=\"mt-auto grid grid-cols-3 gap-3 border-t border-neutral-800 pt-4\">\n                  <Stat\n                    label=\"Preflop packs\"\n                    value={preflop.count > 0 ? String(preflop.count) : \"—\"}\n                  />\n                  <Stat\n                    label=\"Postflop packs\"\n                    value={postflop.count > 0 ? String(postflop.count) : \"—\"}\n                  />\n                  {preflop.count > 0 ? (\n                    <Stat label=\"Preflop data\" value={formatSize(preflop.bytes)} />\n                  ) : (\n                    <Stat label=\"Solved flops\" value={String(postflop.boards)} />\n                  )}\n                </div>\n              </div>\n\n              <div className=\"flex h-10 items-center justify-between gap-3 px-4 text-[13px]\">\n                <span className=\"text-neutral-400\">\n                  Browse free · {total} pack{total === 1 ? \"\" : \"s\"}\n                </span>\n                {bundle ? (\n                  <span className=\"flex items-center gap-2\">\n                    <span className=\"rounded-full bg-neutral-100 px-2 py-0.5 text-[11px] font-medium text-neutral-900\">\n                      {percentOff(bundle.priceCents, bundle.singlesCents)}\n                    </span>\n                    <span className=\"font-medium text-neutral-100\">\n                      {bundle.label} {formatPrice(bundle.priceCents)}\n                    </span>\n                  </span>\n                ) : preflop.count > 0 ? (\n                  <span className=\"font-medium text-neutral-100\">\n                    {preflop.priceLabel} per pack\n                  </span>\n                ) : null}\n              </div>\n            </Link>\n          </li>\n        );\n      })}\n    </ul>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pokerdata/game-grid.tsx"
    }
  ],
  "type": "registry:block"
}