# Custom Controls

> Reading route: start with `workflow.md`. Core generated-app rules live in `core/*`; this file is a focused custom-control reference for the topic below.

Use a custom control only when no built-in Toolcraft control represents the product interaction.

Built-ins come first: `slider`, `rangeSlider`, `select`, `segmented`, `switch`, `checkbox`, `color`, `colorOpacity`, `vector`, `gradient`, `curves`, `fontPicker`, `imagePicker`, `fileDrop`, `text`, `code`, `rangeInput`, `palette`, `actions`, `sourceCollection`, `collectionActions`, and `panelActions`.

Register custom renderers through `ToolcraftApp controlRenderers`.

Do not use `controlRenderers` to recreate a built-in control. If the product needs a slider, select, segmented mode picker, color input, gradient editor, font picker, image upload, arbitrary file upload, textarea, local action group, source-sized repeated item editor, repeatable item add/remove, or footer action, declare the matching schema control instead of rendering the component manually.

Product modules never import deep paths below `src/toolcraft/ui/components/controls/**` and never replace schema value models with native `color`, `range`, `file`, `checkbox`, `radio`, `select`, or `textarea` controls. If a built-in lacks a required variant, improve the shared runtime instead of copying its private popover, parser, history, or state mechanics.

Do not edit `ControlsPanel`, copied `src/toolcraft`, or Toolcraft internals inside a generated app.

Import custom renderer types from `@/toolcraft/runtime/react`.

## Required Schema

Custom control schemas still need:

- `type`;
- `target`;
- `defaultValue`;
- `label`;
- `orderRole`;
- acceptance coverage;
- `customControlCoverage`;
- `builtInFitCheck`;
- browser coverage;
- performance coverage when they can trigger product work.

`builtInFitCheck` is required for every custom control acceptance row:

```ts
builtInFitCheck: {
  capabilities: [
    "collection",
    "reorder",
    "selection",
    "commands",
    "custom-value-model",
  ],
  checkedBuiltIns: ["fileDrop", "sourceCollection", "collectionActions", "imagePicker"],
  closestBuiltIn: "fileDrop",
  whyInsufficient:
    "FileDrop imports, previews, orders, and removes source files, but this product also needs per-glyph density thresholds stored with each item.",
  productObservable:
    "Changing a glyph density threshold changes which uploaded glyph renders for the same depth-map tone.",
}
```

`capabilities` is required and uses broad behavior facts: `collection`, `reorder`, `selection`, `commands`, `custom-interaction`, `custom-value-model`, or `custom-visualization`. At least one of the three `custom-*` capabilities must explain why built-ins cannot own the interaction. `checkedBuiltIns` must name real Toolcraft built-in controls. `closestBuiltIn` must be one of those checked controls or `"none"` when no built-in is meaningfully close. `whyInsufficient` explains the missing interaction. `productObservable` names the output or side effect that proves the custom control is necessary.

If the custom control owns a repeated runtime item set, `checkedBuiltIns` must include `sourceCollection` and `collectionActions` so the fit check distinguishes source-owned from user-owned cardinality. Include `actions` when the custom interaction also exposes commands. Decide this from the value model and workflow, such as arrays, `{ items: [...] }` objects, selected-item state, or add/remove/reorder behavior, not from entity names like masks or glyphs. This applies even when the empty state visually looks like a few icon buttons: the fit check must prove why neither built-in collection owner can represent the state and why command UI is necessary when commands exist.

Do not justify a custom control with icons, layout, styling, compactness, or custom buttons alone. If the built-in control has the right value model and mechanics, use it or improve that built-in instead.

## State Rules

Custom renderers must write through the provided `setValue(nextValue, meta)` callback or existing runtime commands.

Local-only custom control state is invalid unless it is transient draft, hover, focus, or drag state. Final product state belongs to the Toolcraft runtime.

## Keyframes

If a custom value is keyframe-capable, the renderer must work with runtime keyframes instead of local animation state. Store typed values in keyframes through runtime commands and consume `useToolcraftEvaluatedValues`, `useToolcraftEvaluatedValue`, `evaluateToolcraftTimelineValues`, or `evaluateToolcraftTimelineValue`.

## Visual Rules

Custom controls should use Toolcraft tokens, spacing, focus states, disabled opacity, and interaction patterns. A custom control should look like it belongs in the controls panel.

Custom controls must render the minimum UI needed to understand the value, context, and available actions. Do not add decorative metadata or text that repeats the section title, control label, or obvious item state.

Every visible custom-control element must justify its space by enabling selection, ordering, preview, removal, upload, editing, or a product-affecting status. If text is only nice-to-have, remove it.

Use Toolcraft primitives for all custom-control chrome. Do not hand-style basic buttons, inputs, selects, sliders, scroll areas, or focus states.

Custom controls may use primitives for app-specific chrome, but they must not duplicate toolbar, timeline, layers, canvas, panel, or built-in control mechanics.

Choose element sizes from interaction need, not from how much content you want to fit. Glyphs, swatches, chips, and thumbnails can be compact, but destructive, reorder, upload, and primary actions must keep comfortable kit button or icon-button sizes.

When a custom list item needs context, prefer concise semantic labels such as `Darkest`, `Mid tone`, or `Lightest`. Omit file names, long captions, and duplicate helper text unless they are required to distinguish items.

### Bounded Image And Thumbnail Lists

When an image or thumbnail grid has its own bounded scroll viewport inside the controls panel, render that viewport through `ScrollFade` with `scrollBoundaryBehavior="chain"`. The grid scrolls internally first; at its top or bottom boundary, native wheel and trackpad momentum continues through the owning controls panel.

This rule is independent of thumbnail dimensions, source-image aspect ratio, column count, fade preset, and viewport height. Keep the default contained behavior for independent selects, font lists, popovers, timeline lists, and other scroll surfaces that must not move the parent panel. Do not inspect descendants for `<img>` elements and do not forward wheel events manually.

Before choosing a custom interaction, declare its typed `interactionOwnership` and compare both surfaces. User request, inspected reference, or product usability selects one primary owner. A custom panel control must not mirror a canvas operation, and a canvas handle must not mirror a panel operation. Different operations may remain complementary: direct manipulation or selection on canvas can coexist with any useful property, mode, constraint, collection, command, or exact-value editing in the panel.
