{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "template-podcast",
  "title": "The Long Table: podcast",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A warm five-page podcast site built on a new audio player: a home with a latest-episode hero and a mini player, an episodes archive, an episode page with chapters and a transcript that follows playback, hosts and subscribe.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/audio-player.json",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-fonts.ts",
      "content": "// Ballmac UI: Podcast template fonts. https://ui.ballmac.com/templates/template-podcast\nimport { Nunito_Sans, Young_Serif } from \"next/font/google\"\n\nexport const podcastDisplay = Young_Serif({ variable: \"--podcast-display\", subsets: [\"latin\"], weight: \"400\", display: \"swap\", preload: false })\nexport const podcastSans = Nunito_Sans({ variable: \"--podcast-sans\", subsets: [\"latin\"], display: \"swap\", preload: false })\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-fonts.ts"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-data.ts",
      "content": "import type { MediaSource } from \"@/components/ballmac/media\"\n\n// Ballmac UI: Podcast sample content. https://ui.ballmac.com/templates/template-podcast\n\nexport type TranscriptLine = { t: number; who: string; text: string }\nexport type Episode = {\n  n: number\n  slug: string\n  title: string\n  guest: string\n  guestRole: string\n  date: string\n  /** Length in seconds. */\n  duration: number\n  season: 1 | 2 | 3\n  summary: string\n  art: number\n  /** Your own image instead of the painted art: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. */\n  image?: MediaSource\n  /** Describes `image` when it is a plain URL. */\n  imageAlt?: string\n  chapters: { start: number; title: string }[]\n}\n\nexport const episodes: Episode[] = [\n  { n: 42, slug: \"the-optimised-life\", title: \"The Optimised Life\", guest: \"Amara Okafor\", guestRole: \"Writer and technologist\", date: \"2026-09-29\", duration: 2460, season: 3, summary: \"A year without metrics, and what was left when she stopped counting. Plus: why the best dinner party has no agenda.\", art: 0, chapters: [{ start: 0, title: \"Cold open\" }, { start: 95, title: \"Why we stopped measuring\" }, { start: 612, title: \"The spreadsheet that ate a year\" }, { start: 1480, title: \"What replaced it\" }, { start: 2210, title: \"Listener questions\" }] },\n  { n: 41, slug: \"bread-and-patience\", title: \"Bread and Patience\", guest: \"Tomás Reyes\", guestRole: \"Baker\", date: \"2026-09-15\", duration: 2920, season: 3, summary: \"A third-generation baker on sourdough, sleep and why nothing good happens quickly.\", art: 1, chapters: [{ start: 0, title: \"Welcome\" }, { start: 300, title: \"The starter\" }, { start: 1500, title: \"Night shifts\" }] },\n  { n: 40, slug: \"the-unfinished-city\", title: \"The Unfinished City\", guest: \"Inês Marques\", guestRole: \"Editor\", date: \"2026-09-01\", duration: 2210, season: 3, summary: \"Scaffolding, cranes and the case for cities that are never done.\", art: 2, chapters: [{ start: 0, title: \"Welcome\" }, { start: 410, title: \"Rua da Madalena\" }] },\n  { n: 39, slug: \"what-the-radio-knew\", title: \"What the Radio Knew\", guest: \"Elena Rossi\", guestRole: \"Culture editor\", date: \"2026-08-18\", duration: 1980, season: 3, summary: \"Why the old shipping forecast still feels like poetry.\", art: 3, chapters: [{ start: 0, title: \"Welcome\" }, { start: 600, title: \"The forecast\" }] },\n  { n: 38, slug: \"a-year-offline\", title: \"A Year Offline\", guest: \"Dev Patel\", guestRole: \"Engineer\", date: \"2026-08-04\", duration: 3120, season: 3, summary: \"He deleted every feed and kept the group chats. Here is what changed.\", art: 4, chapters: [{ start: 0, title: \"Welcome\" }, { start: 800, title: \"Week three\" }] },\n  { n: 37, slug: \"small-kitchens\", title: \"Small Kitchens\", guest: \"Mei Tanaka\", guestRole: \"Chef\", date: \"2026-07-21\", duration: 2640, season: 2, summary: \"Cooking for twelve on two burners, and why constraints taste good.\", art: 5, chapters: [{ start: 0, title: \"Welcome\" }, { start: 520, title: \"Two burners\" }] },\n  { n: 36, slug: \"the-long-way-home\", title: \"The Long Way Home\", guest: \"Sasha Petrova\", guestRole: \"Travel writer\", date: \"2026-07-07\", duration: 2750, season: 2, summary: \"Trains, ferries and the pleasure of arriving slowly.\", art: 0, chapters: [{ start: 0, title: \"Welcome\" }] },\n  { n: 35, slug: \"on-listening\", title: \"On Listening\", guest: \"Marcus Lindqvist\", guestRole: \"Therapist\", date: \"2026-06-23\", duration: 2300, season: 2, summary: \"What a good listener actually does, and how to become one.\", art: 2, chapters: [{ start: 0, title: \"Welcome\" }] },\n]\n\nexport const transcript: TranscriptLine[] = [\n  { t: 0, who: \"Nora\", text: \"Welcome to The Long Table, the show where we sit down for a long dinner and talk about whatever comes up.\" },\n  { t: 14, who: \"Sam\", text: \"Tonight it's Amara Okafor, who spent a year without measuring anything. No steps, no calories, no inbox zero.\" },\n  { t: 31, who: \"Nora\", text: \"Which sounds either wonderful or like a recipe for disaster.\" },\n  { t: 40, who: \"Amara\", text: \"Both, honestly. The first month I felt like I'd lost my map. I kept reaching for numbers the way you reach for a phone you've left in another room.\" },\n  { t: 63, who: \"Sam\", text: \"What were you actually measuring before? Walk us through a normal day.\" },\n  { t: 76, who: \"Amara\", text: \"Everything. Sleep, steps, tasks closed, words written, minutes meditated. I had a dashboard for my own life and it had a streak counter.\" },\n  { t: 99, who: \"Nora\", text: \"A streak counter for meditating. That's the part that gets me.\" },\n  { t: 108, who: \"Amara\", text: \"I know. I once meditated in a taxi to keep a streak alive. I wasn't calm. I was anxious about the streak.\" },\n  { t: 126, who: \"Sam\", text: \"So what made you stop?\" },\n  { t: 132, who: \"Amara\", text: \"A very ordinary Tuesday. I realised I couldn't remember anything about the week except the numbers.\" },\n]\n\nexport const getEpisode = (slug: string) => episodes.find((e) => e.slug === slug) ?? episodes[0]\nconst fmt = new Intl.DateTimeFormat(\"en-US\", { month: \"long\", day: \"numeric\", year: \"numeric\", timeZone: \"UTC\" })\nexport const formatDate = (iso: string) => fmt.format(new Date(iso))\nexport const minutes = (s: number) => `${Math.round(s / 60)} min`\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-data.ts"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-theme.tsx",
      "content": "// Ballmac UI: Podcast template shell. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { Menu, Rss, X } from \"lucide-react\"\n\nimport { AudioPlayer } from \"@/components/ballmac/audio-player\"\nimport { type Episode } from \"@/components/ballmac/templates/podcast/podcast-data\"\nimport { podcastDisplay, podcastSans } from \"@/components/ballmac/templates/podcast/podcast-fonts\"\nimport { cn } from \"@/lib/utils\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype PodcastPage = \"home\" | \"episodes\" | \"episode\" | \"hosts\" | \"subscribe\"\ntype PodcastHrefs = Record<PodcastPage, string>\n\nconst defaultHrefs: PodcastHrefs = { home: \"/podcast\", episodes: \"/podcast/episodes\", episode: \"/podcast/episodes/the-optimised-life\", hosts: \"/podcast/hosts\", subscribe: \"/podcast/subscribe\" }\n\n/** The Long Table's palette: warm cream and aubergine ink with amber candlelight. Dark mode is the same table after dark. */\nconst podcastCss = `\n.podcast-theme,body:not(:has([data-gallery])):has(.podcast-theme){--background:oklch(0.972 0.02 70);--foreground:oklch(0.24 0.06 340);--card:oklch(0.99 0.012 70);--card-foreground:oklch(0.24 0.06 340);--popover:oklch(0.99 0.012 70);--popover-foreground:oklch(0.24 0.06 340);--primary:oklch(0.3 0.08 340);--primary-foreground:oklch(0.975 0.02 70);--secondary:oklch(0.945 0.03 68);--secondary-foreground:oklch(0.24 0.06 340);--muted:oklch(0.945 0.03 68);--muted-foreground:oklch(0.45 0.05 340);--accent:oklch(0.93 0.04 66);--accent-foreground:oklch(0.24 0.06 340);--border:oklch(0.24 0.06 340 / 14%);--input:oklch(0.24 0.06 340 / 22%);--ring:oklch(0.45 0.16 345);--surface:oklch(0.955 0.025 68);--destructive:oklch(0.52 0.21 27);--chart-1:oklch(0.78 0.16 70);--chart-2:oklch(0.45 0.16 345);--chart-3:oklch(0.55 0.1 160);--chart-4:oklch(0.68 0.15 35);--chart-5:oklch(0.42 0.1 280);--podcast-on-amber:oklch(0.24 0.06 340);--radius:1rem}\n.dark .podcast-theme,.dark body:not(:has([data-gallery])):has(.podcast-theme){--background:oklch(0.18 0.04 335);--foreground:oklch(0.95 0.025 70);--card:oklch(0.22 0.045 335);--card-foreground:oklch(0.95 0.025 70);--popover:oklch(0.24 0.047 335);--popover-foreground:oklch(0.95 0.025 70);--primary:oklch(0.95 0.025 70);--primary-foreground:oklch(0.22 0.05 340);--secondary:oklch(0.27 0.05 335);--secondary-foreground:oklch(0.95 0.025 70);--muted:oklch(0.26 0.05 335);--muted-foreground:oklch(0.74 0.04 60);--accent:oklch(0.3 0.055 335);--accent-foreground:oklch(0.95 0.025 70);--border:oklch(1 0 0 / 11%);--input:oklch(1 0 0 / 16%);--ring:oklch(0.78 0.16 70);--surface:oklch(0.2 0.042 335);--destructive:oklch(0.7 0.19 27);--chart-1:oklch(0.8 0.16 72);--chart-2:oklch(0.76 0.13 350);--chart-3:oklch(0.78 0.11 160);--chart-4:oklch(0.74 0.14 38);--chart-5:oklch(0.74 0.1 285)}\nbody:not(:has([data-gallery])):has(.podcast-theme){font-family:var(--podcast-sans),ui-sans-serif,system-ui,sans-serif}\n`\n\nconst display = \"[font-family:var(--podcast-display),ui-serif,Georgia,serif] font-normal tracking-[-0.01em]\"\n\n/** Cover art: a table seen from above, in the show's colours. */\nfunction ShowArt({ variant, className, image, imageAlt }: { variant: number; className?: string; image?: MediaSource; imageAlt?: string }) {\n  const v = ((variant % 6) + 6) % 6\n  if (image) return <Media media={image} alt={imageAlt} fill className={cn(\"aspect-square w-full\", className)} />\n  const bg = [\"bg-chart-2\", \"bg-chart-1\", \"bg-chart-4\", \"bg-chart-3\", \"bg-chart-5\", \"bg-primary\"][v]\n  return (\n    <div aria-hidden=\"true\" className={cn(\"@container relative isolate aspect-square w-full overflow-hidden\", bg, className)}>\n      <div className=\"bg-card/90 absolute top-1/2 left-1/2 size-[62%] -translate-x-1/2 -translate-y-1/2 rounded-full\" />\n      <div className=\"border-foreground/30 absolute top-1/2 left-1/2 size-[44%] -translate-x-1/2 -translate-y-1/2 rounded-full border-2\" />\n      <div className=\"bg-chart-1 absolute top-[10%] start-[12%] size-[14%] rounded-full\" />\n      <div className=\"bg-foreground/80 absolute end-[10%] bottom-[12%] h-[7%] w-[34%] rounded-full\" style={{ transform: `rotate(${-20 + v * 12}deg)` }} />\n      <div className=\"bg-foreground/80 absolute bottom-[12%] start-[10%] h-[30%] w-[3%] rounded-full\" />\n      <span className={cn(\"text-foreground absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-[30cqw] leading-none\", display)}>{[\"42\", \"41\", \"40\", \"39\", \"38\", \"37\"][v]}</span>\n    </div>\n  )\n}\n\ntype Ctx = { now: Episode | null; play: (e: Episode) => void }\nconst PodcastContext = React.createContext<Ctx>({ now: null, play: () => {} })\n/** The episode in the mini player and a way to start one. Use it inside a PodcastShell. */\nfunction usePodcast() {\n  return React.useContext(PodcastContext)\n}\n\nconst links: { key: PodcastPage; label: string }[] = [\n  { key: \"episodes\", label: \"Episodes\" },\n  { key: \"hosts\", label: \"Hosts\" },\n  { key: \"subscribe\", label: \"Subscribe\" },\n]\n\ntype PodcastShellProps = React.ComponentProps<\"div\"> & {\n  /** The page being shown, so its nav link is marked current. */\n  page: PodcastPage\n  /** Override where pages live (used by previews). */\n  hrefs?: Partial<PodcastHrefs>\n}\n\n/** The Long Table's frame: a warm header, a footer with platforms and a mini player that appears when you press play on a list. */\nfunction PodcastShell({ page, hrefs: overrides, className, style, children, ...props }: PodcastShellProps) {\n  const hrefs = { ...defaultHrefs, ...overrides }\n  const [open, setOpen] = React.useState(false)\n  const [now, setNow] = React.useState<Episode | null>(null)\n  React.useEffect(() => {\n    const classes = [podcastDisplay.variable, podcastSans.variable].filter(Boolean)\n    document.body.classList.add(...classes)\n    return () => document.body.classList.remove(...classes)\n  }, [])\n  const active = page === \"episode\" ? \"episodes\" : page\n  const ctx = React.useMemo<Ctx>(() => ({ now, play: setNow }), [now])\n  return (\n    <PodcastContext.Provider value={ctx}>\n      <div\n        data-slot=\"podcast\"\n        className={cn(\"podcast-theme bg-background text-foreground relative min-h-dvh overflow-x-clip\", now && \"pb-32\", podcastDisplay.variable, podcastSans.variable, className)}\n        style={{ fontFamily: \"var(--podcast-sans), ui-sans-serif, system-ui, sans-serif\", ...style }}\n        {...props}\n      >\n        <style>{podcastCss}</style>\n        <header className=\"bg-background/85 sticky top-0 z-40 border-b backdrop-blur-xl\">\n          <div className=\"mx-auto flex h-16 max-w-6xl items-center justify-between px-4 sm:px-6\">\n            <a href={hrefs.home} className=\"focus-visible:ring-ring/50 flex items-center gap-3 rounded-lg outline-none focus-visible:ring-[3px]\">\n              <span className=\"bg-chart-1 flex size-10 items-center justify-center rounded-full\" aria-hidden=\"true\"><span className=\"border-[var(--podcast-on-amber)] size-5 rounded-full border-[3px]\" /></span>\n              <span className={cn(\"text-xl\", display)}>The Long Table</span>\n            </a>\n            <nav aria-label=\"Main\" className=\"hidden items-center gap-1 md:flex\">\n              {links.map((l) => <a key={l.key} href={hrefs[l.key]} aria-current={active === l.key ? \"page\" : undefined} className=\"hover:bg-accent aria-[current=page]:bg-accent focus-visible:ring-ring/50 rounded-full px-4 py-2 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]\">{l.label}</a>)}\n              <a href={hrefs.subscribe} className=\"bg-chart-1 ms-2 inline-flex h-10 items-center gap-2 rounded-full px-5 text-sm font-bold text-[var(--podcast-on-amber)] outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none\"><Rss className=\"size-4\" aria-hidden=\"true\" />Follow</a>\n            </nav>\n            <button type=\"button\" aria-label={open ? \"Close menu\" : \"Open menu\"} aria-expanded={open} aria-controls=\"podcast-mobile-menu\" onClick={() => setOpen((v) => !v)} className=\"hover:bg-accent focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-full outline-none focus-visible:ring-[3px] md:hidden\">{open ? <X className=\"size-5\" aria-hidden=\"true\" /> : <Menu className=\"size-5\" aria-hidden=\"true\" />}</button>\n          </div>\n          {open && <nav id=\"podcast-mobile-menu\" aria-label=\"Mobile\" className=\"px-4 pb-4 md:hidden\">{links.map((l) => <a key={l.key} href={hrefs[l.key]} className=\"hover:bg-accent block rounded-2xl px-4 py-3 text-lg font-semibold\">{l.label}</a>)}</nav>}\n        </header>\n        {children}\n        <footer className=\"bg-primary text-primary-foreground mt-24\">\n          <div className=\"mx-auto grid max-w-6xl gap-10 px-4 py-14 sm:px-6 md:grid-cols-[1.4fr_1fr_1fr]\">\n            <div><p className={cn(\"text-3xl\", display)}>The Long Table</p><p className=\"mt-3 max-w-xs opacity-80 text-pretty\">A long dinner and a good conversation, every other Tuesday. Hosted by Nora Vale and Sam Okoye.</p></div>\n            <div><h2 className=\"text-xs font-bold tracking-widest uppercase opacity-70\">Listen</h2><ul className=\"mt-4 space-y-2.5\">{[\"Apple Podcasts\", \"Spotify\", \"YouTube\", \"RSS feed\"].map((p) => <li key={p}><a href={hrefs.subscribe} className=\"opacity-90 hover:underline\">{p}</a></li>)}</ul></div>\n            <div><h2 className=\"text-xs font-bold tracking-widest uppercase opacity-70\">The show</h2><ul className=\"mt-4 space-y-2.5\">{[[\"Episodes\", hrefs.episodes], [\"Hosts\", hrefs.hosts], [\"Sponsor\", hrefs.subscribe], [\"Contact\", hrefs.hosts]].map(([l, h]) => <li key={l}><a href={h} className=\"opacity-90 hover:underline\">{l}</a></li>)}</ul></div>\n          </div>\n          <p className=\"mx-auto max-w-6xl border-t border-current/20 px-4 py-5 text-xs opacity-70 sm:px-6\">© 2026 The Long Table. Recorded in a very small kitchen.</p>\n        </footer>\n        {now && (\n          <div role=\"region\" aria-label=\"Now playing\" className=\"fixed inset-x-3 bottom-3 z-50 mx-auto max-w-3xl\">\n            <AudioPlayer key={now.slug} variant=\"compact\" title={`${now.n}. ${now.title}`} duration={now.duration} className=\"shadow-[0_20px_50px_-12px_rgb(0_0_0/0.4)]\" />\n            <button type=\"button\" onClick={() => setNow(null)} className=\"bg-card hover:bg-accent focus-visible:ring-ring/50 absolute -top-3 -end-1 inline-flex size-8 items-center justify-center rounded-full border shadow-sm outline-none focus-visible:ring-[3px]\" aria-label=\"Close the player\"><X className=\"size-4\" aria-hidden=\"true\" /></button>\n          </div>\n        )}\n      </div>\n    </PodcastContext.Provider>\n  )\n}\n\nexport { PodcastShell, ShowArt, defaultHrefs as podcastDefaultHrefs, display as podcastDisplayClass, usePodcast, type PodcastHrefs, type PodcastPage, type PodcastShellProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-theme.tsx"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-home.tsx",
      "content": "// Ballmac UI: Podcast home page. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Check, Play } from \"lucide-react\"\n\nimport { episodes, formatDate, minutes } from \"@/components/ballmac/templates/podcast/podcast-data\"\nimport { PodcastShell, ShowArt, podcastDisplayClass, usePodcast, type PodcastHrefs } from \"@/components/ballmac/templates/podcast/podcast-theme\"\nimport { cn } from \"@/lib/utils\"\n\nconst eyebrow = \"text-chart-2 text-xs font-extrabold tracking-[0.14em] uppercase\"\n\nfunction PlayButton({ ep, big }: { ep: (typeof episodes)[number]; big?: boolean }) {\n  const { play, now } = usePodcast()\n  const on = now?.slug === ep.slug\n  return (\n    <button type=\"button\" onClick={() => play(ep)} aria-label={`Play episode ${ep.n}: ${ep.title}`} aria-pressed={on} className={cn(\"bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex shrink-0 items-center justify-center gap-2 rounded-full font-bold outline-none transition-transform hover:scale-105 focus-visible:ring-[3px] motion-reduce:transition-none\", big ? \"h-14 px-7 text-base\" : \"size-11\")}>\n      <Play className={cn(\"fill-current\", big ? \"size-5\" : \"size-4 translate-x-px\")} aria-hidden=\"true\" />{big && (on ? \"Playing\" : \"Play latest\")}\n    </button>\n  )\n}\n\nfunction Subscribe() {\n  const [email, setEmail] = React.useState(\"\")\n  const [error, setError] = React.useState(\"\")\n  const [done, setDone] = React.useState(false)\n  return done ? (\n    <p role=\"status\" className=\"inline-flex items-center gap-2 font-semibold\"><Check className=\"text-chart-3 size-5\" aria-hidden=\"true\" />Saved a seat for you. See you Tuesday.</p>\n  ) : (\n    <div className=\"w-full max-w-md\"><form noValidate onSubmit={(e) => { e.preventDefault(); if (!/^\\S+@\\S+\\.\\S+$/.test(email.trim())) return setError(\"Enter an email like you@example.com.\"); setError(\"\"); setDone(true) }} className=\"flex w-full flex-col gap-2 sm:flex-row\">\n      <label htmlFor=\"pc-email\" className=\"sr-only\">Email address</label>\n      <input id=\"pc-email\" type=\"email\" value={email} onChange={(e) => setEmail(e.target.value)} aria-invalid={!!error} aria-describedby={error ? \"pc-err\" : undefined} placeholder=\"you@example.com\" className=\"bg-background text-foreground focus-visible:ring-ring/50 placeholder:text-muted-foreground aria-[invalid=true]:border-destructive h-12 flex-1 rounded-full border px-5 outline-none focus-visible:ring-[3px]\" />\n      <button type=\"submit\" className=\"bg-chart-1 h-12 rounded-full px-6 font-bold text-[var(--podcast-on-amber)] outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-[3px] focus-visible:ring-ring motion-reduce:transition-none\">Send me show notes</button>\n    </form>\n    {error && <p id=\"pc-err\" role=\"alert\" className=\"bg-background text-destructive mt-3 inline-block rounded-full px-4 py-1.5 text-sm font-semibold\">{error}</p>}\n    </div>\n  )\n}\n\ntype PodcastHomeProps = React.ComponentProps<\"div\"> & { hrefs?: Partial<PodcastHrefs> }\n\n/** The Podcast home page: the latest episode with a play button, the recent list, the hosts and a show-notes sign-up. The mini player appears when you play. */\nfunction PodcastHome({ hrefs, ...props }: PodcastHomeProps) {\n  const [latest, ...rest] = episodes\n  const ep = hrefs?.episode ?? \"/podcast/episodes/the-optimised-life\"\n  return (\n    <PodcastShell page=\"home\" hrefs={hrefs} {...props}>\n      <main>\n        <section className=\"mx-auto grid max-w-6xl items-center gap-10 px-4 pt-12 sm:px-6 sm:pt-20 md:grid-cols-[1.15fr_1fr]\">\n          <div>\n            <p className={eyebrow}>New episode · {formatDate(latest.date)}</p>\n            <h1 className={cn(\"mt-4 text-[clamp(2.8rem,7vw,5.5rem)] leading-[1.02] text-balance\", podcastDisplayClass)}>Long dinners. Good questions. No agenda.</h1>\n            <p className=\"text-muted-foreground mt-6 max-w-lg text-xl leading-relaxed text-pretty\">The Long Table is a podcast recorded at an actual table, every other Tuesday, with someone worth listening to for an hour.</p>\n            <div className=\"mt-8 flex flex-wrap items-center gap-3\">\n              <PlayButton ep={latest} big />\n              <a href={ep} className=\"hover:bg-accent focus-visible:ring-ring/50 inline-flex h-14 items-center gap-2 rounded-full border px-6 font-bold outline-none transition-colors focus-visible:ring-[3px]\">Show notes <ArrowRight className=\"size-4 rtl:rotate-180\" aria-hidden=\"true\" /></a>\n            </div>\n            <ul aria-label=\"Listen on\" className=\"mt-8 flex flex-wrap gap-2\">{[\"Apple Podcasts\", \"Spotify\", \"YouTube\", \"RSS\"].map((p) => <li key={p}><a href={hrefs?.subscribe ?? \"/podcast/subscribe\"} className=\"hover:bg-accent focus-visible:ring-ring/50 inline-flex h-9 items-center rounded-full border px-4 text-sm font-semibold outline-none transition-colors focus-visible:ring-[3px]\">{p}</a></li>)}</ul>\n          </div>\n          <a href={ep} className=\"group focus-visible:ring-ring/50 block rotate-2 rounded-[2rem] outline-none transition-transform duration-500 hover:rotate-0 focus-visible:ring-[3px] focus-visible:ring-offset-4 focus-visible:ring-offset-background motion-reduce:transition-none motion-reduce:hover:rotate-2\">\n            <ShowArt variant={latest.art} image={latest.image} imageAlt={latest.imageAlt} className=\"rounded-[2rem] shadow-[0_30px_70px_-30px_oklch(0.3_0.1_340/0.6)]\" />\n            <span className=\"sr-only\">Episode {latest.n}: {latest.title}</span>\n          </a>\n        </section>\n\n        <section aria-labelledby=\"pc-recent\" className=\"mx-auto mt-24 max-w-6xl px-4 sm:px-6\">\n          <div className=\"flex items-end justify-between gap-4\"><h2 id=\"pc-recent\" className={cn(\"text-4xl sm:text-5xl\", podcastDisplayClass)}>Recent episodes</h2><a href={hrefs?.episodes ?? \"/podcast/episodes\"} className=\"text-chart-2 inline-flex items-center gap-1.5 font-bold underline-offset-4 hover:underline\">All {episodes.length + 34} episodes <ArrowRight className=\"size-4 rtl:rotate-180\" aria-hidden=\"true\" /></a></div>\n          <ul className=\"mt-8 divide-y border-y\">\n            {rest.slice(0, 5).map((e) => (\n              <li key={e.slug} className=\"grid grid-cols-[3.5rem_1fr_auto] items-center gap-4 py-5 sm:grid-cols-[5rem_1fr_auto] sm:gap-6\">\n                <ShowArt variant={e.art} image={e.image} imageAlt={e.imageAlt} className=\"rounded-xl\" />\n                <a href={ep} className=\"group focus-visible:ring-ring/50 min-w-0 rounded outline-none focus-visible:ring-[3px]\">\n                  <p className=\"text-muted-foreground text-xs font-semibold\">Ep. {e.n} · {formatDate(e.date)} · {minutes(e.duration)}</p>\n                  <h3 className={cn(\"mt-1 text-xl leading-tight text-balance group-hover:underline underline-offset-4 sm:text-2xl\", podcastDisplayClass)}>{e.title}</h3>\n                  <p className=\"text-muted-foreground mt-1 hidden text-pretty sm:block\">{e.summary}</p>\n                </a>\n                <PlayButton ep={e} />\n              </li>\n            ))}\n          </ul>\n        </section>\n\n        <section aria-labelledby=\"pc-hosts\" className=\"mx-auto mt-24 grid max-w-6xl items-center gap-10 px-4 sm:px-6 md:grid-cols-2\">\n          <div>\n            <h2 id=\"pc-hosts\" className={cn(\"text-4xl sm:text-5xl\", podcastDisplayClass)}>Two friends, one table.</h2>\n            <p className=\"text-muted-foreground mt-5 max-w-md text-lg leading-relaxed text-pretty\">Nora Vale is a food writer. Sam Okoye is a former radio producer. They started the show to have the long conversations they kept promising each other.</p>\n            <a href={hrefs?.hosts ?? \"/podcast/hosts\"} className=\"text-chart-2 mt-5 inline-flex items-center gap-1.5 font-bold underline-offset-4 hover:underline\">Meet the hosts <ArrowRight className=\"size-4 rtl:rotate-180\" aria-hidden=\"true\" /></a>\n          </div>\n          <div aria-hidden=\"true\" className=\"flex items-center justify-center gap-4\">{[[\"NV\", \"bg-chart-4\"], [\"SO\", \"bg-chart-3\"]].map(([i, c], idx) => <div key={i} className={cn(\"text-[var(--podcast-on-amber)] flex size-40 items-center justify-center rounded-full text-5xl sm:size-52\", c, podcastDisplayClass, idx === 1 && \"translate-y-8\")}>{i}</div>)}</div>\n        </section>\n\n        <section className=\"mx-auto mt-28 max-w-6xl px-4 sm:px-6\">\n          <div className=\"bg-primary text-primary-foreground relative rounded-[2rem] px-6 py-14 text-center sm:px-14\">\n            <h2 className={cn(\"mx-auto max-w-xl text-4xl text-balance sm:text-5xl\", podcastDisplayClass)}>Show notes, in your inbox, before the episode.</h2>\n            <p className=\"mx-auto mt-4 max-w-md opacity-85 text-pretty\">One email every other Tuesday: the guest, the three best ideas and the recipe we made.</p>\n            <div className=\"mt-8 flex justify-center\"><Subscribe /></div>\n          </div>\n        </section>\n      </main>\n    </PodcastShell>\n  )\n}\n\nexport { PodcastHome, PlayButton, type PodcastHomeProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-home.tsx"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-episodes.tsx",
      "content": "// Ballmac UI: Podcast episodes page. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { Search } from \"lucide-react\"\n\nimport { episodes, formatDate, minutes } from \"@/components/ballmac/templates/podcast/podcast-data\"\nimport { PlayButton } from \"@/components/ballmac/templates/podcast/podcast-home\"\nimport { PodcastShell, ShowArt, podcastDisplayClass, type PodcastHrefs } from \"@/components/ballmac/templates/podcast/podcast-theme\"\nimport { cn } from \"@/lib/utils\"\n\ntype Season = \"All\" | \"3\" | \"2\"\n\ntype PodcastEpisodesProps = React.ComponentProps<\"div\"> & { hrefs?: Partial<PodcastHrefs> }\n\n/** The Podcast episodes page: search, a season filter and a sort, with a play button on every row. */\nfunction PodcastEpisodes({ hrefs, ...props }: PodcastEpisodesProps) {\n  const [query, setQuery] = React.useState(\"\")\n  const [season, setSeason] = React.useState<Season>(\"All\")\n  const [sort, setSort] = React.useState<\"new\" | \"long\">(\"new\")\n  const q = query.trim().toLowerCase()\n  const list = episodes\n    .filter((e) => (season === \"All\" || String(e.season) === season) && (!q || [e.title, e.guest, e.summary].join(\" \").toLowerCase().includes(q)))\n    .sort((a, b) => (sort === \"new\" ? b.n - a.n : b.duration - a.duration))\n  const ep = hrefs?.episode ?? \"/podcast/episodes/the-optimised-life\"\n  return (\n    <PodcastShell page=\"episodes\" hrefs={hrefs} {...props}>\n      <main className=\"mx-auto max-w-4xl px-4 pt-12 sm:px-6 sm:pt-16\">\n        <h1 className={cn(\"text-[clamp(3rem,8vw,5.5rem)] leading-none\", podcastDisplayClass)}>Episodes</h1>\n        <p className=\"text-muted-foreground mt-4 max-w-xl text-xl text-pretty\">Every conversation, newest first. Search a guest or an idea.</p>\n        <div className=\"mt-8 flex flex-wrap items-center gap-3\">\n          <div className=\"relative w-full sm:w-72\">\n            <Search className=\"text-muted-foreground pointer-events-none absolute top-1/2 start-4 size-4 -translate-y-1/2\" aria-hidden=\"true\" />\n            <input type=\"search\" aria-label=\"Search episodes\" value={query} onChange={(e) => setQuery(e.target.value)} placeholder=\"Search episodes\" className=\"bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-11 w-full rounded-full border pe-4 ps-11 outline-none focus-visible:ring-[3px]\" />\n          </div>\n          <div role=\"group\" aria-label=\"Season\" className=\"flex gap-1.5\">\n            {([\"All\", \"3\", \"2\"] as const).map((s) => <button key={s} type=\"button\" aria-pressed={season === s} onClick={() => setSeason(s)} className={cn(\"focus-visible:ring-ring/50 h-11 rounded-full border px-5 text-sm font-bold outline-none transition-colors focus-visible:ring-[3px]\", season === s ? \"bg-primary text-primary-foreground border-transparent\" : \"hover:bg-accent\")}>{s === \"All\" ? \"All seasons\" : `Season ${s}`}</button>)}\n          </div>\n          <label className=\"text-muted-foreground ms-auto flex items-center gap-2 text-sm font-semibold\">Sort <select value={sort} onChange={(e) => setSort(e.target.value as \"new\" | \"long\")} className=\"bg-card text-foreground focus-visible:ring-ring/50 h-11 rounded-full border px-4 outline-none focus-visible:ring-[3px]\"><option value=\"new\">Newest</option><option value=\"long\">Longest</option></select></label>\n        </div>\n        <p className=\"sr-only\" role=\"status\">{list.length} episodes</p>\n        {list.length === 0 ? (\n          <div className=\"mt-10 rounded-[2rem] border border-dashed p-14 text-center\"><p className={cn(\"text-3xl\", podcastDisplayClass)}>No episodes found</p><p className=\"text-muted-foreground mt-2\">Try a different guest, or clear the filters.</p></div>\n        ) : (\n          <ul className=\"mt-8 divide-y border-y\">\n            {list.map((e) => (\n              <li key={e.slug} className=\"grid grid-cols-[4rem_1fr_auto] items-center gap-4 py-6 sm:grid-cols-[6rem_1fr_auto] sm:gap-6\">\n                <ShowArt variant={e.art} image={e.image} imageAlt={e.imageAlt} className=\"rounded-2xl\" />\n                <a href={ep} className=\"group focus-visible:ring-ring/50 min-w-0 rounded outline-none focus-visible:ring-[3px]\">\n                  <p className=\"text-chart-2 text-xs font-extrabold tracking-[0.12em] uppercase\">Season {e.season} · Ep. {e.n}</p>\n                  <h2 className={cn(\"mt-1 text-2xl leading-tight text-balance group-hover:underline underline-offset-4 sm:text-3xl\", podcastDisplayClass)}>{e.title}</h2>\n                  <p className=\"text-muted-foreground mt-1.5 text-sm\">with {e.guest}, {e.guestRole.toLowerCase()} · {formatDate(e.date)} · {minutes(e.duration)}</p>\n                  <p className=\"text-muted-foreground mt-2 hidden text-pretty sm:block\">{e.summary}</p>\n                </a>\n                <PlayButton ep={e} />\n              </li>\n            ))}\n          </ul>\n        )}\n      </main>\n    </PodcastShell>\n  )\n}\n\nexport { PodcastEpisodes, type PodcastEpisodesProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-episodes.tsx"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-episode.tsx",
      "content": "// Ballmac UI: Podcast episode page. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { Search } from \"lucide-react\"\n\nimport { AudioPlayer, formatTime } from \"@/components/ballmac/audio-player\"\nimport { episodes, formatDate, getEpisode, minutes, transcript } from \"@/components/ballmac/templates/podcast/podcast-data\"\nimport { PodcastShell, ShowArt, podcastDisplayClass, type PodcastHrefs } from \"@/components/ballmac/templates/podcast/podcast-theme\"\nimport { cn } from \"@/lib/utils\"\n\nconst eyebrow = \"text-chart-2 text-xs font-extrabold tracking-[0.14em] uppercase\"\n\ntype PodcastEpisodeProps = React.ComponentProps<\"div\"> & { hrefs?: Partial<PodcastHrefs>; slug?: string }\n\n/** The Podcast episode page: a player whose chapters and transcript follow along, a searchable transcript you can click to seek, show notes and more episodes. */\nfunction PodcastEpisode({ hrefs, slug = \"the-optimised-life\", ...props }: PodcastEpisodeProps) {\n  const ep = getEpisode(slug)\n  const [time, setTime] = React.useState(0)\n  const [follow, setFollow] = React.useState(true)\n  const [query, setQuery] = React.useState(\"\")\n  const box = React.useRef<HTMLOListElement>(null)\n  const q = query.trim().toLowerCase()\n  const current = [...transcript].reverse().find((l) => time >= l.t)\n  const matches = q ? transcript.filter((l) => l.text.toLowerCase().includes(q)).length : 0\n\n  // Keep the spoken line in view when following along.\n  React.useEffect(() => {\n    if (!follow || !box.current) return\n    const el = box.current.querySelector<HTMLElement>(\"[data-current='true']\")\n    const parent = box.current.parentElement\n    if (el && parent && typeof parent.scrollTo === \"function\") parent.scrollTo({ top: Math.max(0, el.offsetTop - 96), behavior: \"smooth\" })\n  }, [current?.t, follow])\n\n  const marks = (text: string) => {\n    if (!q) return text\n    const i = text.toLowerCase().indexOf(q)\n    return i < 0 ? text : <>{text.slice(0, i)}<mark className=\"bg-chart-1 rounded-sm px-0.5 text-[var(--podcast-on-amber)]\">{text.slice(i, i + q.length)}</mark>{text.slice(i + q.length)}</>\n  }\n\n  return (\n    <PodcastShell page=\"episode\" hrefs={hrefs} {...props}>\n      <main className=\"mx-auto max-w-5xl px-4 pt-10 sm:px-6 sm:pt-14\">\n        <p className={eyebrow}>Season {ep.season} · Episode {ep.n}</p>\n        <h1 className={cn(\"mt-3 text-[clamp(2.6rem,6.4vw,4.8rem)] leading-[1.02] text-balance\", podcastDisplayClass)}>{ep.title}</h1>\n        <p className=\"text-muted-foreground mt-4 max-w-2xl text-xl leading-relaxed text-pretty\">{ep.summary}</p>\n        <p className=\"text-muted-foreground mt-4 text-sm\">{formatDate(ep.date)} · {minutes(ep.duration)} · with <span className=\"text-foreground font-semibold\">{ep.guest}</span></p>\n\n        <div className=\"mt-8\">\n          <AudioPlayer title={`${ep.n}. ${ep.title}`} subtitle={`The Long Table · ${minutes(ep.duration)}`} duration={ep.duration} chapters={ep.chapters} time={time} onTimeChange={setTime} artwork={<ShowArt variant={ep.art} image={ep.image} imageAlt={ep.imageAlt} />} />\n        </div>\n\n        <div className=\"mt-14 grid gap-12 lg:grid-cols-[minmax(0,1fr)_18rem]\">\n          <section aria-labelledby=\"pe-transcript\">\n            <div className=\"flex flex-wrap items-center justify-between gap-3\">\n              <h2 id=\"pe-transcript\" className={cn(\"text-3xl\", podcastDisplayClass)}>Transcript</h2>\n              <label className=\"flex cursor-pointer items-center gap-2 text-sm font-semibold\"><input type=\"checkbox\" checked={follow} onChange={(e) => setFollow(e.target.checked)} className=\"accent-primary size-4\" />Follow along</label>\n            </div>\n            <div className=\"relative mt-4\">\n              <Search className=\"text-muted-foreground pointer-events-none absolute top-1/2 start-4 size-4 -translate-y-1/2\" aria-hidden=\"true\" />\n              <input type=\"search\" aria-label=\"Search the transcript\" value={query} onChange={(e) => setQuery(e.target.value)} placeholder=\"Search the transcript\" className=\"bg-card focus-visible:ring-ring/50 placeholder:text-muted-foreground h-11 w-full rounded-full border pe-4 ps-11 outline-none focus-visible:ring-[3px]\" />\n            </div>\n            <p className=\"text-muted-foreground mt-2 text-sm\" role=\"status\">{q ? `${matches} ${matches === 1 ? \"match\" : \"matches\"}` : \"Click any line to jump to it.\"}</p>\n            <div tabIndex={0} role=\"region\" aria-label=\"Transcript\" className=\"focus-visible:ring-ring/50 bg-card mt-3 max-h-[28rem] overflow-y-auto rounded-3xl border p-3 outline-none focus-visible:ring-[3px]\">\n              <ol ref={box} className=\"space-y-1\">\n                {transcript.map((l) => {\n                  const on = current?.t === l.t\n                  return (\n                    <li key={l.t} data-current={on}>\n                      <button type=\"button\" onClick={() => setTime(l.t)} aria-current={on ? \"true\" : undefined} className={cn(\"focus-visible:ring-ring/50 grid w-full grid-cols-[3.2rem_4rem_1fr] gap-2 rounded-2xl px-3 py-3 text-start outline-none transition-colors focus-visible:ring-[3px] motion-reduce:transition-none\", on ? \"bg-chart-1 text-[var(--podcast-on-amber)]\" : \"hover:bg-accent\")}>\n                        <span className={cn(\"text-xs font-bold tabular-nums\", !on && \"text-chart-2\")}>{formatTime(l.t)}</span>\n                        <span className=\"text-sm font-extrabold\">{l.who}</span>\n                        <span className=\"text-pretty\">{marks(l.text)}</span>\n                      </button>\n                    </li>\n                  )\n                })}\n              </ol>\n            </div>\n          </section>\n\n          <aside className=\"space-y-8\">\n            <section aria-labelledby=\"pe-chapters\">\n              <h2 id=\"pe-chapters\" className={cn(\"text-2xl\", podcastDisplayClass)}>Chapters</h2>\n              <ol className=\"mt-3 divide-y border-y\">{ep.chapters.map((c) => <li key={c.start}><button type=\"button\" onClick={() => setTime(c.start)} className=\"hover:bg-accent focus-visible:ring-ring/50 flex w-full items-baseline justify-between gap-3 rounded px-1 py-3 text-start outline-none focus-visible:ring-[3px]\"><span className=\"font-semibold text-pretty\">{c.title}</span><span className=\"text-muted-foreground text-sm tabular-nums\">{formatTime(c.start)}</span></button></li>)}</ol>\n            </section>\n            <section aria-labelledby=\"pe-guest\" className=\"bg-secondary rounded-3xl p-5\">\n              <h2 id=\"pe-guest\" className=\"text-xs font-extrabold tracking-[0.14em] uppercase\">Today’s guest</h2>\n              <p className={cn(\"mt-3 text-2xl\", podcastDisplayClass)}>{ep.guest}</p>\n              <p className=\"text-muted-foreground text-sm\">{ep.guestRole}</p>\n              <ul className=\"mt-4 space-y-2 text-sm font-semibold\"><li><a href=\"#\" className=\"text-chart-2 underline underline-offset-4\">Her website</a></li><li><a href=\"#\" className=\"text-chart-2 underline underline-offset-4\">The essay we discuss</a></li></ul>\n            </section>\n          </aside>\n        </div>\n\n        <section aria-labelledby=\"pe-more\" className=\"mt-20\">\n          <h2 id=\"pe-more\" className={cn(\"text-3xl\", podcastDisplayClass)}>More conversations</h2>\n          <ul className=\"mt-6 grid gap-6 sm:grid-cols-3\">{episodes.filter((e) => e.slug !== ep.slug).slice(0, 3).map((e) => <li key={e.slug}><a href={hrefs?.episode ?? \"#\"} className=\"group focus-visible:ring-ring/50 block rounded-3xl outline-none focus-visible:ring-[3px]\"><ShowArt variant={e.art} image={e.image} imageAlt={e.imageAlt} className=\"rounded-3xl\" /><p className=\"text-muted-foreground mt-3 text-xs font-semibold\">Ep. {e.n} · {minutes(e.duration)}</p><h3 className={cn(\"mt-1 text-xl leading-tight text-balance group-hover:underline underline-offset-4\", podcastDisplayClass)}>{e.title}</h3></a></li>)}</ul>\n        </section>\n      </main>\n    </PodcastShell>\n  )\n}\n\nexport { PodcastEpisode, type PodcastEpisodeProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-episode.tsx"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-hosts.tsx",
      "content": "// Ballmac UI: Podcast hosts page. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { Mail } from \"lucide-react\"\n\nimport { PodcastShell, podcastDisplayClass, type PodcastHrefs } from \"@/components/ballmac/templates/podcast/podcast-theme\"\nimport { cn } from \"@/lib/utils\"\n\nconst hosts = [\n  { name: \"Nora Vale\", role: \"Host and food writer\", bio: \"Nora has written about food for fifteen years and cooks every dinner we record. She believes most arguments end better with a second helping.\", tone: \"bg-chart-4\", initials: \"NV\", fact: \"Cannot eat a meal without asking where the recipe came from.\" },\n  { name: \"Sam Okoye\", role: \"Host and producer\", bio: \"Sam spent a decade making radio before the long table. He asks the second question, the one after the polite answer.\", tone: \"bg-chart-3\", initials: \"SO\", fact: \"Still keeps a drawer of cassette tapes he will never digitise.\" },\n]\nconst guests = [\"Amara Okafor\", \"Tomás Reyes\", \"Inês Marques\", \"Elena Rossi\", \"Dev Patel\", \"Mei Tanaka\", \"Sasha Petrova\", \"Marcus Lindqvist\", \"Priya Raman\", \"Colm Brennan\", \"Jonas Eklund\", \"Yuki Tanaka\"]\n\ntype PodcastHostsProps = React.ComponentProps<\"div\"> & { hrefs?: Partial<PodcastHrefs> }\n\n/** The Podcast hosts page: two hosts with bios, a wall of past guests and a way to write in. */\nfunction PodcastHosts({ hrefs, ...props }: PodcastHostsProps) {\n  return (\n    <PodcastShell page=\"hosts\" hrefs={hrefs} {...props}>\n      <main className=\"mx-auto max-w-5xl px-4 pt-12 sm:px-6 sm:pt-16\">\n        <h1 className={cn(\"text-[clamp(3rem,8vw,5.5rem)] leading-none\", podcastDisplayClass)}>Meet the hosts</h1>\n        <p className=\"text-muted-foreground mt-4 max-w-xl text-xl text-pretty\">Two friends, one table, and a recording light that is mostly a candle.</p>\n        <ul className=\"mt-12 grid gap-8 md:grid-cols-2\">\n          {hosts.map((h) => (\n            <li key={h.name} className=\"bg-card rounded-[2rem] border p-8\">\n              <div aria-hidden=\"true\" className={cn(\"text-[var(--podcast-on-amber)] flex size-32 items-center justify-center rounded-full text-5xl\", h.tone, podcastDisplayClass)}>{h.initials}</div>\n              <h2 className={cn(\"mt-6 text-3xl\", podcastDisplayClass)}>{h.name}</h2>\n              <p className=\"text-chart-2 text-sm font-bold\">{h.role}</p>\n              <p className=\"text-muted-foreground mt-4 text-lg leading-relaxed text-pretty\">{h.bio}</p>\n              <p className=\"bg-secondary mt-5 rounded-2xl p-4 text-sm text-pretty\"><span className=\"font-bold\">Fun fact: </span>{h.fact}</p>\n            </li>\n          ))}\n        </ul>\n        <section aria-labelledby=\"ph-guests\" className=\"mt-20\">\n          <h2 id=\"ph-guests\" className={cn(\"text-4xl\", podcastDisplayClass)}>Everyone who sat down</h2>\n          <ul className=\"mt-8 flex flex-wrap gap-2\">{guests.map((g) => <li key={g} className=\"bg-secondary rounded-full px-4 py-2 text-sm font-semibold\">{g}</li>)}</ul>\n        </section>\n        <section aria-labelledby=\"ph-write\" className=\"bg-chart-1 mt-20 rounded-[2rem] p-8 text-[var(--podcast-on-amber)] sm:p-12\">\n          <h2 id=\"ph-write\" className={cn(\"text-4xl\", podcastDisplayClass)}>Write to the table</h2>\n          <p className=\"mt-3 max-w-lg text-lg text-pretty\">Suggest a guest, argue with an episode or send us a recipe. We read every message and answer the good questions on air.</p>\n          <a href=\"mailto:hello@longtable.example\" className=\"bg-primary text-primary-foreground focus-visible:ring-ring mt-6 inline-flex h-12 items-center gap-2 rounded-full px-6 font-bold outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-[3px] motion-reduce:transition-none\"><Mail className=\"size-4\" aria-hidden=\"true\" />hello@longtable.example</a>\n        </section>\n      </main>\n    </PodcastShell>\n  )\n}\n\nexport { PodcastHosts, type PodcastHostsProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-hosts.tsx"
    },
    {
      "path": "registry/ballmac/components/templates/podcast/podcast-subscribe.tsx",
      "content": "// Ballmac UI: Podcast subscribe page. https://ui.ballmac.com/templates/template-podcast\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Heart } from \"lucide-react\"\n\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { PodcastShell, podcastDisplayClass, type PodcastHrefs } from \"@/components/ballmac/templates/podcast/podcast-theme\"\nimport { cn } from \"@/lib/utils\"\n\nconst platforms = [[\"Apple Podcasts\", \"Open in Podcasts\"], [\"Spotify\", \"Follow on Spotify\"], [\"YouTube\", \"Watch the full dinners\"], [\"Overcast\", \"Subscribe\"], [\"Pocket Casts\", \"Subscribe\"], [\"Amazon Music\", \"Listen\"]]\nconst tiers = [\n  { id: \"guest\", name: \"Guest\", price: 5, perks: [\"Ad-free episodes\", \"Bonus recipe card each episode\"] },\n  { id: \"regular\", name: \"Regular\", price: 10, perks: [\"Everything in Guest\", \"The unedited hour after each recording\", \"Name in the credits\"], featured: true },\n  { id: \"host\", name: \"Host the dinner\", price: 25, perks: [\"Everything in Regular\", \"Vote on the next guest\", \"A yearly letter from Nora and Sam\"] },\n]\n\ntype PodcastSubscribeProps = React.ComponentProps<\"div\"> & { hrefs?: Partial<PodcastHrefs> }\n\n/** The Podcast subscribe page: every platform, the RSS feed to copy, and three ways to support the show. */\nfunction PodcastSubscribe({ hrefs, ...props }: PodcastSubscribeProps) {\n  const [tier, setTier] = React.useState(\"regular\")\n  return (\n    <PodcastShell page=\"subscribe\" hrefs={hrefs} {...props}>\n      <main className=\"mx-auto max-w-5xl px-4 pt-12 sm:px-6 sm:pt-16\">\n        <h1 className={cn(\"text-[clamp(3rem,8vw,5.5rem)] leading-none\", podcastDisplayClass)}>Pull up a chair</h1>\n        <p className=\"text-muted-foreground mt-4 max-w-xl text-xl text-pretty\">Follow The Long Table wherever you listen. New episodes land every other Tuesday at 6am.</p>\n        <ul className=\"mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n          {platforms.map(([p, l]) => <li key={p}><a href=\"#\" className=\"bg-card hover:bg-accent focus-visible:ring-ring/50 group flex items-center gap-4 rounded-3xl border p-5 outline-none transition-colors focus-visible:ring-[3px]\"><span className=\"bg-chart-1 flex size-12 shrink-0 items-center justify-center rounded-2xl text-lg font-extrabold text-[var(--podcast-on-amber)]\" aria-hidden=\"true\">{p[0]}</span><span><span className=\"block font-bold\">{p}</span><span className=\"text-muted-foreground text-sm\">{l}</span></span></a></li>)}\n        </ul>\n        <section aria-labelledby=\"ps-rss\" className=\"bg-secondary mt-8 flex flex-wrap items-center gap-4 rounded-3xl p-5\">\n          <div className=\"min-w-0 flex-1\"><h2 id=\"ps-rss\" className=\"font-bold\">RSS feed</h2><p className=\"text-muted-foreground truncate font-mono text-sm\">https://feeds.longtable.example/the-long-table.xml</p></div>\n          <CopyButton value=\"https://feeds.longtable.example/the-long-table.xml\" label=\"Copy feed\" variant=\"outline\" />\n        </section>\n\n        <section aria-labelledby=\"ps-support\" className=\"mt-24\">\n          <h2 id=\"ps-support\" className={cn(\"text-4xl sm:text-5xl\", podcastDisplayClass)}>Keep the candle lit</h2>\n          <p className=\"text-muted-foreground mt-3 max-w-xl text-lg text-pretty\">The show is made by three people and paid for by listeners. No sponsors in the middle of the conversation, ever.</p>\n          <div role=\"radiogroup\" aria-label=\"Choose a way to support the show\" className=\"mt-8 grid gap-4 md:grid-cols-3\">\n            {tiers.map((t) => {\n              const on = tier === t.id\n              return (\n                <label key={t.id} className={cn(\"has-[:focus-visible]:ring-ring/50 relative flex cursor-pointer flex-col rounded-3xl border-2 p-6 transition-colors has-[:focus-visible]:ring-[3px]\", on ? \"border-primary bg-card\" : \"hover:bg-accent/50 border-transparent bg-secondary\")}>\n                  <input type=\"radio\" name=\"tier\" value={t.id} checked={on} onChange={() => setTier(t.id)} className=\"sr-only\" />\n                  {\"featured\" in t && t.featured && <span className=\"bg-chart-1 absolute -top-3 start-6 rounded-full px-3 py-0.5 text-xs font-extrabold text-[var(--podcast-on-amber)]\">Most chosen</span>}\n                  <span className={cn(\"text-2xl\", podcastDisplayClass)}>{t.name}</span>\n                  <span className=\"mt-3 flex items-baseline gap-1\"><span className=\"text-5xl font-extrabold tabular-nums\">${t.price}</span><span className=\"text-muted-foreground text-sm\">a month</span></span>\n                  <ul className=\"mt-5 space-y-2.5\">{t.perks.map((p) => <li key={p} className=\"flex items-start gap-2.5 text-sm text-pretty\"><Check className=\"text-chart-3 mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />{p}</li>)}</ul>\n                  <span className={cn(\"mt-6 inline-flex items-center gap-1.5 text-sm font-bold\", on ? \"text-chart-2\" : \"text-muted-foreground\")} aria-hidden=\"true\">{on ? <><Check className=\"size-4\" />Selected</> : \"Choose\"}</span>\n                </label>\n              )\n            })}\n          </div>\n          <a href=\"#\" className=\"bg-primary text-primary-foreground focus-visible:ring-ring/50 mt-8 inline-flex h-14 items-center gap-2 rounded-full px-8 text-lg font-bold outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-[3px] motion-reduce:transition-none\"><Heart className=\"size-5\" aria-hidden=\"true\" />Support at ${tiers.find((t) => t.id === tier)?.price} a month</a>\n        </section>\n      </main>\n    </PodcastShell>\n  )\n}\n\nexport { PodcastSubscribe, type PodcastSubscribeProps }\n",
      "type": "registry:component",
      "target": "@components/ballmac/templates/podcast/podcast-subscribe.tsx"
    },
    {
      "path": "registry/ballmac/app/podcast/page.tsx",
      "content": "// Ballmac UI: Podcast template route. https://ui.ballmac.com/templates/template-podcast\nimport type { Metadata } from \"next\"\n\nimport { PodcastHome } from \"@/components/ballmac/templates/podcast/podcast-home\"\n\nexport const metadata: Metadata = {\n  title: \"The Long Table: a podcast recorded at a real table\",\n  description: \"Long dinners, good questions, no agenda. New episodes every other Tuesday.\",\n}\n\nexport default function Page() {\n  return <PodcastHome />\n}\n",
      "type": "registry:page",
      "target": "app/podcast/page.tsx"
    },
    {
      "path": "registry/ballmac/app/podcast/episodes/page.tsx",
      "content": "// Ballmac UI: Podcast template route. https://ui.ballmac.com/templates/template-podcast\nimport type { Metadata } from \"next\"\n\nimport { PodcastEpisodes } from \"@/components/ballmac/templates/podcast/podcast-episodes\"\n\nexport const metadata: Metadata = {\n  title: \"Episodes · The Long Table\",\n  description: \"Every conversation, newest first.\",\n}\n\nexport default function Page() {\n  return <PodcastEpisodes />\n}\n",
      "type": "registry:page",
      "target": "app/podcast/episodes/page.tsx"
    },
    {
      "path": "registry/ballmac/app/podcast/episodes/[slug]/page.tsx",
      "content": "// Ballmac UI: Podcast template route. https://ui.ballmac.com/templates/template-podcast\nimport type { Metadata } from \"next\"\n\nimport { PodcastEpisode } from \"@/components/ballmac/templates/podcast/podcast-episode\"\n\nexport const metadata: Metadata = {\n  title: \"The Optimised Life · The Long Table\",\n  description: \"A year without metrics, and what was left when she stopped counting.\",\n}\n\nexport default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n  const { slug } = await params\n  return <PodcastEpisode slug={slug} />\n}\n",
      "type": "registry:page",
      "target": "app/podcast/episodes/[slug]/page.tsx"
    },
    {
      "path": "registry/ballmac/app/podcast/hosts/page.tsx",
      "content": "// Ballmac UI: Podcast template route. https://ui.ballmac.com/templates/template-podcast\nimport type { Metadata } from \"next\"\n\nimport { PodcastHosts } from \"@/components/ballmac/templates/podcast/podcast-hosts\"\n\nexport const metadata: Metadata = {\n  title: \"Hosts · The Long Table\",\n  description: \"Meet Nora Vale and Sam Okoye.\",\n}\n\nexport default function Page() {\n  return <PodcastHosts />\n}\n",
      "type": "registry:page",
      "target": "app/podcast/hosts/page.tsx"
    },
    {
      "path": "registry/ballmac/app/podcast/subscribe/page.tsx",
      "content": "// Ballmac UI: Podcast template route. https://ui.ballmac.com/templates/template-podcast\nimport type { Metadata } from \"next\"\n\nimport { PodcastSubscribe } from \"@/components/ballmac/templates/podcast/podcast-subscribe\"\n\nexport const metadata: Metadata = {\n  title: \"Subscribe · The Long Table\",\n  description: \"Follow the show on any platform, or support it directly.\",\n}\n\nexport default function Page() {\n  return <PodcastSubscribe />\n}\n",
      "type": "registry:page",
      "target": "app/podcast/subscribe/page.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "template",
      "podcast",
      "audio",
      "episodes",
      "transcript",
      "player",
      "show notes"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Installs a five-page podcast site on the audio-player component. Change episodes in podcast-data.ts, add src URLs for real audio, and edit podcastCss for the palette.",
      "whenToUse": [
        "Podcasts, audio shows and interview series",
        "Any site where a transcript should follow audio playback"
      ],
      "whenNotToUse": [
        "Music streaming with playlists and queues"
      ],
      "composesWith": [
        "audio-player",
        "slider",
        "scroll-progress"
      ],
      "a11y": [
        {
          "keys": "Click or Enter on a transcript line",
          "action": "Seeks the player to that line"
        },
        {
          "keys": "Enter / Space on a chapter",
          "action": "Jumps to the start of the chapter"
        }
      ],
      "customization": [
        "Add src to AudioPlayer in podcast-episode.tsx and podcast-theme.tsx",
        "Edit podcastCss for the light and dark palettes",
        "Replace ShowArt with cover images"
      ]
    },
    "examples": [
      "template-podcast-demo",
      "template-podcast-episodes",
      "template-podcast-episode",
      "template-podcast-hosts",
      "template-podcast-subscribe"
    ],
    "url": "https://ui.ballmac.com/components/template-podcast"
  },
  "docs": "Pages are at /podcast, /podcast/episodes, /podcast/episodes/[slug], /podcast/hosts and /podcast/subscribe. Episodes and the transcript live in podcast-data.ts; pass a real src to AudioPlayer in podcast-episode.tsx to play audio (without one it runs a silent demo).",
  "categories": [
    "templates"
  ],
  "type": "registry:block"
}