{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "match-card",
  "title": "Match Card",
  "description": "Compact match row with teams, score or kickoff time, status, and an optional odds row.",
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/mrdoge-ui/match-card/match-card.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\nimport { EntityImage } from \"@/registry/mrdoge-ui/entity-image/entity-image\"\nimport {\n  OddsSelector,\n  type OddsOption,\n} from \"@/registry/mrdoge-ui/odds-selector/odds-selector\"\n\nexport type MatchCardStatus =\n  | \"scheduled\"\n  | \"live\"\n  | \"paused\"\n  | \"intermission\"\n  | \"interrupted\"\n  | \"finished\"\n\nexport interface MatchCardTeam {\n  name: string\n  logoUrl?: string\n  /** Shown as a small red-card marker (position set by MatchCardProps.redCardPosition) once the match is live or finished. */\n  redCards?: number\n}\n\nexport interface MatchCardOdds {\n  /** Market name, e.g. \"Match Result\". Not currently rendered. */\n  market: string\n  options: OddsOption[]\n}\n\nexport interface MatchCardDataProps {\n  loading?: false\n  status: MatchCardStatus\n  kickoff?: Date | string\n  elapsed?: string\n  /**\n   * Clock format for the kickoff time.\n   *\n   * @defaultValue \"24h\"\n   */\n  timeFormat?: \"12h\" | \"24h\"\n  home: MatchCardTeam\n  away: MatchCardTeam\n  homeScore?: number\n  awayScore?: number\n  /**\n   * Where the red-card marker renders relative to each team row: \"left\"\n   * (next to the team name) or \"right\" (next to the score).\n   *\n   * @defaultValue \"right\"\n   */\n  redCardPosition?: \"left\" | \"right\"\n  /**\n   * Renders a labeled odds card when provided. Any market works: Match\n   * Card doesn't care which one, it just needs a name and options.\n   */\n  odds?: MatchCardOdds\n  /** Shows a skeleton in the odds area while set. Ignored once `odds` is provided. */\n  oddsLoading?: boolean\n  /**\n   * Where the odds card renders. Falls back to \"bottom\" below a `@lg`\n   * (32rem) container width.\n   *\n   * @defaultValue \"bottom\"\n   */\n  oddsPosition?: \"bottom\" | \"right\"\n  selectedOddsId?: string\n  /** Called with the pressed option's id, or `undefined` when pressing the already-selected option deselects it. */\n  onSelectOdds?: (id: string | undefined) => void\n  className?: string\n}\n\ninterface MatchCardLoadingProps {\n  /** Renders a skeleton with the same dimensions instead; no other prop is needed. */\n  loading: true\n  /** Reserves space for an odds row/column in the skeleton too. */\n  oddsLoading?: boolean\n  /** Same as MatchCardDataProps.oddsPosition. */\n  oddsPosition?: \"bottom\" | \"right\"\n  className?: string\n}\n\nexport type MatchCardProps = MatchCardDataProps | MatchCardLoadingProps\n\nfunction formatKickoff(kickoff: Date | string, timeFormat: \"12h\" | \"24h\") {\n  const date = typeof kickoff === \"string\" ? new Date(kickoff) : kickoff\n  return date.toLocaleTimeString(undefined, {\n    hour: \"numeric\",\n    minute: \"2-digit\",\n    hour12: timeFormat === \"12h\",\n  })\n}\n\n// Today's kickoff time is more useful than the date; any other day, the\n// date is what tells the two matches apart.\nfunction formatFinishedDate(kickoff: Date | string, timeFormat: \"12h\" | \"24h\") {\n  const date = typeof kickoff === \"string\" ? new Date(kickoff) : kickoff\n  const isToday = date.toDateString() === new Date().toDateString()\n  return isToday\n    ? formatKickoff(date, timeFormat)\n    : date.toLocaleDateString(undefined, { day: \"2-digit\", month: \"2-digit\" })\n}\n\nconst stoppedPlayLabel: Record<\"paused\" | \"intermission\" | \"interrupted\", string> = {\n  paused: \"Paused\",\n  intermission: \"Intermission\",\n  interrupted: \"Interrupted\",\n}\n\nfunction StatusColumn({\n  status,\n  kickoff,\n  elapsed,\n  timeFormat,\n}: {\n  status: MatchCardStatus\n  kickoff?: Date | string\n  elapsed?: string\n  timeFormat: \"12h\" | \"24h\"\n}) {\n  if (status === \"live\") {\n    return (\n      <span className=\"text-xs font-bold leading-tight text-destructive tabular-nums\">\n        {elapsed ?? \"LIVE\"}\n      </span>\n    )\n  }\n\n  if (status === \"paused\" || status === \"intermission\" || status === \"interrupted\") {\n    return (\n      <span className=\"text-xs font-bold leading-tight text-amber-600 tabular-nums dark:text-amber-400\">\n        {elapsed ?? stoppedPlayLabel[status]}\n      </span>\n    )\n  }\n\n  if (status === \"finished\") {\n    return (\n      <div className=\"flex flex-col items-center gap-0.5\">\n        <span className=\"text-xs font-bold leading-tight text-muted-foreground tabular-nums\">\n          {elapsed ?? \"FT\"}\n        </span>\n        {kickoff ? (\n          <span className=\"text-xs font-medium leading-tight text-muted-foreground/70 tabular-nums\">\n            {formatFinishedDate(kickoff, timeFormat)}\n          </span>\n        ) : null}\n      </div>\n    )\n  }\n\n  return (\n    <span className=\"text-xs font-medium leading-tight text-muted-foreground tabular-nums\">\n      {kickoff ? formatKickoff(kickoff, timeFormat) : \"—\"}\n    </span>\n  )\n}\n\nfunction RedCardIndicator({ count }: { count: number }) {\n  return (\n    <div className=\"flex items-center gap-0.5\">\n      {count > 1 ? (\n        <span className=\"text-[11px] font-bold tabular-nums text-destructive\">x{count}</span>\n      ) : null}\n      <span className=\"block h-3 w-2 shrink-0 rounded-[1.5px] bg-destructive\" />\n    </div>\n  )\n}\n\nfunction TeamRow({\n  team,\n  score,\n  showScore,\n  showRedCards,\n  redCardPosition,\n  live,\n  dimmed,\n}: {\n  team: MatchCardTeam\n  score?: number\n  showScore: boolean\n  showRedCards: boolean\n  redCardPosition: \"left\" | \"right\"\n  /** True while the match is in progress (live/paused/intermission/interrupted); the score reads in the live color. */\n  live: boolean\n  /** True for the losing side once the match is finished. */\n  dimmed: boolean\n}) {\n  const redCard =\n    showRedCards && team.redCards ? <RedCardIndicator count={team.redCards} /> : null\n\n  return (\n    <div className=\"flex items-center justify-between gap-2\">\n      <div className=\"flex min-w-0 items-center gap-2\">\n        <EntityImage src={team.logoUrl} name={team.name} size=\"sm\" />\n        <span className=\"truncate text-sm font-medium\">{team.name}</span>\n        {redCardPosition === \"left\" ? redCard : null}\n      </div>\n      <div className=\"flex shrink-0 items-center gap-2.5\">\n        {redCardPosition === \"right\" ? redCard : null}\n        {showScore ? (\n          <span\n            className={cn(\n              \"text-sm font-semibold tabular-nums\",\n              live && \"text-destructive\",\n              dimmed && \"text-muted-foreground\"\n            )}\n          >\n            {score ?? 0}\n          </span>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nfunction TeamRowSkeleton({ nameWidth }: { nameWidth: string }) {\n  return (\n    <div className=\"flex items-center justify-between gap-2\">\n      <div className=\"flex min-w-0 items-center gap-2\">\n        <span className=\"size-5 shrink-0 animate-pulse rounded-full bg-muted\" />\n        <span className={cn(\"h-3 animate-pulse rounded bg-muted\", nameWidth)} />\n      </div>\n      <span className=\"h-3 w-4 shrink-0 animate-pulse rounded bg-muted\" />\n    </div>\n  )\n}\n\n/** Same shape as OddsSelector's button grid, sized to match its real height. */\nfunction OddsSkeleton() {\n  return (\n    <div className=\"flex h-full flex-col\">\n      <div className=\"grid flex-1 auto-cols-fr grid-flow-col divide-x\">\n        {[0, 1, 2].map((i) => (\n          <div key={i} className=\"flex flex-col items-center justify-center gap-0.5 px-2 py-2.5\">\n            <div className=\"flex h-4 items-center\">\n              <span className=\"h-2.5 w-4 animate-pulse rounded bg-muted\" />\n            </div>\n            <div className=\"flex h-5 items-center\">\n              <span className=\"h-3 w-10 animate-pulse rounded bg-muted\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Matches MatchCard's real dimensions exactly, so nothing shifts when\n * live data arrives. Exported separately too, for a loading list of\n * several cards before any of them have data yet.\n */\nexport function MatchCardSkeleton({\n  className,\n  oddsLoading,\n  oddsPosition = \"bottom\",\n}: {\n  className?: string\n  oddsLoading?: boolean\n  oddsPosition?: \"bottom\" | \"right\"\n}) {\n  const wantsRow = oddsLoading && oddsPosition === \"right\"\n\n  return (\n    <div\n      className={cn(\n        \"@container w-full overflow-hidden rounded-xl border bg-card text-card-foreground\",\n        className\n      )}\n    >\n      <div className={cn(wantsRow && \"@lg:flex @lg:items-stretch\")}>\n        <div className={cn(\"flex flex-1 gap-3 px-3 py-3\", wantsRow && \"@lg:min-w-0\")}>\n          <div className=\"flex w-11 shrink-0 items-center justify-center border-r pr-3\">\n            <span className=\"h-3 w-7 animate-pulse rounded bg-muted\" />\n          </div>\n          <div className=\"flex min-w-0 flex-1 flex-col justify-center gap-2\">\n            <TeamRowSkeleton nameWidth=\"w-24\" />\n            <TeamRowSkeleton nameWidth=\"w-20\" />\n          </div>\n        </div>\n        {oddsLoading ? (\n          <div\n            className={cn(\n              \"border-t\",\n              wantsRow && \"@lg:w-64 @lg:shrink-0 @lg:border-t-0 @lg:border-l\"\n            )}\n          >\n            <OddsSkeleton />\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nexport function MatchCard(props: MatchCardProps) {\n  if (props.loading) {\n    return (\n      <MatchCardSkeleton\n        className={props.className}\n        oddsLoading={props.oddsLoading}\n        oddsPosition={props.oddsPosition}\n      />\n    )\n  }\n\n  const {\n    status,\n    kickoff,\n    elapsed,\n    timeFormat = \"24h\",\n    home,\n    away,\n    homeScore,\n    awayScore,\n    redCardPosition = \"right\",\n    odds,\n    oddsLoading,\n    oddsPosition = \"bottom\",\n    selectedOddsId,\n    onSelectOdds,\n    className,\n  } = props\n\n  const showScore = status !== \"scheduled\"\n  const showRedCards = status !== \"scheduled\"\n  const isFinished = status === \"finished\"\n  const isLive = showScore && !isFinished\n  const hasScores = homeScore != null && awayScore != null\n  const homeLost = isFinished && hasScores && homeScore < awayScore\n  const awayLost = isFinished && hasScores && awayScore < homeScore\n  const hasOdds = Boolean(odds && odds.options.length > 0)\n  const showOddsSlot = hasOdds || (oddsLoading && !odds)\n  // @container below falls back to stacked under @lg regardless of oddsPosition.\n  const wantsRow = showOddsSlot && oddsPosition === \"right\"\n\n  const oddsCard = hasOdds ? (\n    <OddsSelector\n      variant=\"bare\"\n      options={odds!.options}\n      selectedId={selectedOddsId}\n      onSelect={onSelectOdds}\n      className=\"w-full\"\n    />\n  ) : showOddsSlot ? (\n    <OddsSkeleton />\n  ) : null\n\n  return (\n    <div\n      className={cn(\n        \"@container w-full overflow-hidden rounded-xl border bg-card text-card-foreground\",\n        className\n      )}\n    >\n      <div className={cn(wantsRow && \"@lg:flex @lg:items-stretch\")}>\n        <div className={cn(\"flex flex-1 gap-3 px-3 py-3\", wantsRow && \"@lg:min-w-0\")}>\n          <div className=\"flex w-11 shrink-0 items-center justify-center border-r pr-3 text-center\">\n            <StatusColumn status={status} kickoff={kickoff} elapsed={elapsed} timeFormat={timeFormat} />\n          </div>\n          <div className=\"flex min-w-0 flex-1 flex-col justify-center gap-2\">\n            <TeamRow\n              team={home}\n              score={homeScore}\n              showScore={showScore}\n              showRedCards={showRedCards}\n              redCardPosition={redCardPosition}\n              live={isLive}\n              dimmed={homeLost}\n            />\n            <TeamRow\n              team={away}\n              score={awayScore}\n              showScore={showScore}\n              showRedCards={showRedCards}\n              redCardPosition={redCardPosition}\n              live={isLive}\n              dimmed={awayLost}\n            />\n          </div>\n        </div>\n        {showOddsSlot ? (\n          <div\n            className={cn(\n              \"border-t\",\n              wantsRow && \"@lg:w-64 @lg:shrink-0 @lg:border-t-0 @lg:border-l\"\n            )}\n          >\n            {oddsCard}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/mrdoge-ui/odds-selector/odds-selector.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { ChevronDown, LayoutGrid, SlidersHorizontal } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\nexport type OddsMovement = \"up\" | \"down\" | \"flat\"\n\nexport interface OddsOption {\n  id: string\n  /** Selection label, e.g. \"1\", \"X\", \"2\" or a team name / \"Draw\". */\n  label: string\n  /** Formatted price, e.g. \"1.85\". */\n  price: string\n  movement?: OddsMovement\n  suspended?: boolean\n}\n\n/**\n * One market rendered as a two-column row, e.g. Over/Under at a single\n * goal threshold. Use `lines` (a market per row) instead of `options` (a\n * flat row of a single market) for bet types that post one market per\n * line: see the \"Multiple Lines\" example.\n */\nexport interface OddsLine {\n  id: string\n  /** Short label for the line itself, e.g. \"2.5\". Shown above the slider in slider view. Omit if there's no natural short label. */\n  label?: string\n  over: OddsOption\n  under: OddsOption\n}\n\nexport interface OddsSelectorProps {\n  /** Market name shown as a header above the options, e.g. \"Match Result\". Omit to render just the options. */\n  label?: string\n  /** A single market's selections, laid out in one row. Ignored when `lines` is set. */\n  options?: OddsOption[]\n  /** Id of the currently selected option, for `options` mode. */\n  selectedId?: string\n  /** Called with the pressed option's id, or `undefined` when pressing the already-selected option deselects it. For `options` mode. */\n  onSelect?: (id: string | undefined) => void\n  /** Several markets, one per row, e.g. every Over/Under goal threshold stacked in one card. Takes over `options` when set. */\n  lines?: OddsLine[]\n  /** Ids of every currently selected option across `lines`. Unlike `options`, more than one row can be selected at once. */\n  selectedLineIds?: string[]\n  /** Called with a pressed option's id and the resulting selected state (not a bare toggle), for `lines` mode. */\n  onSelectLine?: (id: string, selected: boolean) => void\n  /**\n   * Ids that render as unavailable, e.g. because they're logically\n   * incompatible with the current selection. Applies to both `options`\n   * and `lines`. Computed externally (e.g. via a conflict adapter) and\n   * passed in; OddsSelector never decides this itself. Unlike\n   * `suspended`, a disabled option still shows its real price: it's\n   * fully priced and available, just not currently combinable.\n   */\n  disabledIds?: string[]\n  /** Adds a header toggle to browse `lines` one at a time via a slider instead of a stacked list. No effect with fewer than 2 lines. */\n  enableSliderView?: boolean\n  /** Adds a header toggle to collapse the selector down to just its header. Requires `label` (or `lines`/`enableSliderView`) to have a header to put it in. */\n  collapsible?: boolean\n  /**\n   * \"card\" (default) renders its own border/background, for standalone\n   * use. \"bare\" drops that chrome, for embedding inside another card\n   * (e.g. Match Card's odds row) without a card-in-a-card look.\n   */\n  variant?: \"card\" | \"bare\"\n  className?: string\n}\n\n// A price shortening (going down) means the market now thinks the outcome\n// is more likely: the conventional \"hot\" direction in real odds boards,\n// shown green. Drifting (going up) means the opposite, shown red. Same\n// mechanical rule regardless of market type.\nconst movementColor: Record<OddsMovement, string> = {\n  up: \"text-destructive\",\n  down: \"text-emerald-600 dark:text-emerald-500\",\n  flat: \"\",\n}\n\nfunction OddsOptionButton({\n  option,\n  selected,\n  disabled,\n  onClick,\n  layout,\n}: {\n  option: OddsOption\n  selected: boolean\n  /** Conflict-disabled, distinct from `option.suspended`: still shows the real price. */\n  disabled?: boolean\n  onClick: () => void\n  layout: \"column\" | \"row\"\n}) {\n  const inactive = option.suspended || disabled\n  return (\n    <button\n      type=\"button\"\n      disabled={inactive}\n      onClick={(e) => {\n        e.stopPropagation()\n        onClick()\n      }}\n      className={cn(\n        \"flex min-w-0 cursor-pointer\",\n        layout === \"column\"\n          ? \"flex-col items-center justify-center gap-0.5 px-2 py-2.5\"\n          : \"items-center justify-between gap-2 px-3 py-2.5 text-left\",\n        selected ? \"bg-primary text-primary-foreground\" : \"hover:bg-accent/50\",\n        inactive && \"pointer-events-none opacity-50\"\n      )}\n    >\n      <span\n        className={cn(\n          \"truncate text-xs\",\n          layout === \"column\" && \"max-w-full\",\n          selected ? \"text-primary-foreground/70\" : \"text-muted-foreground\"\n        )}\n      >\n        {option.label}\n      </span>\n      <span\n        className={cn(\n          \"text-sm font-semibold\",\n          !selected && !option.suspended && option.movement && movementColor[option.movement]\n        )}\n      >\n        {option.suspended ? \"—\" : option.price}\n      </span>\n    </button>\n  )\n}\n\nfunction OddsLineRow({\n  line,\n  selectedIds,\n  disabledIds,\n  onToggle,\n}: {\n  line: OddsLine\n  selectedIds: string[]\n  disabledIds?: string[]\n  onToggle: (id: string, selected: boolean) => void\n}) {\n  return (\n    <div className=\"grid grid-cols-2 divide-x\">\n      {[line.over, line.under].map((option) => {\n        const selected = selectedIds.includes(option.id)\n        return (\n          <OddsOptionButton\n            key={option.id}\n            option={option}\n            layout=\"row\"\n            selected={selected}\n            disabled={disabledIds?.includes(option.id)}\n            onClick={() => onToggle(option.id, !selected)}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\n// Starts the slider on the line closest to an even split (e.g. 1.90/2.00)\n// rather than always the first/lowest threshold, which is often the most\n// lopsided line (e.g. 1.17/4.45).\nfunction mostBalancedIndex(lines: OddsLine[]): number {\n  let bestIndex = 0\n  let bestSpread = Infinity\n  lines.forEach((line, i) => {\n    const spread = Math.abs(parseFloat(line.over.price) - parseFloat(line.under.price))\n    if (spread < bestSpread) {\n      bestIndex = i\n      bestSpread = spread\n    }\n  })\n  return bestIndex\n}\n\nfunction OddsLinesSlider({\n  lines,\n  selectedIds,\n  disabledIds,\n  onSelectLine,\n}: {\n  lines: OddsLine[]\n  selectedIds: string[]\n  disabledIds?: string[]\n  onSelectLine?: (id: string, selected: boolean) => void\n}) {\n  const [index, setIndex] = useState(() => mostBalancedIndex(lines))\n  // Lines can arrive/change size as live data updates; clamp rather than\n  // point past the end.\n  const currentIndex = Math.min(index, lines.length - 1)\n  const line = lines[currentIndex]\n\n  return (\n    <div className=\"flex flex-col\">\n      <OddsLineRow\n        line={line}\n        selectedIds={selectedIds}\n        disabledIds={disabledIds}\n        onToggle={(id, selected) => onSelectLine?.(id, selected)}\n      />\n      <div className=\"flex flex-col items-center gap-1 border-t px-3 py-3\">\n        {line.label ? (\n          <span className=\"text-sm font-semibold text-card-foreground\">{line.label}</span>\n        ) : null}\n        <input\n          type=\"range\"\n          min={0}\n          max={lines.length - 1}\n          step={1}\n          value={currentIndex}\n          onChange={(e) => setIndex(Number(e.target.value))}\n          className=\"w-full cursor-pointer accent-primary\"\n          aria-label=\"Select line\"\n        />\n      </div>\n    </div>\n  )\n}\n\nexport function OddsSelector({\n  label,\n  options = [],\n  selectedId,\n  onSelect,\n  lines,\n  selectedLineIds = [],\n  onSelectLine,\n  disabledIds,\n  enableSliderView = false,\n  collapsible = false,\n  variant = \"card\",\n  className,\n}: OddsSelectorProps) {\n  const [collapsed, setCollapsed] = useState(false)\n  const [linesView, setLinesView] = useState<\"list\" | \"slider\">(\"list\")\n  const showLinesToggle = Boolean(lines && lines.length > 1 && enableSliderView)\n  const showHeader = Boolean(label) || showLinesToggle || collapsible\n\n  return (\n    <div\n      className={cn(\n        \"flex h-full flex-col overflow-hidden\",\n        variant === \"card\" && \"rounded-xl border bg-card\",\n        className\n      )}\n    >\n      {showHeader ? (\n        <div\n          className={cn(\n            \"flex h-10 items-center justify-between gap-2 py-2 pl-3 pr-2 border-b\",\n            collapsed && \"border-b-transparent\"\n          )}\n        >\n          <span className=\"truncate text-sm font-medium text-card-foreground\">{label}</span>\n          <div className=\"flex items-center gap-1\">\n            {showLinesToggle ? (\n              <div className=\"flex items-center gap-1 rounded-md border p-0.5\">\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"icon-xs\"\n                  aria-label=\"List view\"\n                  aria-pressed={linesView === \"list\"}\n                  onClick={() => setLinesView(\"list\")}\n                  className={cn(\n                    \"rounded\",\n                    linesView === \"list\"\n                      ? \"bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground\"\n                      : \"text-muted-foreground hover:text-foreground\"\n                  )}\n                >\n                  <LayoutGrid className=\"size-3.5\" />\n                </Button>\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"icon-xs\"\n                  aria-label=\"Slider view\"\n                  aria-pressed={linesView === \"slider\"}\n                  onClick={() => setLinesView(\"slider\")}\n                  className={cn(\n                    \"rounded\",\n                    linesView === \"slider\"\n                      ? \"bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground\"\n                      : \"text-muted-foreground hover:text-foreground\"\n                  )}\n                >\n                  <SlidersHorizontal className=\"size-3.5\" />\n                </Button>\n              </div>\n            ) : null}\n            {collapsible ? (\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-xs\"\n                aria-label={collapsed ? \"Expand\" : \"Collapse\"}\n                aria-expanded={!collapsed}\n                onClick={() => setCollapsed((c) => !c)}\n                className=\"rounded text-muted-foreground hover:text-foreground\"\n              >\n                <ChevronDown className={cn(\"size-3.5 transition-transform\", collapsed && \"-rotate-90\")} />\n              </Button>\n            ) : null}\n          </div>\n        </div>\n      ) : null}\n      {!collapsed &&\n        (lines ? (\n          showLinesToggle && linesView === \"slider\" ? (\n            <OddsLinesSlider\n              lines={lines}\n              selectedIds={selectedLineIds}\n              disabledIds={disabledIds}\n              onSelectLine={onSelectLine}\n            />\n          ) : (\n            <div className=\"divide-y\">\n              {lines.map((line) => (\n                <OddsLineRow\n                  key={line.id}\n                  line={line}\n                  selectedIds={selectedLineIds}\n                  disabledIds={disabledIds}\n                  onToggle={(id, selected) => onSelectLine?.(id, selected)}\n                />\n              ))}\n            </div>\n          )\n        ) : (\n          <div className=\"grid flex-1 auto-cols-fr grid-flow-col divide-x\">\n            {options.map((option) => {\n              const selected = option.id === selectedId\n              return (\n                <OddsOptionButton\n                  key={option.id}\n                  option={option}\n                  layout=\"column\"\n                  selected={selected}\n                  disabled={disabledIds?.includes(option.id)}\n                  onClick={() => onSelect?.(selected ? undefined : option.id)}\n                />\n              )\n            })}\n          </div>\n        ))}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/mrdoge-ui/entity-image/entity-image.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface EntityImageProps {\n  src?: string | null\n  /** Used for the alt text and the initials fallback. */\n  name: string\n  size?: \"sm\" | \"default\" | \"lg\"\n  className?: string\n}\n\nconst sizeClass: Record<NonNullable<EntityImageProps[\"size\"]>, string> = {\n  sm: \"size-5\",\n  default: \"size-8\",\n  lg: \"size-10\",\n}\n\n/**\n * Just the image: no background, border, or corner radius, so team crests\n * and region flags render at their native shape. Pass a `className` (e.g.\n * `size-6`) to override the size; it takes precedence over `size`.\n */\nexport function EntityImage({ src, name, size = \"default\", className }: EntityImageProps) {\n  const [errored, setErrored] = useState(false)\n\n  if (!src || errored) {\n    return (\n      <span\n        className={cn(\n          \"flex shrink-0 items-center justify-center rounded-full bg-muted text-[0.6rem] font-medium text-muted-foreground\",\n          sizeClass[size],\n          className\n        )}\n      >\n        {name.slice(0, 2).toUpperCase()}\n      </span>\n    )\n  }\n\n  return (\n    // eslint-disable-next-line @next/next/no-img-element -- framework-agnostic registry component, no next/image dependency\n    <img\n      src={src}\n      alt={name}\n      className={cn(\"shrink-0 object-contain\", sizeClass[size], className)}\n      onError={() => setErrored(true)}\n    />\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}