diff --git a/.claude/skills/api-reference/SKILL.md b/.claude/skills/api-reference/SKILL.md
index f4c42557..ebf0a95e 100644
--- a/.claude/skills/api-reference/SKILL.md
+++ b/.claude/skills/api-reference/SKILL.md
@@ -1,24 +1,25 @@
---
name: api-reference
description: >-
- Scaffold API reference documentation for Video.js 10 components. Validates
- the api-docs-builder output, checks design docs and linked PRs for context,
- creates the MDX reference page with anatomy, prose sections, demos, and the
- ApiReference component. Triggers: "api reference", "reference page",
- "scaffold api docs", "add api docs", "component reference".
+ Scaffold API reference documentation for Video.js 10 components and utility
+ APIs. Validates the api-docs-builder output, checks design docs and linked
+ PRs for context, creates the MDX reference page with anatomy, prose sections,
+ demos, and the ComponentReference/UtilReference component. Triggers: "api
+ reference", "reference page", "scaffold api docs", "add api docs",
+ "component reference", "util reference", "hook reference".
---
# API Reference
-Scaffold a complete API reference page for a Video.js 10 component.
+Scaffold a complete API reference page for a Video.js 10 component or utility API.
## Usage
```
-/api-reference [component-name]
+/api-reference [name]
```
-- `component-name` (optional): kebab-case component name (e.g., `play-button`). If omitted, will prompt.
+- `name` (optional): kebab-case component name (e.g., `play-button`) or util name (e.g., `use-player`). If omitted, will prompt.
## Arguments
@@ -31,6 +32,7 @@ Load these files based on task:
| Need | Load |
|------|------|
| Builder naming conventions | `references/builder-conventions.md` |
+| Util conventions | `references/util-conventions.md` |
| MDX page structure | `references/mdx-structure.md` |
| Demo file patterns | `references/demo-patterns.md` |
| Component libraries reference | `docs` skill → `references/component-libraries.md` |
@@ -38,7 +40,7 @@ Load these files based on task:
| Accessibility | `aria` skill |
| Design docs | `internal/design/` |
-## Your Tasks
+## Component Reference Workflow
### Step 1: Gather context
@@ -54,7 +56,7 @@ Accept component name as argument (kebab-case).
### Step 2: Validate api-docs-builder compatibility
1. Run `pnpm -F site api-docs` and check for errors
-2. Read the generated JSON at `site/src/content/generated-api-reference/{name}.json`
+2. Read the generated JSON at `site/src/content/generated-component-reference/{name}.json`
3. Verify the JSON has expected sections (props, state, dataAttributes, platforms.html.tagName)
4. For multi-part: verify each part appears in `parts` with correct names
@@ -88,9 +90,36 @@ Load `references/mdx-structure.md` for the full structure template.
1. Create `site/src/content/docs/reference/{name}.mdx`
2. Add to sidebar in `site/src/docs.config.ts` (alphabetically within Components section)
-3. Structure: frontmatter → imports → Anatomy → prose sections → Examples → ``
+3. Structure: frontmatter → imports → Anatomy → prose sections → Examples → ``
4. Run `pnpm dev` from root and verify the page renders in both HTML and React framework modes
+## Util Reference Workflow
+
+For hooks, controllers, mixins, factories, and utilities. Load `references/util-conventions.md` for full details.
+
+### Step 1: Ensure auto-discovery can find the util
+
+1. The util must be exported from one of the scanned package index files (`packages/react/src/index.ts`, `packages/html/src/index.ts`, or the store subpath indexes)
+2. Add JSDoc with a description to the source export
+3. If the export doesn't match a naming convention (`use*`, `*Controller`, `create*`), add `@public` to its JSDoc
+
+Load `references/util-conventions.md` for the full inclusion and classification rules.
+
+### Step 2: Generate and validate JSON
+
+1. Run `pnpm -F site api-docs` and check for errors
+2. Read the generated JSON at `site/src/content/generated-util-reference/{slug}.json`
+3. Verify it has the expected overloads, parameters, and return value
+
+### Step 3: Create MDX page
+
+1. Create `site/src/content/docs/reference/{slug}.mdx`
+2. Structure: frontmatter → `import UtilReference` → `## Import` → `## Usage` → ``
+3. Add to sidebar in `site/src/docs.config.ts`:
+ - React utils → "Hooks & Utilities" section (`frameworks: ['react']`)
+ - HTML utils → "Controllers & Mixins" section (`frameworks: ['html']`)
+4. Run `pnpm dev` and verify the page renders correctly
+
## Related Skills
| Need | Use |
diff --git a/.claude/skills/api-reference/references/builder-conventions.md b/.claude/skills/api-reference/references/builder-conventions.md
index 6e4fa088..1729d7a2 100644
--- a/.claude/skills/api-reference/references/builder-conventions.md
+++ b/.claude/skills/api-reference/references/builder-conventions.md
@@ -56,6 +56,10 @@ export const Value = ...;
- **Part descriptions**: From JSDoc on React component exports in their `.tsx` files
- **Prop/state descriptions**: From JSDoc on interface properties in the core file
+### Util JSDoc
+
+Util exports (hooks, controllers, factories, selectors) have their own JSDoc conventions for `@param`, `@label`, and `@public` tags. See `references/util-conventions.md` → "JSDoc Conventions".
+
## Common Failures
The builder fails silently for many issues — data just won't appear in the JSON:
@@ -76,10 +80,13 @@ The builder fails silently for many issues — data just won't appear in the JSO
# Generate JSON
pnpm -F site api-docs
-# Check output
-cat site/src/content/generated-api-reference/{name}.json
+# Check component output
+cat site/src/content/generated-component-reference/{name}.json
+
+# Check util output
+cat site/src/content/generated-util-reference/{slug}.json
# Verify schema
-# The builder validates against ComponentApiReferenceSchema before writing.
+# The builder validates against ComponentReferenceSchema / UtilReferenceSchema before writing.
# Schema errors are logged as errors and cause exit code 1.
```
diff --git a/.claude/skills/api-reference/references/mdx-structure.md b/.claude/skills/api-reference/references/mdx-structure.md
index bd9ad629..74456f0d 100644
--- a/.claude/skills/api-reference/references/mdx-structure.md
+++ b/.claude/skills/api-reference/references/mdx-structure.md
@@ -2,7 +2,9 @@
Structure and conventions for API reference MDX pages at `site/src/content/docs/reference/`.
-## Frontmatter
+## Component Pages
+
+### Frontmatter
```yaml
---
@@ -17,7 +19,7 @@ description: A button component for muting and unmuting audio playback
- `frameworkTitle.html`: The `static tagName` from the HTML element file
- `description`: One-line description of the component
-## Page Structure
+### Page Structure
```
frontmatter
@@ -29,7 +31,7 @@ imports (React demos, HTML demos)
## Examples
### BasicUsage
### [Additional demos]
-
+
```
## Imports Section
@@ -185,19 +187,139 @@ Key details:
- React source tabs: `App.tsx`, `App.css`
- HTML source tabs: `index.html`, `index.css`, `index.ts`
-## ApiReference Component
+### ComponentReference Component
Always the last element in the file:
```mdx
-
+
```
The component auto-renders Props, State, Data Attributes for single-part and all Parts for multi-part.
+### Required Astro Component Imports
+
+Every component reference MDX needs these at the top of the imports:
+
+```mdx
+import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
+import FrameworkCase from "@/components/docs/FrameworkCase.astro";
+import StyleCase from "@/components/docs/StyleCase.astro";
+import Demo from "@/components/docs/demos/Demo.astro";
+```
+
+---
+
+## Cross-linking
+
+Link generously between related reference pages.
+
+Same-framework or cross-framework link:
+
+```mdx
+Within a player provider, `usePlayer` is usually simpler.
+```
+
+Selector page linking to framework-specific utils:
+
+```mdx
+
+Pass `selectPlayback` to `usePlayer` to subscribe.
+
+
+
+Pass `selectPlayback` to `PlayerController` to subscribe.
+
+```
+
+---
+
+## Util Pages
+
+Util pages document React hooks/utilities and HTML controllers/mixins. They are simpler than component pages — no demos, no anatomy.
+
+### Frontmatter
+
+```yaml
+---
+title: usePlayer
+description: Hook to access the player store from within a Player Provider
+---
+```
+
+- `title`: The exported function/class name (e.g., `usePlayer`, `PlayerController`)
+- No `frameworkTitle` — util pages are framework-specific
+- `description`: One-line description
+
+### Page Structure
+
+```
+frontmatter
+import UtilReference
+## Import
+## Usage
+
+```
+
+### Import Section
+
+Show the import statement for the util:
+
+```mdx
+## Import
+
+\`\`\`tsx
+import { usePlayer } from '@videojs/react';
+\`\`\`
+```
+
+### Usage Section
+
+Explain usage patterns with code examples. For multi-overload utils, document each overload:
+
+```mdx
+## Usage
+
+`usePlayer` has two overloads:
+
+**Store access (no subscription)** -- returns the store instance.
+
+\`\`\`tsx
+const store = usePlayer();
+\`\`\`
+
+**Selector-based subscription** -- returns selected state.
+
+\`\`\`tsx
+const paused = usePlayer((s) => s.paused);
+\`\`\`
+```
+
+### UtilReference Component
+
+Always the last element in the file:
+
+```mdx
+
+```
+
+The component auto-renders Parameters and Return Value tables. For multi-overload utils, it renders each overload with its own sections.
+
+### Required Import
+
+```mdx
+import UtilReference from "@/components/docs/api-reference/UtilReference.astro";
+```
+
+---
+
## Sidebar Entry
-Add to `site/src/docs.config.ts` in the Components section, alphabetically:
+Add to `site/src/docs.config.ts` in the appropriate section, alphabetically:
+
+- **Components** — UI component reference pages
+- **Hooks & Utilities** (`frameworks: ['react']`) — React hooks and utilities
+- **Controllers & Mixins** (`frameworks: ['html']`) — HTML controllers and mixins
```ts
{
@@ -212,14 +334,3 @@ Add to `site/src/docs.config.ts` in the Components section, alphabetically:
],
},
```
-
-## Required Astro Component Imports
-
-Every reference MDX needs these at the top of the imports:
-
-```mdx
-import ApiReference from "@/components/docs/api-reference/ApiReference.astro";
-import FrameworkCase from "@/components/docs/FrameworkCase.astro";
-import StyleCase from "@/components/docs/StyleCase.astro";
-import Demo from "@/components/docs/demos/Demo.astro";
-```
diff --git a/.claude/skills/api-reference/references/util-conventions.md b/.claude/skills/api-reference/references/util-conventions.md
new file mode 100644
index 00000000..8eb108a0
--- /dev/null
+++ b/.claude/skills/api-reference/references/util-conventions.md
@@ -0,0 +1,228 @@
+# Util Reference Conventions
+
+Conventions for the util reference system that documents React hooks/utilities and HTML controllers/mixins.
+
+## Architecture
+
+Util references use **convention-based auto-discovery** from package index files. The builder scans entry points, resolves local module paths, and includes exports matching naming conventions or annotated with `@public`.
+
+## Auto-Discovery Pipeline
+
+The builder (`site/scripts/api-docs-builder/src/util-handler.ts`) scans entry points:
+
+```ts
+packages/react/src/index.ts → framework: 'react'
+packages/store/src/react/hooks/index.ts → framework: 'react'
+packages/html/src/index.ts → framework: 'html'
+packages/store/src/html/controllers/index.ts → framework: 'html'
+packages/core/src/dom/store/selectors.ts → framework: null (agnostic)
+packages/store/src/core/selector.ts → framework: null (agnostic)
+```
+
+Framework-agnostic entries (`framework: null`) produce JSON without a `frameworks` field, meaning they apply to all frameworks. Framework-specific entries get `frameworks: ['react']` or `frameworks: ['html']` in the JSON.
+
+**Phase 1 — Resolve local modules.** Raw TS AST reads export declarations from each index file, keeping only local paths (`./...`), skipping external packages (`@videojs/...`).
+
+**Phase 2 — Filter by convention.** Each local module is parsed with TAE (typescript-api-extractor) or raw TS AST. Exports are included if they match naming conventions or have `@public`.
+
+### Adding a New Util
+
+1. Export it from the appropriate package index file
+2. Add JSDoc with a description
+3. If it doesn't match a naming convention (see below), add `@public` to the JSDoc
+4. Run `pnpm api-docs` to generate its JSON
+4. Create an MDX page with ``
+5. Add to the sidebar in `docs.config.ts`
+
+No code changes needed in the builder itself — convention over configuration.
+
+## JSDoc Conventions
+
+The builder extracts JSDoc from source exports to populate reference pages. These rules override the root CLAUDE.md "Minimal JSDoc" guidelines for API reference exports.
+
+### Summary description (required)
+
+Every util export needs a JSDoc summary. This becomes the description in the generated JSON:
+
+```ts
+/** Subscribe to the player's volume state. */
+export function useVolume(...): VolumeResult;
+```
+
+### `@param` descriptions (required for non-obvious params)
+
+Unlike internal code, API reference exports need `@param` tags so the builder can populate parameter tables. Describe intent and defaults, not types:
+
+```ts
+/**
+ * Subscribe to derived state with customizable equality check.
+ *
+ * @param subscribe - Subscribe function that returns an unsubscribe callback.
+ * @param selector - Derives a value from the snapshot.
+ * @param isEqual - Custom equality function. Defaults to `shallowEqual`.
+ */
+export function useSelector(...): R;
+```
+
+Format: `@param name - description` (dash after name).
+
+### No `@returns`
+
+Return types are inferred from the TypeScript signature. Don't add `@returns`.
+
+### `@label` for multi-overload functions
+
+When a function has multiple overload signatures with different return types, each overload gets its own JSDoc block with an `@label` tag. The label becomes a heading in the docs:
+
+```ts
+/**
+ * Create a player instance with typed store, Provider, and hooks.
+ *
+ * @label Video
+ * @param config - Player configuration with features.
+ */
+export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+
+/**
+ * Create a player for audio media.
+ *
+ * @label Audio
+ * @param config - Player configuration with features.
+ */
+export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+```
+
+Without `@label`, overloads render as "Overload 1", "Overload 2", etc.
+
+### `@label` for constructor overloads
+
+Same pattern applies to controller constructors:
+
+```ts
+/**
+ * @label Without Selector
+ * @param host - The host element that owns this controller.
+ * @param state - The State container to subscribe to.
+ */
+constructor(host: ReactiveControllerHost, state: State);
+
+/**
+ * @label With Selector
+ * @param host - The host element that owns this controller.
+ * @param state - The State container to subscribe to.
+ * @param selector - Derives a value from the state.
+ */
+constructor(host: ReactiveControllerHost, state: State, selector: Selector);
+```
+
+### `@public` for non-convention exports
+
+Exports that don't match a naming convention (`use*`, `*Controller`, `create*`, `select*`) need `@public` to be discovered:
+
+```ts
+/** @public The default player context for consuming the player store. */
+export const playerContext = createContext(...);
+```
+
+## Inclusion Conventions
+
+Exports are auto-included when they match these patterns:
+
+| Pattern | Match Rule | Examples |
+|---------|-----------|----------|
+| Hooks | Name starts with `use`, is a function | `usePlayer`, `useStore` |
+| Controllers | Name ends with `Controller` | `PlayerController`, `StoreController` |
+| Factories | Name starts with `create`, is a function | `createPlayer` |
+| Mixin factories | Name starts with `create` + contains `Mixin` | `createProviderMixin` |
+| `@public` | Has `@public` JSDoc tag | `playerContext`, `mergeProps`, `renderElement` |
+
+Exports that don't match any convention are excluded (UI components, types, internal helpers).
+
+## Slug Conventions
+
+- Slugs are kebab-case: `use-player`, `player-controller`, `merge-props`
+- All slugs must be unique across both frameworks
+- When the same name exists in both React and HTML (e.g., `createPlayer`), prefix the HTML slug: `html-create-player`
+
+## Frameworks
+
+| Framework | Sidebar Section | Source Packages |
+|-----------|----------------|-----------------|
+| `react` | Hooks & Utilities | `@videojs/react`, `@videojs/store/react` |
+| `html` | Controllers & Mixins | `@videojs/html`, `@videojs/store/html` |
+| `null` (agnostic) | Selectors | `@videojs/core/dom`, `@videojs/store` |
+
+Agnostic utils omit the `frameworks` field in JSON, meaning they're available to all frameworks.
+
+## Overloads
+
+Use multiple overloads when the return type genuinely differs between signatures:
+
+```ts
+// Two overloads — different return types
+usePlayer() → PlayerStore
+usePlayer(selector) → T (selected value)
+```
+
+For simple param-count differences with the same return type, use a single overload with optional params instead.
+
+## Generated JSON
+
+Output: `site/src/content/generated-util-reference/{slug}.json`
+
+Schema: `UtilReferenceSchema` from `site/src/types/util-reference.ts`
+
+```json
+{
+ "name": "usePlayer",
+ "description": "...",
+ "overloads": [
+ {
+ "description": "...",
+ "parameters": { "selector": { "type": "...", "required": true } },
+ "returnValue": { "type": "...", "description": "..." }
+ }
+ ]
+}
+```
+
+## Content Collection
+
+The `utilReference` collection in `site/src/content.config.ts` loads from `generated-util-reference/` and validates against `UtilReferenceSchema`.
+
+## Astro Components
+
+| Component | Purpose |
+|-----------|---------|
+| `UtilReference.astro` | Main component — loads JSON, renders sections |
+| `UtilParamsTable.astro` | Parameter table (reuses `PropRow.astro`) |
+| `UtilReturnTable.astro` | Return value table (reuses `StateRow.astro` for object returns) |
+
+## Common Failures
+
+| Symptom | Cause |
+|---------|-------|
+| No JSON generated | Export not in a scanned index file, or doesn't match convention / lack `@public` |
+| Zod validation error | Schema mismatch — check field names and types |
+| Missing from sidebar | Not added to `docs.config.ts` in correct section |
+| Page 404 | MDX file missing or slug mismatch |
+| Wrong framework section | Export in wrong entry point — check which index file re-exports it |
+| TAE crash on index file | Known issue with `UniqueESSymbol` types — raw TS AST fallback handles this |
+
+## Discovery vs MDX Pages
+
+Auto-discovery generates JSON files in `site/src/content/generated-util-reference/`. A JSON file without a corresponding MDX page is harmless — it sits unused.
+
+Only utils with **both** generated JSON **and** a manually-created MDX page appear in the docs. This is intentional: discovery casts a wide net using naming conventions, while MDX pages are curated to document the public API surface.
+
+For example, `SubscriptionController` is discovered (matches `*Controller`) but has no MDX page because it's an internal building block not intended for direct consumer use.
+
+When adding a new util to the docs:
+1. Ensure the builder discovers it (check with `pnpm api-docs`)
+2. Create the MDX page at `site/src/content/docs/reference/{slug}.mdx`
+3. Add to the sidebar in `docs.config.ts`
+
+## Tests
+
+- `site/scripts/api-docs-builder/src/tests/util-handler.test.ts` — fixture-based tests for discovery, slug uniqueness, frameworks, overloads
+- `site/src/utils/tests/utilReferenceModel.test.ts` — validates model structure and TOC headings
diff --git a/.claude/skills/docs/references/writing-style.md b/.claude/skills/docs/references/writing-style.md
index dc891e5a..394f8da8 100644
--- a/.claude/skills/docs/references/writing-style.md
+++ b/.claude/skills/docs/references/writing-style.md
@@ -132,12 +132,24 @@ unsubscribe();
```markdown
// ✅ Natural
-See [Events](/concepts/events) for the full list.
+See Events for the full list.
// ❌ Awkward
For more information about events, please refer to the Events page.
```
+### Cross-link reference pages generously
+
+When a reference page mentions another API by name, link to it. Readers exploring one API often need context from related APIs. Link the first prose mention per page — don't link inside code blocks.
+
+```markdown
+// ✅ Linked
+Within a player provider, `usePlayer` is usually simpler.
+
+// ❌ Unlinked
+Within a player provider, `usePlayer` is usually simpler.
+```
+
## Length guidelines
| Content type | Target |
diff --git a/.gitignore b/.gitignore
index 53e74d92..08933e57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,8 @@ coverage/
.jest/
__tests__/coverage/
site/src/content/generated-api-reference/
+site/src/content/generated-component-reference/
+site/src/content/generated-util-reference/
# -------------------------
# Environment
diff --git a/CLAUDE.md b/CLAUDE.md
index f3424ea4..0b9803d6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -437,7 +437,7 @@ const media = node.querySelector('video, audio') as HTMLMediaElement | null;
JSDoc should add value, not restate what TypeScript already shows:
-**No redundant @param/@returns** — TypeScript signatures are the documentation:
+**No redundant @param/@returns** (exception: API reference exports — see below) — TypeScript signatures are the documentation:
```ts
// Bad
@@ -452,7 +452,7 @@ export function animationFrame(callback: FrameRequestCallback): () => void;
export function animationFrame(callback: FrameRequestCallback): () => void;
```
-**Single JSDoc for overloads** — Document the first overload only:
+**Single JSDoc for overloads** (exception: API reference exports — see below) — Document the first overload only:
```ts
/** Wait for an event to occur on a target. */
@@ -482,6 +482,12 @@ export interface Media extends HTMLMediaElement {}
export type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
```
+**API reference exports are different** — Exports that feed the api-docs-builder (`use*` hooks, `*Controller` classes, `create*` factories, selectors, and `@public`-annotated exports) need richer JSDoc for the generated reference pages. See the `api-reference` skill → `references/util-conventions.md` for the full rules. Key differences from above:
+
+- `@param name - description` is required (the builder extracts these into parameter tables).
+- Multi-overload functions get per-overload JSDoc with `@label` tags (not a single JSDoc block).
+- `@public` opts in exports that don't match naming conventions.
+
## Design Documents
| Location | Purpose |
diff --git a/internal/design/site/api-docs-builder.md b/internal/design/site/api-docs-builder.md
new file mode 100644
index 00000000..b348dbe3
--- /dev/null
+++ b/internal/design/site/api-docs-builder.md
@@ -0,0 +1,1264 @@
+# API Docs Builder Spec
+
+Ground-truth specification for the API docs builder pipeline: from TypeScript source code to
+rendered documentation tables. When implementation diverges from this spec, this spec wins.
+
+Inspired by [Base UI](https://github.com/mui/base-ui)'s API reference system. Base UI generates one JSON
+file per component part, and each part gets its own props and data-attributes tables. Our system
+aspires to the same philosophy but has a known architectural limitation: a single Props/State
+interface per component at the core level means only one part (the "primary") can own props and
+state. Non-primary parts are documented with a tag name and description only.
+
+**Principles:**
+
+- **Convention over configuration.** The builder infers structure from file naming and placement.
+ No config files, no explicit annotations for standard cases. Follow the conventions and things
+ just work.
+- **Spec wins.** When implementation diverges from this spec, the spec is the source of truth.
+
+**Scope:** This spec covers the builder (extraction + JSON generation), the reference model
+layer (JSON → heading/section structure), TOC integration, and rendered output (tables + disclosure
+panels). It does not cover CSS styling, demo scaffolding, or MDX page authoring.
+
+---
+
+## 1. Sample Inputs
+
+These fictional examples are the source-of-truth for expected behavior throughout this spec.
+
+### 1a. Single-part component: `ToggleButton`
+
+**Core file** — `packages/core/src/core/ui/toggle-button/toggle-button-core.ts`:
+
+```ts
+interface ToggleButtonProps {
+ /** Whether the button is disabled. */
+ disabled: boolean;
+ /** Custom label for the button. */
+ label: string | ((state: ToggleButtonState) => string);
+}
+
+interface ToggleButtonState {
+ /** Whether the toggle is pressed. */
+ pressed: boolean;
+ /** Whether the button is disabled. */
+ disabled: boolean;
+}
+
+class ToggleButtonCore {
+ static readonly defaultProps = {
+ disabled: false,
+ label: '',
+ } as const;
+}
+```
+
+**Data attributes file** — `packages/core/src/core/ui/toggle-button/toggle-button-data-attrs.ts`:
+
+```ts
+import type { StateAttrMap } from '../types';
+
+export const ToggleButtonDataAttrs = {
+ /** Present when the toggle is pressed. */
+ pressed: 'data-pressed',
+ /** Present when the button is disabled. */
+ disabled: 'data-disabled',
+} as const satisfies StateAttrMap;
+```
+
+**HTML element file** — `packages/html/src/ui/toggle-button/toggle-button-element.ts`:
+
+```ts
+export class ToggleButtonElement extends ... {
+ static readonly tagName = 'media-toggle-button';
+}
+```
+
+### 1b. Multi-part component: `Meter`
+
+**Core file** — `packages/core/src/core/ui/meter/meter-core.ts`:
+
+```ts
+interface MeterProps {
+ /** Minimum value. */
+ min: number;
+ /** Maximum value. */
+ max: number;
+ /** Custom label for accessibility. */
+ label: string | ((state: MeterState) => string);
+}
+
+interface MeterState {
+ /** Current value as a percentage (0–1). */
+ percentage: number;
+ /** The fill level. */
+ fillState: 'empty' | 'partial' | 'full';
+}
+
+class MeterCore {
+ static readonly defaultProps = {
+ min: 0,
+ max: 100,
+ label: '',
+ } as const;
+}
+```
+
+**Data attributes file** — `packages/core/src/core/ui/meter/meter-data-attrs.ts`:
+
+```ts
+import type { StateAttrMap } from '../types';
+
+export const MeterDataAttrs = {
+ /** Current percentage as a string. */
+ percentage: 'data-percentage',
+ /** The fill level. */
+ fillState: 'data-fill-state',
+} as const satisfies StateAttrMap;
+```
+
+**HTML element files:**
+
+- `packages/html/src/ui/meter/meter-element.ts` → `static tagName = 'media-meter'`
+- `packages/html/src/ui/meter/meter-track-element.ts` → `static tagName = 'media-meter-track'`
+- `packages/html/src/ui/meter/meter-fill-element.ts` → `static tagName = 'media-meter-fill'`
+
+**React parts index** — `packages/react/src/ui/meter/index.parts.ts`:
+
+```ts
+export { default as Track } from './Track';
+export { default as Fill } from './Fill';
+export { default as Indicator } from './Indicator';
+```
+
+**React component JSDoc:**
+
+```tsx
+// Track.tsx
+/** The track area of the meter. Renders a `
` element. */
+export default function Track(...) { ... }
+
+// Fill.tsx
+/** The filled portion of the meter. Renders a `
` element. */
+export default function Fill(...) { ... }
+
+// Indicator.tsx — no matching HTML element file
+/** A visual indicator for the current value. Renders a `` element. */
+export default function Indicator(...) { ... }
+```
+
+### 1c. Single-overload util: `useVolume`
+
+```ts
+/**
+ * Subscribe to the player's volume state.
+ */
+export function useVolume(options?: { muted?: boolean }): {
+ /** Current volume level (0–1). */
+ volume: number;
+ /** Whether audio is muted. */
+ muted: boolean;
+ /** Set the volume level. */
+ setVolume: (level: number) => void;
+};
+```
+
+Exported from `packages/react/src/index.ts` → framework: `react`.
+
+### 1d. Multi-overload util: `createPlayer`
+
+```ts
+/**
+ * Create a player instance with typed store, Provider component, Container, and hooks.
+ */
+
+/** @label Video */
+export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+/** @label Audio */
+export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+```
+
+Exported from `packages/react/src/index.ts` → framework: `react`.
+
+Return types differ (`VideoPlayerStore` vs `AudioPlayerStore`). Each overload uses the optional
+`@label` JSDoc tag to give it a descriptive heading in the docs (see §2d).
+
+### 1e. Context: `playerContext`
+
+**Source** — `packages/html/src/player/context.ts`:
+
+```ts
+/** @public The default player context instance for consuming the player store in controllers. */
+export const playerContext = createContext(PLAYER_CONTEXT_KEY);
+```
+
+Exported from `packages/html/src/index.ts` → framework: `html`. Discovered via `@public` JSDoc.
+This is a non-function, non-controller export — a context value.
+
+---
+
+## 2. Builder Pipeline
+
+### 2a. Component discovery
+
+The builder scans `packages/core/src/core/ui/` for directories. For each directory with
+kebab-name `{name}`:
+
+| File | Location | Required |
+|------|----------|----------|
+| Core | `packages/core/src/core/ui/{name}/{name}-core.ts` | Yes |
+| Data attrs | `packages/core/src/core/ui/{name}/{name}-data-attrs.ts` | No |
+| HTML element | `packages/html/src/ui/{name}/{name}-element.ts` | No |
+| React parts index | `packages/react/src/ui/{name}/index.parts.ts` | No |
+
+**Kebab-to-PascalCase conversion:** `toggle-button` → `ToggleButton`. For cases where standard
+conversion fails (e.g., `pip-button` → `PiPButton`), a `NAME_OVERRIDES` map provides the correct
+PascalCase name.
+
+**Multi-part detection:** A component is multi-part if and only if `index.parts.ts` exists.
+
+### 2b. Component extraction
+
+#### Single-part components
+
+Extract from the three source files and merge into one reference object.
+
+| Source | Extracts |
+|--------|----------|
+| Core file | Props interface members (if present), State interface members (if present), `defaultProps` values (if present) |
+| Data attrs file | Data attribute names, JSDoc descriptions, and inferred types (if file exists) |
+| HTML element file | `static tagName` value (if file exists) |
+
+**Naming conventions the builder depends on:**
+
+| Symbol | Expected name |
+|--------|---------------|
+| Props interface | `{PascalCase}Props` |
+| State interface | `{PascalCase}State` |
+| Core class | `{PascalCase}Core` |
+| Data attrs export | `{PascalCase}DataAttrs` |
+| HTML element class | `{PascalCase}Element` |
+
+**All symbols are optional.** Only the Core class is required. If a component has no Props
+interface, `props` is `{}`. If it has no State interface, `state` is `{}`. If the Core class
+has no `defaultProps`, no defaults are populated. If both Props and State are missing, the
+component is skipped with a warning.
+
+**Extraction conventions** (apply to both Props and State members):
+
+- Members named `ref` are auto-skipped (React internal).
+- Members with `@ignore` JSDoc tag are skipped.
+
+**Missing-symbol behavior:**
+
+| Missing symbol | Behavior | Output |
+|---|---|---|
+| Props interface only | Silent | `props: {}` |
+| State interface only | Silent | `state: {}` |
+| Both Props and State | Warn, skip component | Component omitted |
+| `defaultProps` static | Silent | Props have no `default` field |
+| Data-attrs file/export | Silent | `dataAttributes: {}` |
+| JSDoc on a data attribute | Silent | `description: ""` (empty string) |
+| HTML element file | Silent | No `platforms.html` section |
+
+This applies uniformly to single-part and multi-part component extraction.
+
+**Data attribute type inference:**
+
+The builder infers data attribute types from the `StateAttrMap` constraint on the
+data-attrs export. Every data-attrs file follows the pattern:
+
+```ts
+export const FooDataAttrs = { ... } as const satisfies StateAttrMap;
+```
+
+The builder uses the TypeScript type checker to:
+
+1. Extract the `State` type argument from the `satisfies StateAttrMap` expression
+2. Resolve each property key's type on the state interface
+3. Format the resolved type as a display string
+
+| State property type | Inferred display type | Example |
+|---|---|---|
+| `boolean` | _(omitted)_ | `data-paused` — presence/absence, no type shown |
+| String literal union | The union | `'off' \| 'low' \| 'medium' \| 'high'` |
+| Named type alias (resolves to union) | Expanded literals | `VolumeLevel` → `'off' \| 'low' \| 'medium' \| 'high'` |
+| `string` | `string` | Rare — freeform string value |
+| `number` | `number` | Rare — numeric attribute |
+
+Boolean attributes are presence/absence by convention (the runtime uses `setAttribute`/
+`removeAttribute`), so their type is omitted from the output to avoid noise. Non-boolean
+types are included to show the enumerated values the attribute can take.
+
+**Fallback:** If the `satisfies` expression is absent or the type checker cannot resolve the
+state type, the builder falls back to the existing `@type` JSDoc tag extraction. Manual
+`@type` tags are no longer needed when the `satisfies StateAttrMap` pattern is used.
+
+#### Multi-part components
+
+The builder discovers parts from `index.parts.ts` and matches them to HTML element files.
+
+**Primary vs. sub-part convention:**
+
+Every multi-part component has one **primary part** and one or more **sub-parts**. The
+convention is file naming:
+
+- The **root element** file is `{component}-element.ts` (e.g., `time-element.ts`)
+- **Sub-part element** files are `{component}-{part}-element.ts` (e.g., `time-group-element.ts`)
+- The primary part is whichever part maps to the root element — i.e., the part that does NOT
+ have a `{component}-{part}-element.ts` file, because its element IS the root element.
+
+This is a naming convention, not configuration. The root element file always exists for the
+primary part; sub-parts always have their own element files.
+
+**Part-to-element matching:**
+
+For each named export in `index.parts.ts`:
+1. Derive kebab segment from the export's source path (e.g., `./time-value` → `value`)
+2. Look for `{component}-{part}-element.ts` in the HTML directory
+3. If found → sub-part (gets its own tag name)
+4. If not found → primary part (gets the root element's tag name from `{component}-element.ts`)
+
+**What the primary part gets:** The shared core Props, State, data attributes, and the root
+element's tag name.
+
+**What sub-parts get:** Their own tag name, a description (from React component JSDoc), and
+empty props/state/dataAttributes.
+
+**What the top-level component gets:** Empty props, state, dataAttributes, and empty platforms.
+All meaningful data lives in the `parts` record.
+
+> **Known limitation:** Our architecture has a single Props/State interface per component at the
+> core level, so only the primary part can own them. In Base UI, each part has its own props
+> independently. If a sub-part needs its own props in the future, the core architecture would
+> need per-part interfaces.
+
+### 2c. Util discovery
+
+The builder scans a fixed set of entry points:
+
+| Entry point | Framework |
+|-------------|-----------|
+| `packages/react/src/index.ts` | `react` |
+| `packages/store/src/react/hooks/index.ts` | `react` |
+| `packages/html/src/index.ts` | `html` |
+| `packages/store/src/html/controllers/index.ts` | `html` |
+| `packages/core/src/dom/store/selectors.ts` | _(none — framework-agnostic)_ |
+| `packages/store/src/core/selector.ts` | _(none — framework-agnostic)_ |
+
+**Re-export scoping:** For each entry point, the builder resolves local re-exports (relative
+import paths starting with `./`) and scans the resolved modules. External package re-exports
+(e.g., `export * from '@videojs/core/dom'`) are not followed — cross-package exports are
+discovered via their own dedicated entry points instead. This prevents duplication and
+incorrect framework tagging.
+
+**Inclusion rules** — An export is included if it matches any naming convention OR has
+`@public` JSDoc:
+
+| Pattern | Rule | Category |
+|---------|------|----------|
+| `use*` (capital 4th char) + function | Hook | `react` |
+| `select*` (capital 7th char) + function | Selector | varies |
+| `*Controller` (non-function) | Controller | `html` |
+| `create*` + function | Factory | varies |
+| `@public` JSDoc tag | Explicit inclusion | varies |
+
+**Note:** The raw TS AST fallback (used when TAE fails on a module) relaxes the function
+check for `select*` — it includes by name alone, since type information isn't reliably
+available in that path.
+
+**Display name:** The export name is used as-is, except `create*Mixin` factories strip the
+`create` prefix (e.g., `createProviderMixin` → `ProviderMixin`).
+
+**Leaf module scanning:** When an entry point has no relative re-exports (i.e., `resolveLocalModules`
+returns an empty list), the file itself is scanned directly.
+
+**Slug generation:** kebab-case of the display name. If two frameworks produce the same slug,
+React keeps the bare slug and HTML gets prefixed with `html-` (e.g., `create-player` vs
+`html-create-player`). React entries must come before HTML entries in `UTIL_ENTRY_POINTS`
+to ensure this ordering.
+
+### 2d. Util extraction
+
+**Functions (hooks, factories):** Extract call signatures via TypeScript API. Each signature
+becomes an overload with parameters and return value.
+
+**Controllers:** Extract constructor signatures. Each constructor becomes an overload. The
+return value type uses `ClassName` when the class has type parameters, or just
+`ClassName` when it has none. The return value describes the controller's public interface
+(e.g., `value` property).
+
+**Contexts (non-function, non-controller):** These are non-function, non-controller exports
+included via `@public` JSDoc. They produce a single overload with empty parameters and a
+`returnValue` containing the type string.
+
+**All overloads are preserved.** When a function or constructor has multiple overload
+signatures, each becomes a separate entry in the `overloads` array. This applies uniformly
+to function call signatures and class constructor signatures.
+
+**Overload labels (`@label`):** If an overload signature has a `@label` JSDoc tag, the
+builder extracts its value as the overload's `label`. This is optional — overloads without
+`@label` get no label and fall back to "Overload {N}" headings in the rendered output.
+
+**Degraded type repair:** After TAE extraction, the builder cross-references param and return
+types against the raw TS AST. When a formatted type contains `any` (word boundary) or `__type`
+— indicators that TAE couldn't resolve the type — the builder replaces it with the source
+annotation text from the raw AST declaration. This handles types TAE can't resolve (e.g.,
+cross-package interfaces like `Media`, ESSymbol-based types like `PlayerContext`).
+
+### 2e. Type formatting
+
+Every type string goes through two stages:
+
+**Stage 1: Format** — Convert the TypeScript type node to a human-readable string.
+
+| TS construct | Formatted as | Example |
+|--------------|-------------|---------|
+| Primitives | Lowercase name | `boolean`, `string`, `number` |
+| String literals | Single-quoted | `'current'` |
+| Unions | Pipe-separated | `'current' \| 'duration' \| 'remaining'` |
+| Objects | Inline notation | `{ volume: number; muted: boolean }` |
+| Arrays | Bracket suffix | `string[]` |
+| Functions | Arrow notation | `((level: number) => void)` |
+| Tuples | Bracket notation | `[string, number]` |
+| Empty object type | `object` | `object` (not `{}`) |
+| Type parameter (small constraint) | Constraint expansion | `string` for `T extends string` |
+| Type parameter (large constraint, >5 union members) | Parameter name | `TagName` for `TagName extends keyof JSX.IntrinsicElements` |
+
+**Stage 2: Abbreviate** — Shorten complex types for display. The abbreviated form goes in `type`;
+the full form goes in `detailedType` (shown in the disclosure panel).
+
+Abbreviation checks rules in the following order. The first match wins:
+
+| # | Rule | Condition | Abbreviated to | `detailedType` |
+|---|------|-----------|---------------|----------------|
+| 1 | Pure function | Type contains `=>`, and either no `\|` or is a single function type (paren-depth matching detects `(params) => return \| union` vs `(fn) \| undefined`) | `function` | Full signature |
+| 2 | `on*` / `get*` callback | Name matches, type contains `=>` | `function` | Full signature |
+| 3 | `className` | Name matches, type contains `=>` | `string \| function` | Full union |
+| 4 | `style` | Name matches, type contains `=>` | `CSSProperties \| function` | Full union |
+| 5 | `render` | Name matches, type contains `=>` | `ReactElement \| function` | Full union |
+| 6 | Simple primitive | `boolean`, `string`, `number` | _(no abbreviation)_ | _(omitted)_ |
+| 7 | Object literal (> 40 chars) | Starts with `{ `, length > 40 | `object` | Full inline notation |
+| 8 | Short union (< 3 members AND < 40 chars, no fn) | — | _(no abbreviation)_ | _(omitted)_ |
+| 9 | Union with a function member | Type contains `=>` and `\|` | Non-fn members `\| function` | Full union |
+| 10 | Long type (> 40 chars) | — | _(as-is, truncated)_ | Full type |
+| 11 | Fallback | Everything else | _(no abbreviation)_ | _(omitted)_ |
+
+`detailedType` is only present when abbreviation occurred. If the type is short enough to
+display as-is, `detailedType` is omitted.
+
+**Union member ordering:** `null`, `undefined`, and `any` sort to the end.
+
+**Default values:** Stored as string representations of the literal value. Examples: `'false'`,
+`"''"` (empty string), `'0'`, `'null'`, `'[]'`.
+
+---
+
+## 3. JSON Schemas
+
+These are the intermediate format between builder and front-end. Defined as Zod schemas in
+`site/src/types/`.
+
+### 3a. Component reference
+
+Output: `site/src/content/generated-component-reference/{kebab-name}.json`
+
+```
+ComponentReference
+├── name: string — PascalCase (e.g., "ToggleButton")
+├── description?: string — JSDoc description of the component
+├── props: Record — Empty {} for multi-part top-level
+├── state: Record — Empty {} for multi-part top-level
+├── dataAttributes: Record
+├── platforms
+│ └── html?
+│ └── tagName: string — e.g., "media-toggle-button"
+└── parts?: Record — Only for multi-part components
+ └── [partId]
+ ├── name: string — PascalCase part name (e.g., "Track")
+ ├── description?: string — From React component JSDoc
+ ├── props: Record
+ ├── state: Record
+ ├── dataAttributes: Record
+ └── platforms
+ └── html?
+ └── tagName: string
+```
+
+**PropDef:**
+
+```
+├── type: string — Abbreviated type for display
+├── detailedType?: string — Full type (only if abbreviated)
+├── description?: string — JSDoc description
+├── default?: string — String representation of default value
+└── required?: boolean — Only present when true
+```
+
+**StateDef:**
+
+```
+├── type: string
+├── detailedType?: string
+└── description?: string
+```
+
+**DataAttrDef:**
+
+```
+├── description: string
+├── type?: string — Inferred from state type. Omitted for boolean (presence/absence).
+│ Shows enumerated values for non-boolean types (e.g., "'empty' | 'partial' | 'full'").
+│ Abbreviated via the same rules as PropDef/StateDef (§2e Stage 2).
+└── detailedType?: string — Full type when abbreviation occurred. Same semantics as PropDef.detailedType.
+```
+
+### 3b. Util reference
+
+Output: `site/src/content/generated-util-reference/{slug}.json`
+
+```
+UtilReference
+├── name: string — Display name (e.g., "useVolume")
+├── description?: string — JSDoc description
+├── frameworks?: string[] — e.g., ["react"] or ["html"]; omitted if agnostic
+└── overloads: UtilOverload[] — At least one
+ └── [n]
+ ├── label?: string — From @label JSDoc tag (e.g., "Video")
+ ├── description?: string — Overload-specific description
+ ├── parameters: Record
+ └── returnValue: ReturnValue
+```
+
+**ParamDef** — Same shape as PropDef:
+
+```
+├── type: string
+├── detailedType?: string
+├── description?: string
+├── default?: string — Default parameter value (e.g., "0", "'muted'")
+└── required?: boolean
+```
+
+**ReturnValue:**
+
+```
+├── type: string — e.g., "object", "void", "boolean"
+├── detailedType?: string
+├── description?: string — Used when return is a simple type (no fields)
+└── fields?: Record — Used when return is an object
+ └── [fieldName]
+ ├── type: string
+ ├── detailedType?: string
+ └── description?: string
+```
+
+**Cleanup rules:** Optional fields are omitted from JSON when undefined. `required: false` is
+omitted (absence means not required). This keeps JSON files small.
+
+---
+
+## 4. Expected JSON Output for Sample Inputs
+
+### 4a. ToggleButton (single-part component)
+
+```json
+{
+ "name": "ToggleButton",
+ "props": {
+ "disabled": {
+ "type": "boolean",
+ "description": "Whether the button is disabled.",
+ "default": "false"
+ },
+ "label": {
+ "type": "string | function",
+ "detailedType": "string | ((state: ToggleButtonState) => string)",
+ "description": "Custom label for the button.",
+ "default": "''"
+ }
+ },
+ "state": {
+ "pressed": {
+ "type": "boolean",
+ "description": "Whether the toggle is pressed."
+ },
+ "disabled": {
+ "type": "boolean",
+ "description": "Whether the button is disabled."
+ }
+ },
+ "dataAttributes": {
+ "data-pressed": {
+ "description": "Present when the toggle is pressed."
+ },
+ "data-disabled": {
+ "description": "Present when the button is disabled."
+ }
+ },
+ "platforms": {
+ "html": {
+ "tagName": "media-toggle-button"
+ }
+ }
+}
+```
+
+### 4b. Meter (multi-part component)
+
+```json
+{
+ "name": "Meter",
+ "props": {},
+ "state": {},
+ "dataAttributes": {},
+ "platforms": {},
+ "parts": {
+ "indicator": {
+ "name": "Indicator",
+ "description": "A visual indicator for the current value. Renders a `` element.",
+ "props": {
+ "min": {
+ "type": "number",
+ "description": "Minimum value.",
+ "default": "0"
+ },
+ "max": {
+ "type": "number",
+ "description": "Maximum value.",
+ "default": "100"
+ },
+ "label": {
+ "type": "string | function",
+ "detailedType": "string | ((state: MeterState) => string)",
+ "description": "Custom label for accessibility.",
+ "default": "''"
+ }
+ },
+ "state": {
+ "percentage": {
+ "type": "number",
+ "description": "Current value as a percentage (0–1)."
+ }
+ },
+ "dataAttributes": {
+ "data-percentage": {
+ "description": "Current percentage as a string.",
+ "type": "number"
+ },
+ "data-fill-state": {
+ "description": "The fill level.",
+ "type": "'empty' | 'partial' | 'full'"
+ }
+ },
+ "platforms": {
+ "html": {
+ "tagName": "media-meter"
+ }
+ }
+ },
+ "track": {
+ "name": "Track",
+ "description": "The track area of the meter. Renders a `
` element.",
+ "props": {},
+ "state": {},
+ "dataAttributes": {},
+ "platforms": {
+ "html": {
+ "tagName": "media-meter-fill"
+ }
+ }
+ }
+ }
+}
+```
+
+### 4c. useVolume (single-overload hook)
+
+```json
+{
+ "name": "useVolume",
+ "description": "Subscribe to the player's volume state.",
+ "overloads": [
+ {
+ "parameters": {
+ "options": {
+ "type": "object",
+ "detailedType": "{ muted?: boolean }",
+ "default": "{}"
+ }
+ },
+ "returnValue": {
+ "type": "object",
+ "detailedType": "{ volume: number; muted: boolean; setVolume: (level: number) => void }",
+ "fields": {
+ "volume": {
+ "type": "number",
+ "description": "Current volume level (0–1)."
+ },
+ "muted": {
+ "type": "boolean",
+ "description": "Whether audio is muted."
+ },
+ "setVolume": {
+ "type": "function",
+ "detailedType": "((level: number) => void)",
+ "description": "Set the volume level."
+ }
+ }
+ }
+ }
+ ],
+ "frameworks": ["react"]
+}
+```
+
+### 4d. createPlayer (multi-overload)
+
+Both overloads are preserved. Return types differ (`VideoPlayerStore` vs `AudioPlayerStore`).
+
+```json
+{
+ "name": "createPlayer",
+ "description": "Create a player instance with typed store, Provider component, Container, and hooks.",
+ "overloads": [
+ {
+ "label": "Video",
+ "description": "Create a player instance with typed store, Provider component, Container, and hooks.",
+ "parameters": {
+ "config": {
+ "type": "CreatePlayerConfig",
+ "required": true
+ }
+ },
+ "returnValue": {
+ "type": "CreatePlayerResult",
+ "fields": {
+ "Provider": {
+ "type": "React.FC"
+ },
+ "Container": {
+ "type": "function",
+ "detailedType": "React.ForwardRefExoticComponent>"
+ },
+ "usePlayer": {
+ "type": "UsePlayerHook"
+ }
+ }
+ }
+ },
+ {
+ "label": "Audio",
+ "parameters": {
+ "config": {
+ "type": "CreatePlayerConfig",
+ "required": true
+ }
+ },
+ "returnValue": {
+ "type": "CreatePlayerResult",
+ "fields": {
+ "Provider": {
+ "type": "React.FC"
+ },
+ "Container": {
+ "type": "function",
+ "detailedType": "React.ForwardRefExoticComponent>"
+ },
+ "usePlayer": {
+ "type": "UsePlayerHook"
+ }
+ }
+ }
+ }
+ ],
+ "frameworks": ["react"]
+}
+```
+
+### 4e. playerContext (context)
+
+```json
+{
+ "name": "playerContext",
+ "description": "The default player context instance for consuming the player store in controllers.",
+ "overloads": [
+ {
+ "parameters": {},
+ "returnValue": {
+ "type": "Context"
+ }
+ }
+ ],
+ "frameworks": ["html"]
+}
+```
+
+---
+
+## 5. Reference Model Layer
+
+The reference model transforms flat JSON into a structured heading/section model. This model is
+consumed by two places:
+
+1. **Astro components** — to render headings and tables
+2. **remarkConditionalHeadings** — to inject heading entries into the table of contents
+
+Both consume the same model, which prevents anchor drift (TOC links matching rendered heading IDs).
+
+### 5a. Component reference model
+
+**Single-part** heading structure:
+
+```
+H2 "API Reference" id="api-reference"
+ ├─ H3 "Props" id="props" (if props non-empty)
+ ├─ H3 "State" id="state" (if state non-empty)
+ └─ H3 "Data attributes" id="data-attributes" (if dataAttributes non-empty)
+```
+
+**Multi-part** heading structure:
+
+```
+H2 "API Reference" id="api-reference"
+ ├─ H3 "{Part.name}" (React) id="{partId}"
+ │ or "{part.tagName}" (HTML)
+ │ ├─ H4 "Props" id="{partId}-props" (if props non-empty)
+ │ ├─ H4 "State" id="{partId}-state" (if state non-empty)
+ │ └─ H4 "Data attributes" id="{partId}-data-attributes" (if dataAttributes non-empty)
+ ├─ H3 next part...
+ └─ ...
+```
+
+Multi-part H3 headings are framework-aware: React sees the PascalCase part name (e.g., "Track"),
+HTML sees the tag name (e.g., "media-meter-track"). The TOC emits both variants with
+`frameworks` metadata so the correct one displays per framework.
+
+### 5b. Util reference model
+
+**Single-overload** heading structure:
+
+```
+H2 "API Reference" id="api-reference"
+ ├─ H3 "Parameters" id="parameters" (if params non-empty)
+ └─ H3 "Return Value" id="return-value"
+```
+
+**Multi-overload** heading structure:
+
+```
+H2 "API Reference" id="api-reference"
+ ├─ H3 "{label}" or "Overload 1" id="{slug}" or "overload-1"
+ │ ├─ H4 "Parameters" id="{slug}-parameters" (if params non-empty)
+ │ └─ H4 "Return Value" id="{slug}-return-value"
+ ├─ H3 "{label}" or "Overload 2" id="{slug}" or "overload-2"
+ │ ├─ H4 "Parameters" id="{slug}-parameters"
+ │ └─ H4 "Return Value" id="{slug}-return-value"
+ └─ ...
+```
+
+### 5c. TOC integration
+
+The `remarkConditionalHeadings` remark plugin detects `` and
+`` components in MDX, loads the generated JSON, builds the reference model, and
+injects synthetic heading entries into `frontmatter.conditionalHeadings`. These entries carry the
+same `id`/`slug` values as the rendered headings, so TOC links always match.
+
+---
+
+## 6. Rendered Output
+
+### 6a. Props table (components)
+
+Rendered by `ApiPropsTable` → `PropRow` → `DetailRow`.
+
+**Columns:**
+
+| Prop | Type | Default | |
+|------|------|---------|-|
+
+- **Prop** — Property name in monospace. Required props have an orange `*` suffix.
+- **Type** — Abbreviated type in monospace.
+- **Default** — Default value in monospace, or `—` if none.
+- **(toggle)** — Disclosure triangle. Only present if the row has a description or detailedType.
+
+**Disclosure panel** (when expanded):
+
+Contains a description list (`
`):
+- **Description** — Markdown-rendered description. Only shown if `description` is present.
+- **Type** — Full `detailedType` in monospace. Only shown if `detailedType` is present
+ (i.e., the type was abbreviated).
+
+**Sort order:** Required props first, then alphabetical. (Defined at the builder level via
+`sortProps`.)
+
+**ToggleButton example:**
+
+| Prop | Type | Default | |
+|------|------|---------|-|
+| `disabled` | `boolean` | `false` | |
+| `label` | `string \| function` | `''` | ▸ |
+
+Expanding `label`:
+> **Description:** Custom label for the button.
+> **Type:** `string | ((state: ToggleButtonState) => string)`
+
+### 6b. State table (components)
+
+Rendered by `ApiStateTable` → `StateRow` → `DetailRow`.
+
+**Columns:**
+
+| Property | Type | |
+|----------|------|-|
+
+- **Property** — State property name in monospace.
+- **Type** — Type in monospace (abbreviated if needed).
+- **(toggle)** — Disclosure triangle, same rules as props.
+
+**Disclosure panel:** Same as props (description + detailedType).
+
+**State preamble** (framework-specific, shown above the table):
+
+- **React:** "State is accessible via the `render`, `className`, and `style` props."
+- **HTML:** "State is reflected as data attributes for CSS styling."
+
+**ToggleButton example:**
+
+| Property | Type | |
+|----------|------|-|
+| `pressed` | `boolean` | ▸ |
+| `disabled` | `boolean` | ▸ |
+
+Expanding `pressed`:
+> **Description:** Whether the toggle is pressed.
+
+### 6c. Data attributes table (components)
+
+Rendered by `ApiDataAttrsTable` → `DataAttrRow` → `DetailRow`. Uses the same disclosure
+pattern as props and state tables.
+
+**Columns:**
+
+| Attribute | Type | |
+|-----------|------|-|
+
+- **Attribute** — Data attribute name in monospace (e.g., `data-pressed`).
+- **Type** — Inferred from `StateAttrMap`. Shows enumerated values in monospace for
+ non-boolean types. Empty cell for boolean (present/absent) attributes. Abbreviated via §2e
+ Stage 2 when the type is long.
+- **(toggle)** — Disclosure triangle. Only present if the row has a description or detailedType.
+
+**Disclosure panel** (when expanded):
+
+Contains a description list (`
`):
+- **Description** — Markdown-rendered description. Only shown if `description` is present.
+- **Type** — Full `detailedType` in monospace. Only shown if `detailedType` is present
+ (i.e., the type was abbreviated).
+
+**ToggleButton example** (boolean attributes — type column empty):
+
+| Attribute | Type | |
+|-----------|------|-|
+| `data-pressed` | | ▸ |
+| `data-disabled` | | ▸ |
+
+Expanding `data-pressed`:
+> **Description:** Present when the toggle is pressed.
+
+**Meter example** (mix of boolean and non-boolean — types inferred from state):
+
+| Attribute | Type | |
+|-----------|------|-|
+| `data-percentage` | `number` | ▸ |
+| `data-fill-state` | `'empty' \| 'partial' \| 'full'` | ▸ |
+
+Expanding `data-percentage`:
+> **Description:** Current percentage as a string.
+
+### 6d. Parameters table (utils)
+
+Rendered by `UtilParamsTable` → `PropRow` → `DetailRow`. Reuses the same row component as
+component props.
+
+**Columns:**
+
+| Parameter | Type | Default | |
+|-----------|------|---------|-|
+
+- **Parameter** — Parameter name in monospace. Required params have an orange `*` suffix.
+- **Type** — Abbreviated type in monospace.
+- **Default** — Default value in monospace, or `—` if none.
+- **(toggle)** — Disclosure triangle.
+
+**Disclosure panel:** Same as props (description + detailedType).
+
+**useVolume example:**
+
+| Parameter | Type | Default | |
+|-----------|------|---------|-|
+| `options` | `object` | `{}` | ▸ |
+
+Expanding `options`:
+> **Type:** `{ muted?: boolean }`
+
+### 6e. Return value (utils)
+
+Rendered by `UtilReturnTable`. Two rendering modes:
+
+**Mode 1: Object return with fields** — Renders a table using `StateRow`:
+
+| Property | Type | |
+|----------|------|-|
+
+Same columns and disclosure behavior as the state table.
+
+**useVolume example:**
+
+| Property | Type | |
+|----------|------|-|
+| `volume` | `number` | ▸ |
+| `muted` | `boolean` | ▸ |
+| `setVolume` | `function` | ▸ |
+
+Expanding `setVolume`:
+> **Description:** Set the volume level.
+> **Type:** `((level: number) => void)`
+
+**Mode 2: Simple return with detail** — When no fields but `detailedType` or `description` is
+present, renders a single-row disclosure table using `DetailRow`:
+
+| Type | |
+|------|-|
+| `function` | ▸ |
+
+Expanding the row reveals the detailed type and/or description, same as prop/state disclosure.
+
+**Mode 3: Simple return (no detail)** — When no fields, no `detailedType`, and no `description`,
+renders inline:
+
+> `ReturnType` — Description text here.
+
+### 6f. Multi-part component rendering
+
+For multi-part components, the top-level has no tables (all empty). Each part renders as:
+
+```
+H3: Part name (framework-specific label)
+ Part description (if present)
+ H4: Props (if non-empty) → Props table
+ H4: State (if non-empty) → State table
+ H4: Data attributes (if non-empty) → Data attributes table
+```
+
+**State section preamble** (framework-specific):
+
+- **React:** "State is accessible via the `render`, `className`, and `style` props."
+- **HTML:** "State is reflected as data attributes for CSS styling."
+
+### 6g. Multi-overload util rendering
+
+Each overload renders as:
+
+```
+H3: "{label}" or "Overload {N}"
+ Overload description (if present)
+ H4: Parameters (if non-empty) → Parameters table
+ H4: Return Value → Return value table or inline
+```
+
+**Heading text:** If the overload has a `label` (from `@label` JSDoc), use it as the H3
+heading text. Otherwise fall back to "Overload {N}".
+
+**Heading ID:** Labeled overloads use the kebab-case slug of the label (e.g., `"Video"` →
+`id="video"`). Unlabeled overloads use `id="overload-{n}"`.
+
+### 6h. Disclosure panel interaction
+
+The `DetailRow` component implements an expandable disclosure pattern:
+
+- **Toggle button** renders a disclosure triangle (`▸`) that rotates 90° when expanded.
+- Clicking anywhere on the summary row toggles the detail panel (unless the click target is
+ a link or button).
+- Uses `aria-expanded` and `aria-controls` for accessibility.
+- The detail panel is initially `hidden` and toggled via JavaScript.
+
+---
+
+## 7. Full Rendered Example: ToggleButton
+
+Given the ToggleButton source code from Section 1a, the user sees:
+
+```
+## API Reference
+
+### Props
+
+| Prop | Type | Default | |
+|------------|---------------------|---------|-|
+| disabled | boolean | false | |
+| label | string | function | '' | ▸ |
+
+ └─ [expanded] Description: Custom label for the button.
+ Type: string | ((state: ToggleButtonState) => string)
+
+### State
+
+State is accessible via the render, className, and style props. ← React
+State is reflected as data attributes for CSS styling. ← HTML
+
+| Property | Type | |
+|------------|---------|--|
+| pressed | boolean | ▸ |
+| disabled | boolean | ▸ |
+
+ └─ [expanded] Description: Whether the toggle is pressed.
+ └─ [expanded] Description: Whether the button is disabled.
+
+### Data attributes
+
+| Attribute | Type | |
+|-----------------|------|-|
+| data-pressed | | ▸ |
+| data-disabled | | ▸ |
+
+ └─ [expanded] Description: Present when the toggle is pressed.
+ └─ [expanded] Description: Present when the button is disabled.
+```
+
+## 8. Full Rendered Example: Meter (multi-part)
+
+Given the Meter source code from Section 1b, the user sees:
+
+```
+## API Reference
+
+### Indicator ← React framework
+### media-meter ← HTML framework
+
+A visual indicator for the current value. Renders a `` element.
+
+#### Props
+
+| Prop | Type | Default | |
+|-------|---------------------|---------|-|
+| label | string | function | '' | ▸ |
+| max | number | 100 | |
+| min | number | 0 | |
+
+#### State
+
+State is accessible via the render, className, and style props. ← React
+State is reflected as data attributes for CSS styling. ← HTML
+
+| Property | Type | |
+|------------|--------|-|
+| percentage | number | ▸ |
+
+#### Data attributes
+
+| Attribute | Type | |
+|------------------|-------------------------------|-|
+| data-percentage | number | ▸ |
+| data-fill-state | 'empty' | 'partial' | 'full' | ▸ |
+
+ └─ [expanded] Description: Current percentage as a string.
+ └─ [expanded] Description: The fill level.
+
+
+### Track ← React framework
+### media-meter-track ← HTML framework
+
+The track area of the meter. Renders a `
` element.
+
+(no Props, State, or Data attributes sections — all empty)
+
+
+### Fill ← React framework
+### media-meter-fill ← HTML framework
+
+The filled portion of the meter. Renders a `
` element.
+
+(no Props, State, or Data attributes sections — all empty)
+```
+
+## 9. Full Rendered Example: useVolume (single-overload)
+
+```
+## API Reference
+
+### Parameters
+
+| Parameter | Type | Default | |
+|-----------|--------|---------|-|
+| options | object | {} | ▸ |
+
+ └─ [expanded] Type: { muted?: boolean }
+
+### Return Value
+
+| Property | Type | |
+|-----------|----------|-|
+| volume | number | ▸ |
+| muted | boolean | ▸ |
+| setVolume | function | ▸ |
+
+ └─ [expanded] Description: Set the volume level.
+ Type: ((level: number) => void)
+```
+
+## 10. Full Rendered Example: createPlayer (multi-overload, labeled)
+
+```
+## API Reference
+
+### Video ← from @label "Video"
+
+Create a player instance with typed store, Provider component, Container, and hooks.
+
+#### Parameters
+
+| Parameter | Type | Default | |
+|-----------|-----------------------------------|---------|-|
+| config* | CreatePlayerConfig | — | |
+
+#### Return Value
+
+| Property | Type | |
+|-----------|---------------------------------|-|
+| Provider | React.FC | |
+| Container | function | ▸ |
+| usePlayer | UsePlayerHook | |
+
+ └─ [expanded] Type: React.ForwardRefExoticComponent>
+
+### Audio ← from @label "Audio"
+
+#### Parameters
+
+| Parameter | Type | Default | |
+|-----------|-----------------------------------|---------|-|
+| config* | CreatePlayerConfig | — | |
+
+#### Return Value
+
+| Property | Type | |
+|-----------|---------------------------------|-|
+| Provider | React.FC | |
+| Container | function | ▸ |
+| usePlayer | UsePlayerHook | |
+
+ └─ [expanded] Type: React.ForwardRefExoticComponent>
+```
diff --git a/packages/core/src/dom/store/selectors.ts b/packages/core/src/dom/store/selectors.ts
index 600a5ce2..3cb3d1f7 100644
--- a/packages/core/src/dom/store/selectors.ts
+++ b/packages/core/src/dom/store/selectors.ts
@@ -9,11 +9,19 @@ import { sourceFeature } from './features/source';
import { timeFeature } from './features/time';
import { volumeFeature } from './features/volume';
+/** Select the buffer state (buffered ranges, percent buffered). */
export const selectBuffer = createSelector(bufferFeature);
+/** Select the controls state (controls visible, user-active). */
export const selectControls = createSelector(controlsFeature);
+/** Select the fullscreen state (fullscreen active, availability). */
export const selectFullscreen = createSelector(fullscreenFeature);
+/** Select the PiP state (picture-in-picture active, availability). */
export const selectPiP = createSelector(pipFeature);
+/** Select the playback state (paused, ended, play, pause, toggle). */
export const selectPlayback = createSelector(playbackFeature);
+/** Select the source state (src, type). */
export const selectSource = createSelector(sourceFeature);
+/** Select the time state (currentTime, duration, seek). */
export const selectTime = createSelector(timeFeature);
+/** Select the volume state (volume, muted, setVolume, setMuted). */
export const selectVolume = createSelector(volumeFeature);
diff --git a/packages/html/src/player/context.ts b/packages/html/src/player/context.ts
index 4560b622..7f03e3de 100644
--- a/packages/html/src/player/context.ts
+++ b/packages/html/src/player/context.ts
@@ -10,4 +10,9 @@ export type PlayerContext = Context<
PlayerContextValue
>;
+/**
+ * The default player context instance for consuming the player store in controllers.
+ *
+ * @public
+ */
export const playerContext = createContext(PLAYER_CONTEXT_KEY);
diff --git a/packages/html/src/player/create-player.ts b/packages/html/src/player/create-player.ts
index b5dc8b27..ab4da3e3 100644
--- a/packages/html/src/player/create-player.ts
+++ b/packages/html/src/player/create-player.ts
@@ -67,11 +67,26 @@ export interface CreatePlayerResult {
* #playback = new PlayerController(this, context, selectPlayback);
* }
* ```
+ *
+ * @label Video
+ * @param config - Player configuration with features.
*/
export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+/**
+ * Creates a player factory for audio media.
+ *
+ * @label Audio
+ * @param config - Player configuration with features.
+ */
export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+/**
+ * Creates a player factory with custom features.
+ *
+ * @label Generic
+ * @param config - Player configuration with features.
+ */
export function createPlayer(
config: CreatePlayerConfig
): CreatePlayerResult>;
diff --git a/packages/html/src/player/player-controller.ts b/packages/html/src/player/player-controller.ts
index e6967ba3..7e9428ac 100644
--- a/packages/html/src/player/player-controller.ts
+++ b/packages/html/src/player/player-controller.ts
@@ -38,7 +38,18 @@ export class PlayerController impleme
#consumer: ContextConsumer, PlayerControllerHost>;
#store: StoreController | null = null;
+ /**
+ * @label Without Selector
+ * @param host - The host element that owns this controller.
+ * @param context - Player context to resolve the store from.
+ */
constructor(host: PlayerControllerHost, context: PlayerContext);
+ /**
+ * @label With Selector
+ * @param host - The host element that owns this controller.
+ * @param context - Player context to resolve the store from.
+ * @param selector - Derives a value from the player store state.
+ */
constructor(
host: PlayerControllerHost,
context: PlayerContext,
diff --git a/packages/html/src/player/player-mixin.ts b/packages/html/src/player/player-mixin.ts
index 1b7025bf..3ab04a3a 100644
--- a/packages/html/src/player/player-mixin.ts
+++ b/packages/html/src/player/player-mixin.ts
@@ -16,6 +16,9 @@ export type PlayerMixin = (
context: PlayerContext,
diff --git a/packages/html/src/store/container-mixin.ts b/packages/html/src/store/container-mixin.ts
index 3fdfac97..b12c5ee4 100644
--- a/packages/html/src/store/container-mixin.ts
+++ b/packages/html/src/store/container-mixin.ts
@@ -9,6 +9,11 @@ export type ContainerMixin = Class & PlayerConsumerConstructor;
+/**
+ * Create a mixin that consumes player context and auto-attaches media elements.
+ *
+ * @param context - Player context to consume from an ancestor provider.
+ */
export function createContainerMixin(context: PlayerContext): ContainerMixin {
return (BaseClass: Class) => {
class PlayerContainerElement extends BaseClass implements PlayerConsumer, MediaContainer {
diff --git a/packages/html/src/store/provider-mixin.ts b/packages/html/src/store/provider-mixin.ts
index 658c9a7a..2269a157 100644
--- a/packages/html/src/store/provider-mixin.ts
+++ b/packages/html/src/store/provider-mixin.ts
@@ -9,6 +9,12 @@ export type ProviderMixin = Class & PlayerProviderConstructor;
+/**
+ * Create a mixin that provides player context to descendant elements.
+ *
+ * @param context - Player context to provide to descendants.
+ * @param factory - Factory function that creates a store instance.
+ */
export function createProviderMixin(
context: PlayerContext,
factory: () => Store
diff --git a/packages/react/src/player/context.tsx b/packages/react/src/player/context.tsx
index bd22f655..b07d3936 100644
--- a/packages/react/src/player/context.tsx
+++ b/packages/react/src/player/context.tsx
@@ -26,24 +26,38 @@ export function PlayerContextProvider({
return {children};
}
+/** Access the full player context value. Throws if used outside a Player Provider. */
export function usePlayerContext(): PlayerContextValue {
const ctx = useContext(PlayerContext);
if (!ctx) throw new Error('usePlayerContext must be used within a Player Provider');
return ctx;
}
+/**
+ * Access the player store from within a Player Provider.
+ *
+ * @label Without Selector
+ */
export function usePlayer(): UnknownStore;
+/**
+ * Select a value from the player store. Re-renders when the selected value changes.
+ *
+ * @label With Selector
+ * @param selector - Derives a value from the player store state.
+ */
export function usePlayer(selector: (state: UnknownState) => R): R;
export function usePlayer(selector?: (state: UnknownState) => R) {
const { store } = usePlayerContext();
return useStore(store, selector as any);
}
+/** Access the media element from within a Player Provider. */
export function useMedia(): Media | null {
const { media } = usePlayerContext();
return media;
}
+/** Access the media registration setter for connecting a media element to the player. */
export function useMediaRegistration(): Dispatch> | undefined {
const ctx = useContext(PlayerContext);
return ctx?.setMedia;
diff --git a/packages/react/src/player/create-player.tsx b/packages/react/src/player/create-player.tsx
index b69a68ce..9b0aee41 100644
--- a/packages/react/src/player/create-player.tsx
+++ b/packages/react/src/player/create-player.tsx
@@ -40,10 +40,28 @@ export type UsePlayerHook = {
(selector: (state: InferStoreState) => R): R;
};
+/**
+ * Create a player instance with typed store, Provider component, Container, and hooks.
+ *
+ * @label Video
+ * @param config - Player configuration with features and optional display name.
+ */
export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+/**
+ * Create a player for audio media.
+ *
+ * @label Audio
+ * @param config - Player configuration with features and optional display name.
+ */
export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult;
+/**
+ * Create a player with custom features.
+ *
+ * @label Generic
+ * @param config - Player configuration with features and optional display name.
+ */
export function createPlayer(
config: CreatePlayerConfig
): CreatePlayerResult>;
diff --git a/packages/react/src/ui/hooks/use-button.ts b/packages/react/src/ui/hooks/use-button.ts
index 04d04396..55c11c95 100644
--- a/packages/react/src/ui/hooks/use-button.ts
+++ b/packages/react/src/ui/hooks/use-button.ts
@@ -33,6 +33,8 @@ export interface UseButtonReturnValue {
* props: [elementProps, getButtonProps],
* });
* ```
+ *
+ * @param params - Button configuration with activation handler and disabled check.
*/
export function useButton(params: UseButtonParameters): UseButtonReturnValue {
const { displayName, onActivate, isDisabled } = params;
diff --git a/packages/react/src/utils/merge-props.ts b/packages/react/src/utils/merge-props.ts
index a9b7f22b..3e81a9e4 100644
--- a/packages/react/src/utils/merge-props.ts
+++ b/packages/react/src/utils/merge-props.ts
@@ -93,6 +93,7 @@ function mergeOne(
* - style: merged objects (external wins conflicts)
* - other: last one wins
*
+ * @public
* @example
* ```ts
* const merged = mergeProps(
diff --git a/packages/react/src/utils/use-render.tsx b/packages/react/src/utils/use-render.tsx
index 08a7a157..9bc88b85 100644
--- a/packages/react/src/utils/use-render.tsx
+++ b/packages/react/src/utils/use-render.tsx
@@ -54,6 +54,7 @@ function getElementRef(element: ReactElement): Ref | undefined {
* - Ref composition
* - className/style as functions of state
*
+ * @public
* @example
* ```tsx
* return renderElement('button', componentProps, {
diff --git a/packages/store/src/core/selector.ts b/packages/store/src/core/selector.ts
index 9ed53356..87d156f2 100644
--- a/packages/store/src/core/selector.ts
+++ b/packages/store/src/core/selector.ts
@@ -19,6 +19,8 @@ const stateContext: StateContext = {
* const selectPlayback = createSelector(playbackSlice);
* selectPlayback(store.state); // { paused, play, pause, ... } | undefined
* ```
+ *
+ * @param slice - The feature slice to create a selector for.
*/
export function createSelector(slice: S): (state: object) => InferSliceState | undefined {
const initialState = slice.state(stateContext);
diff --git a/packages/store/src/html/controllers/snapshot-controller.ts b/packages/store/src/html/controllers/snapshot-controller.ts
index cf3bf4b3..23b029fc 100644
--- a/packages/store/src/html/controllers/snapshot-controller.ts
+++ b/packages/store/src/html/controllers/snapshot-controller.ts
@@ -25,7 +25,18 @@ export class SnapshotController implements ReactiveCont
#cached: R | undefined;
#unsubscribe = noop;
+ /**
+ * @label Without Selector
+ * @param host - The host element that owns this controller.
+ * @param state - The State container to subscribe to.
+ */
constructor(host: ReactiveControllerHost, state: State);
+ /**
+ * @label With Selector
+ * @param host - The host element that owns this controller.
+ * @param state - The State container to subscribe to.
+ * @param selector - Derives a value from the state.
+ */
constructor(host: ReactiveControllerHost, state: State, selector: Selector);
constructor(host: ReactiveControllerHost, state: State, selector?: Selector) {
this.#host = host;
diff --git a/packages/store/src/html/controllers/store-controller.ts b/packages/store/src/html/controllers/store-controller.ts
index 869a8c2d..a8902f98 100644
--- a/packages/store/src/html/controllers/store-controller.ts
+++ b/packages/store/src/html/controllers/store-controller.ts
@@ -45,7 +45,18 @@ export class StoreController implements
#snapshot: SnapshotController