diff --git a/.changeset/youtube-provider.md b/.changeset/youtube-provider.md
new file mode 100644
index 0000000..39cba8b
--- /dev/null
+++ b/.changeset/youtube-provider.md
@@ -0,0 +1,17 @@
+---
+"@karnstack/kino": minor
+---
+
+feat: YouTube provider. A new `@karnstack/kino/youtube` entry puts the same kino
+glass chrome over the YouTube IFrame Player API. ``
+accepts a bare id or any watch / youtu.be / embed / shorts URL (resolved via the
+exported `parseYouTubeId` helper).
+
+Play/pause, seek, speed, fullscreen, volume, and a captions menu (driven by the
+video's own subtitle tracks, rendered by YouTube inside the embed) all work
+through kino's controls. The provider follows YouTube's API terms — it plays
+through the official IFrame API and doesn't obscure the player, so YouTube's own
+thumbnail, play button, title, and logo show before playback and while paused.
+Quality, picture-in-picture, and scrub-preview storyboards are hidden because
+the IFrame API doesn't expose them. No runtime dependency — the API is loaded on
+demand.
diff --git a/README.md b/README.md
index 5e54d27..d728160 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
A themeable React video player with a pluggable-provider architecture —
translucent glass chrome, keyboard-first controls, and a small typed surface.
- Mux is the first provider.
+ Mux, raw files, and YouTube are built in.
@@ -24,7 +24,7 @@
> **[Try it live → kino.karnstack.com](https://kino.karnstack.com)** — drop in any public Mux playback ID, pick an accent, and play with the real glass UI.
-kino ships the player UI and a provider contract. Each provider adapts a streaming engine to that contract, so the same glass chrome can sit on top of different backends. The Mux provider is built on the `@mux/mux-video` custom element.
+kino ships the player UI and a provider contract. Each provider adapts a streaming engine to that contract, so the same glass chrome can sit on top of different backends. Three providers ship today: **Mux** (adaptive HLS via `@mux/mux-video`), **Native** (a plain `
@@ -157,6 +190,15 @@ export function InstallPage() {
rows={propRows(NATIVE_PROPS)}
/>
+
)
diff --git a/demo/pages/overview.tsx b/demo/pages/overview.tsx
index be3c7b7..21ed22c 100644
--- a/demo/pages/overview.tsx
+++ b/demo/pages/overview.tsx
@@ -16,7 +16,7 @@ const HIGHLIGHTS = [
n: "01",
term: "Pluggable providers",
detail:
- "One UI contract, many engines. Mux HLS and raw files ship today; YouTube and Vimeo are next.",
+ "One UI contract, many engines. Mux HLS, raw files, and YouTube ship today; Vimeo is next.",
},
{
n: "02",
@@ -50,7 +50,7 @@ export function OverviewPage() {
kino is a themeable React video player with a pluggable-provider
architecture. The same translucent, keyboard-first UI sits over Mux,
- raw files, and more — behind a small typed surface.
+ raw files, and YouTube — behind a small typed surface.
diff --git a/demo/pages/providers.tsx b/demo/pages/providers.tsx
index e95aece..556bb46 100644
--- a/demo/pages/providers.tsx
+++ b/demo/pages/providers.tsx
@@ -27,9 +27,11 @@ const PROVIDERS: ProviderCard[] = [
},
{
name: "YouTube",
- status: "planned",
+ status: "shipped",
+ entry: "@karnstack/kino/youtube",
detail:
- "Embed-backed playback wrapped in the same kino chrome, so a YouTube source feels native to the player.",
+ "The YouTube IFrame Player API wrapped in the same kino chrome — kino owns the controls, keyboard map, and captions menu. Quality and PiP follow YouTube's API limits.",
+ importLine: 'import { YouTubePlayer } from "@karnstack/kino/youtube"',
},
{
name: "Vimeo",
diff --git a/demo/player-studio.tsx b/demo/player-studio.tsx
index 305cd77..97572a8 100644
--- a/demo/player-studio.tsx
+++ b/demo/player-studio.tsx
@@ -1,10 +1,11 @@
import { useState, type CSSProperties } from "react"
import { MuxPlayer } from "../src/mux/mux-player"
import { NativePlayer } from "../src/native/native-player"
+import { YouTubePlayer } from "../src/youtube/youtube-player"
import { CheckIcon } from "./icons"
import { TouchTarget } from "./ui"
-export type Mode = "mux" | "native"
+export type Mode = "mux" | "native" | "youtube"
// Public sample assets so the studio plays real media with no account or signed
// tokens — anyone who clones the repo gets the full UI.
@@ -18,6 +19,13 @@ const NATIVE_SAMPLE = {
label: "accrobra · mp4",
} as const
+// A public, embeddable Creative Commons video so the YouTube tab plays for
+// anyone who clones the repo.
+const YOUTUBE_SAMPLE = {
+ id: "aqz-KE-bpKQ",
+ label: "Big Buck Bunny · YouTube",
+} as const
+
export const DEFAULT_ACCENT = "#f4b942"
const ACCENTS = [
{ name: "Leader", value: DEFAULT_ACCENT },
@@ -48,8 +56,18 @@ export function PlayerStudio({
const [radius, setRadius] = useState(DEFAULT_RADIUS)
const activeSample = SAMPLES.find((s) => s.id === source)
- const label = mode === "native" ? NATIVE_SAMPLE.label : activeSample?.label
- const code = mode === "native" ? NATIVE_SAMPLE.src : source
+ const label =
+ mode === "native"
+ ? NATIVE_SAMPLE.label
+ : mode === "youtube"
+ ? YOUTUBE_SAMPLE.label
+ : activeSample?.label
+ const code =
+ mode === "native"
+ ? NATIVE_SAMPLE.src
+ : mode === "youtube"
+ ? YOUTUBE_SAMPLE.id
+ : source
return (
@@ -71,6 +89,13 @@ export function PlayerStudio({
accentColor={accent}
theme={{ "--kino-radius": `${radius}px` }}
/>
+ ) : mode === "youtube" ? (
+
) : (
{
const active = mode === p.id
diff --git a/docs/superpowers/specs/2026-06-28-youtube-provider-design.md b/docs/superpowers/specs/2026-06-28-youtube-provider-design.md
new file mode 100644
index 0000000..43a53d0
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-28-youtube-provider-design.md
@@ -0,0 +1,153 @@
+# YouTube provider — design
+
+## Goal
+
+Add a YouTube provider to kino so the same glass chrome plays YouTube videos,
+mirroring the existing `mux/` and `native/` providers. Then update the README,
+demo site, and overview copy so the docs advertise **three** shipped providers
+(Mux, Native, YouTube) instead of framing Mux as the only/first one.
+
+## Architecture
+
+kino's contract is `Provider` (`src/core/types.ts`): `mount(container)`,
+`getState()`, `subscribe(listener)`, `actions`, `destroy()`, optional
+`swapSource(opts)`. A thin React wrapper creates the provider once
+(`useRef`) and routes reactive prop changes through `swapSource`. UI controls
+gate themselves on the `capabilities` set the provider reports.
+
+The YouTube provider follows the `native/` shape exactly.
+
+### New files
+
+- `src/youtube/provider.ts` — `createYouTubeProvider(opts): Provider` +
+ `YouTubeProviderOptions` type + `parseYouTubeId(input): string` helper.
+- `src/youtube/youtube-player.tsx` — `` (props =
+ `YouTubeProviderOptions` + `accentColor/theme/className/placeholder/children`).
+- `src/youtube/provider.test.ts` — vitest, mirroring `native/provider.test.ts`.
+- `src/youtube.ts` — entry re-exporting the provider, component, types, helper.
+
+### Wiring
+
+- `package.json` → add `exports["./youtube"]`.
+- `tsdown.config.ts` → add `youtube: "src/youtube.ts"` entry.
+- `package.json` `devDependencies` → `@types/youtube` (global `YT` typings only;
+ no runtime dependency — the IFrame API script is loaded at runtime).
+- `src/styles/kino.css` → add `.kino iframe` to the existing
+ `.kino mux-video, .kino video { inset:0; width/height:100% }` rule.
+
+## Engine integration
+
+YouTube IFrame Player API. A module-level singleton promise lazy-loads
+`https://www.youtube.com/iframe_api` and resolves when `window.YT.Player` is
+ready (chaining the existing global `onYouTubeIframeAPIReady` callback so
+multiple players coexist). Already-loaded `window.YT` short-circuits.
+
+`mount(container)` appends a host `` and, once the API resolves, constructs
+`new YT.Player(hostDiv, { videoId, playerVars: { controls: 0, playsinline: 1,
+rel: 0, modestbranding: 1, autoplay, mute, loop, playlist }, events })`. The API
+replaces the div with an `