{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mrdoge-match-timeline-adapter",
  "title": "Match Timeline Adapter (Mr. Doge SDK)",
  "description": "Maps a real match's timeline events onto Match Timeline's entries prop.",
  "dependencies": [
    "@mrdoge/protocol"
  ],
  "registryDependencies": [
    "match-timeline"
  ],
  "files": [
    {
      "path": "lib/mrdoge-adapters/match-timeline.ts",
      "content": "import type { MatchDetail, TimelineEvent } from \"@mrdoge/protocol\"\nimport type { MatchTimelineEntry, MatchTimelineProps } from \"@/registry/mrdoge-ui/match-timeline/match-timeline\"\n\n// Soccer only. classify() and the phase labels below use soccer's own\n// event/phase sysnames. Other sports just get zero entries for now.\n\n// Matched by prefix. Lower-tier competitions report goals/cards without\n// a named player (\"GoalWithoutScorer\" vs \"GoalWithScorer\"). \"GoalWith\"\n// specifically, not \"GoalKick\" (a real, different event).\nfunction classify(type: string): \"goal\" | \"own-goal\" | \"yellow-card\" | \"red-card\" | \"penalty\" | null {\n  if (type === \"OwnGoal\") return \"own-goal\"\n  if (type.startsWith(\"GoalWith\")) return \"goal\"\n  if (type.startsWith(\"YellowCard\")) return \"yellow-card\"\n  if (type.startsWith(\"RedCard\")) return \"red-card\"\n  if (type === \"PenaltyKick\") return \"penalty\"\n  return null\n}\n\nfunction toTimeLabel(event: TimelineEvent): string | undefined {\n  return event.captions[0] ? `${event.captions[0]}'` : undefined\n}\n\n// clock.phase/display/displayLong only update on backend event triggers\n// and can go stale. StartOfSecondHalf (tracked below) is the reliable\n// \"2nd half started\" signal; elapsedSeconds is only a last-resort\n// fallback for matches with no events to check at all.\nconst SOCCER_PHASE_LABEL: Record<string, string> = {\n  SOCCER_MATCH_FIRST_HALF: \"1st Half\",\n  SOCCER_MATCH_SECOND_HALF: \"2nd Half\",\n  SOCCER_MATCH_EXTRA_FIRST_HALF: \"Extra Time - 1st Half\",\n  SOCCER_MATCH_EXTRA_SECOND_HALF: \"Extra Time - 2nd Half\",\n  SOCCER_MATCH_PENALTIES: \"Penalties\",\n}\n// Only reached when there's no StartOfSecondHalf event to trust.\n// Deliberately wide, since real 1st-half stoppage rarely approaches it.\nconst FIRST_HALF_ELAPSED_CUTOFF_SECONDS = 70 * 60\nconst SECOND_HALF_ELAPSED_CEILING_SECONDS = 115 * 60\n\nfunction phaseLabelFromElapsedSeconds(elapsedSeconds: number | null | undefined): string | undefined {\n  if (elapsedSeconds == null) return undefined\n  if (elapsedSeconds <= FIRST_HALF_ELAPSED_CUTOFF_SECONDS) return \"1st Half\"\n  if (elapsedSeconds <= SECOND_HALF_ELAPSED_CEILING_SECONDS) return \"2nd Half\"\n  return undefined\n}\n\ntype ClockLike = {\n  phase: string | null\n  state?: string | null\n  display: string | null\n  displayLong: string | null\n  elapsedSeconds?: number | null\n} | null | undefined\n\nfunction toLivePhaseLabel(clock: ClockLike, secondHalfStarted: boolean): string {\n  // elapsedSeconds is null during intermission. Trust the backend's\n  // own \"Half Time\" label instead of guessing from minutes.\n  if (clock?.state === \"intermission\") return clock.displayLong ?? clock.display ?? \"Half Time\"\n\n  if (secondHalfStarted) {\n    // Confirmed via a real event: elapsed time only decides whether\n    // we've since moved into extra time.\n    if (clock?.elapsedSeconds != null && clock.elapsedSeconds > SECOND_HALF_ELAPSED_CEILING_SECONDS) {\n      return SOCCER_PHASE_LABEL[clock?.phase ?? \"\"] ?? clock?.displayLong ?? clock?.display ?? \"2nd Half\"\n    }\n    return \"2nd Half\"\n  }\n\n  return (\n    phaseLabelFromElapsedSeconds(clock?.elapsedSeconds) ??\n    SOCCER_PHASE_LABEL[clock?.phase ?? \"\"] ??\n    clock?.displayLong ??\n    clock?.display ??\n    \"Live\"\n  )\n}\n\nfunction isLivePastFirstHalf(clock: ClockLike, secondHalfStarted: boolean): boolean {\n  if (clock?.state === \"intermission\" || secondHalfStarted) return true\n  return clock?.elapsedSeconds != null && clock.elapsedSeconds > FIRST_HALF_ELAPSED_CUTOFF_SECONDS\n}\n\n// Per-event minute, parsed from captions[0] (\"41\", \"45+3\"): a fixed\n// value recorded at the time, not subject to the clock staleness above.\nfunction parseEventMinute(caption: string | undefined): number | null {\n  if (!caption) return null\n  const parsed = parseInt(caption, 10)\n  return Number.isNaN(parsed) ? null : parsed\n}\n\nfunction isEventPastFirstHalf(minute: number | null): boolean {\n  return minute != null && minute > 45\n}\n\n/**\n * Maps a real `matches.get()`/`matches.subscribe()` response to Match\n * Timeline's props: filtered to goals, cards, penalties, and\n * half/full-time, most-recent-first. If the SDK's shape changes, this\n * fails to compile.\n */\nexport function matchToMatchTimelineProps(match: MatchDetail): MatchTimelineProps {\n  const entries: MatchTimelineEntry[] = []\n  let homeScore = 0\n  let awayScore = 0\n  // Fallback HT score for when no EndOfFirstHalf event arrives.\n  let htHomeScore = 0\n  let htAwayScore = 0\n  let halftimeMarked = false\n  let secondHalfStarted = false\n\n  for (const event of match.timeline ?? []) {\n    const kind = classify(event.type)\n\n    if (event.type === \"StartOfSecondHalf\") secondHalfStarted = true\n\n    if (kind === \"goal\" || kind === \"own-goal\") {\n      // An own goal credited to `side` benefits the opposite side's score.\n      const scoringSide = kind === \"own-goal\" ? (event.side === \"home\" ? \"away\" : \"home\") : event.side\n      if (scoringSide === \"home\") homeScore++\n      else if (scoringSide === \"away\") awayScore++\n    }\n\n    if (event.type === \"EndOfFirstHalf\") {\n      const [htHome, htAway] = event.captions\n      entries.push({\n        id: `${entries.length}`,\n        side: \"match\",\n        type: \"divider\",\n        description: `HT ${htHome} - ${htAway}`,\n      })\n      halftimeMarked = true\n      continue\n    }\n\n    if (!isEventPastFirstHalf(parseEventMinute(event.captions[0]))) {\n      htHomeScore = homeScore\n      htAwayScore = awayScore\n    }\n\n    if (!kind) continue\n\n    const [, team, player] = event.captions\n    const base = {\n      id: `${entries.length}`,\n      time: toTimeLabel(event),\n      type: kind,\n      side: event.side as \"home\" | \"away\",\n    }\n\n    if (kind === \"goal\" || kind === \"own-goal\") {\n      entries.push({ ...base, description: player ?? team, score: { home: homeScore, away: awayScore } })\n    } else if (kind === \"penalty\") {\n      entries.push({ ...base, description: team })\n    } else {\n      entries.push({ ...base, description: player ?? team })\n    }\n  }\n\n  // Fallback HT marker once the match is clearly past the first half.\n  if (\n    !halftimeMarked &&\n    (isLivePastFirstHalf(match.stats?.clock, secondHalfStarted) || match.status === \"completed\")\n  ) {\n    entries.push({\n      id: `${entries.length}`,\n      side: \"match\",\n      type: \"divider\",\n      description: `HT ${htHomeScore} - ${htAwayScore}`,\n    })\n  }\n\n  entries.reverse()\n\n  // During intermission, the HT divider above already says exactly this.\n  // A second \"Half Time\" banner here would just repeat it.\n  if (match.status === \"live\" && match.stats?.clock?.state !== \"intermission\") {\n    entries.unshift({\n      id: \"live\",\n      side: \"match\",\n      type: \"divider\",\n      live: true,\n      description: `${toLivePhaseLabel(match.stats?.clock, secondHalfStarted)} ${match.stats?.homeScore ?? 0} - ${match.stats?.awayScore ?? 0}`,\n    })\n  } else if (match.status === \"completed\") {\n    entries.unshift({\n      id: \"ft\",\n      side: \"match\",\n      type: \"divider\",\n      description: `FT ${match.stats?.homeScore ?? 0} - ${match.stats?.awayScore ?? 0}`,\n    })\n  }\n\n  return { entries }\n}\n",
      "type": "registry:lib",
      "target": "lib/mrdoge-adapters/match-timeline.ts"
    }
  ],
  "type": "registry:lib"
}