{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tune",
  "type": "registry:ui",
  "title": "Tune",
  "description": "One line that tunes a component: useTune(name, (t) => ({ … })) returns typed live values and builds a litt Inspector for them. Every corner, space and spring is checked against litt's rules as you drag, values sit on litt's tokens once they fit, takes are saved as versions, and Copy tokens writes litt's own names. The panels show only in development unless you turn them on.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/button.json",
    "https://littmd.vercel.app/r/curve-editor.json",
    "https://littmd.vercel.app/r/inspector.json",
    "https://littmd.vercel.app/r/motion.json",
    "https://littmd.vercel.app/r/segmented.json",
    "https://littmd.vercel.app/r/select.json",
    "https://littmd.vercel.app/r/slider.json",
    "https://littmd.vercel.app/r/spring-editor.json",
    "https://littmd.vercel.app/r/switch.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/tune.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/tune 2026.11.01. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Tune: one call that tunes a component live, checked against litt's rules. useTune(name, (t) => ({ … })) returns typed\n * values and builds an Inspector for them under the nearest TuneRoot.\n *\n * - The builder is the rule. t.radius is a corner (0, the control radius, the surface radius or the pill), t.space a\n *   step of the spacing scale and t.spring a spring on its tier; each adds a Fits or Change row to the Rules group.\n *   t.easing, t.toggle, t.choice and t.number carry no rule.\n * - Values come back ready to use. A corner or a space is a CSS length: the litt token's var() while it sits on one,\n *   so the shipped component follows the theme, and a px length off them. A spring is a Motion transition that holds\n *   still under reduced motion, and an easing is [x1, y1, x2, y2] for Motion's ease.\n * - One panel per name. Every component that calls useTune with the same name reads the same values, and the first\n *   one mounted decides the fields. A control keeps only what differs from the code, so an edit to a default shows at\n *   once in every field you have not touched. Values last while the root is mounted; Save keeps a version across\n *   visits, under the name and the fields.\n * - Copy tokens writes custom properties named after the component and the key, holding litt's own tokens wherever a\n *   value sits on one. Toggles, choices and plain numbers go in a Props comment, since they belong in the call.\n * - TuneRoot draws the panels: floating top right (on a phone across the bottom, folded), or in place after its\n *   children with inline. The floating column is drawn after hydration, above the page at z-50 and under litt's\n *   toasts. It shows them in a development build only, unless enabled says otherwise. Hidden, it draws nothing and\n *   every useTune returns its defaults, but the code still ships wherever tune is imported: tune is scaffolding, and\n *   the copied tokens replace it.\n * - No accent fill (R6): actions are secondary buttons and toggles neutral switches.\n */\nimport { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { Transition } from \"motion/react\";\nimport { surfaceRadius, useDevice, useMotionDial, useTransition } from \"../../lib/litt/motion\";\nimport { cornerRule, RADIUS_PILL, SPACE_SCALE, spaceRule, springRules, type RuleRow, type SpringTier } from \"../../lib/litt/motion/rules\";\nimport { toCss, type Spring } from \"../../lib/litt/motion/spring\";\nimport type { Locked } from \"../../lib/litt/motion/tokens\";\nimport { Button } from \"./button\";\nimport { CurveEditor, toCubicBezier, type CubicBezier } from \"./curve-editor\";\nimport { Inspector, InspectorGroup } from \"./inspector\";\nimport { Segmented } from \"./segmented\";\nimport { SelectField } from \"./select\";\nimport { Slider } from \"./slider\";\nimport { SpringEditor } from \"./spring-editor\";\nimport { Switch } from \"./switch\";\n\n/* ---------- the schema ---------- */\n\ndeclare const OUT: unique symbol;\n\n/** One tuned value, made by a builder of t: its control, the rule that checks it and how Copy tokens writes it. */\nexport type TuneField<Out> = { readonly [OUT]?: Out; readonly spec: Spec };\n\n/** A button in the panel. A press runs it once in every mounted component that declares it. It holds no value. */\nexport type TuneAction = { readonly action: { readonly label: string; readonly run: () => void } };\n\n/** What the schema function returns: a field or an action per key. */\nexport type TuneSchema = { readonly [key: string]: TuneField<unknown> | TuneAction };\n\n/** What useTune returns: each field's value, typed by its builder. Actions are left out. */\nexport type TuneValues<S extends TuneSchema> = {\n  readonly [K in keyof S as S[K] extends TuneAction ? never : K]: S[K] extends TuneField<infer Out> ? Out : never;\n};\n\ntype Named = {\n  /** The control's label, which also starts its rule rows. Default: the key in words, so enterSpring is \"Enter spring\". */\n  label?: string;\n};\n\n/** The builders useTune hands the schema function as t. Each is a control and the litt rule that checks it. */\nexport type TuneKinds = {\n  /** A corner, checked against litt's corners. Start on one by name, or at px. You get a CSS length. */\n  radius(initial?: \"square\" | \"control\" | \"surface\" | \"pill\" | number, options?: Named): TuneField<string>;\n  /** A space in px, such as padding or a gap, checked against the spacing scale (0 to max, 64 by default). You get a CSS length. */\n  space(initial: number, options?: Named & { max?: number }): TuneField<string>;\n  /** A spring, starting on litt's spring for the tier and checked against its rules. You get a Motion transition. */\n  spring(tier: SpringTier, options?: Named): TuneField<Transition>;\n  /** A spring of your own, checked against the tier you name. You get a Motion transition. */\n  spring(initial: Spring, options: Named & { tier: SpringTier }): TuneField<Transition>;\n  /** An easing, starting at litt's ease-out unless you give a curve. You get [x1, y1, x2, y2] for Motion's ease. */\n  easing(initial?: \"out\" | CubicBezier, options?: Named): TuneField<CubicBezier>;\n  /** On or off, as a Switch. */\n  toggle(initial?: boolean, options?: Named & { description?: string }): TuneField<boolean>;\n  /** One of a few words: a Segmented control up to three, a Select past that. You get one of the options, typed. */\n  choice<const O extends readonly [string, ...string[]]>(options: O, initial?: O[number], more?: Named): TuneField<O[number]>;\n  /** A plain number on a Slider (0 to 100 unless you say), with no rule. With a unit, Copy tokens writes it as CSS. */\n  number(initial: number, options?: Named & { min?: number; max?: number; step?: number; unit?: string }): TuneField<number>;\n  /** A button in the panel. A press runs run in every mounted component that declares it. */\n  action(label: string, run: () => void): TuneAction;\n};\n\n/* ---------- one field ---------- */\n\n/** What the panel keeps for a field: plain JSON, so versions compare and copy it. */\ntype Json = number | boolean | string | Spring | CubicBezier;\n/** Every field's kept value, in schema order (versions compare JSON, so the order is part of it). */\ntype TuneRaw = { readonly [key: string]: Json };\ntype Still = { readonly reduced: boolean; readonly still: Transition };\n/** Custom property lines, or a few words for the Props comment when CSS cannot hold the value. */\ntype Copied = { readonly css: readonly string[] } | { readonly prop: string };\n\n/** One field holding one fitted value. Each kind's code works on its own type inside, with nothing cast. */\ntype Bound = {\n  readonly raw: Json;\n  read(motion: Still): unknown;\n  rules(label: string): RuleRow[];\n  copy(at: { readonly key: string; readonly token: string }): Copied;\n  summary?(): string;\n  control(label: string, onChange: (next: Json) => void): ReactNode;\n};\n\ntype Spec = {\n  readonly kind: \"radius\" | \"space\" | \"spring\" | \"easing\" | \"toggle\" | \"choice\" | \"number\";\n  readonly label?: string;\n  /** Tall editors get a fold of their own, open or closed; the rest stack at the top of the panel. */\n  readonly fold?: \"open\" | \"closed\";\n  /** The kind, its options and its default, so a code edit tells a field apart. */\n  readonly sig: string;\n  /** The one way in for a value from outside the code (a control, a saved version, another component's fields): fits it, else the default. */\n  bind(raw: unknown): Bound;\n};\n\nconst clamp = (n: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, n));\nconst finite = (v: unknown): v is number => typeof v === \"number\" && Number.isFinite(v);\nconst isRecord = (v: unknown): v is { readonly [key: string]: unknown } => typeof v === \"object\" && v !== null;\nconst same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);\nconst num = (n: number) => String(+n.toFixed(2));\nconst ms = (seconds: number) => `${Math.round(seconds * 1000)}ms`;\nconst px = (n: number) => `${n}px`;\n\n/* Whole token names, so the registry renames them for a host. */\nconst CORNER_VARS = { control: \"var(--litt-radius-control)\", surface: \"var(--litt-radius-surface)\", pill: \"var(--litt-radius-pill)\" } as const;\nconst TIER_VARS = {\n  responsive: { duration: \"var(--litt-dur-responsive)\", easing: \"var(--litt-ease-responsive)\" },\n  expressive: { duration: \"var(--litt-dur-expressive)\", easing: \"var(--litt-ease-expressive)\" },\n} as const;\nconst EASE_OUT_VAR = \"var(--litt-ease-out)\";\n\nconst RADIUS_MAX = 24;\n/** The radius slider's last stop, one step past its max: the pill. */\nconst PILL_STOP = RADIUS_MAX + 2;\nconst SPRING_TIME: [number, number] = [0.05, 0.8];\nconst SPRING_BOUNCE: [number, number] = [0, 0.5];\n\nconst spec = (kind: Spec[\"kind\"], named: Named | undefined, sig: unknown, bind: Spec[\"bind\"], fold?: Spec[\"fold\"]): Spec => ({\n  kind,\n  label: named?.label,\n  fold,\n  sig: JSON.stringify([kind, named?.label, sig]),\n  bind,\n});\nconst field = <Out,>(s: Spec): TuneField<Out> => ({ spec: s });\n\nfunction makeKinds(locked: Locked): TuneKinds {\n  const control = locked.radius;\n  const surface = surfaceRadius(locked.radius);\n  return {\n    radius(initial = \"surface\", named) {\n      const corners = { square: 0, control, surface, pill: RADIUS_PILL };\n      const fit = (v: unknown) => (!finite(v) ? undefined : v === RADIUS_PILL ? v : clamp(Math.round(v), 0, RADIUS_MAX));\n      const start = fit(typeof initial === \"number\" ? initial : corners[initial]) ?? surface;\n      const css = (r: number) =>\n        r === surface ? CORNER_VARS.surface : r === control ? CORNER_VARS.control : r === RADIUS_PILL ? CORNER_VARS.pill : r === 0 ? \"0\" : px(r);\n      const stop = (r: number) => (r === RADIUS_PILL ? PILL_STOP : r);\n      return field<string>(\n        spec(\"radius\", named, start, (raw) => {\n          const r = fit(raw) ?? start;\n          return {\n            raw: r,\n            read: () => css(r),\n            rules: (label) => [cornerRule(r, label)],\n            copy: ({ token }) => ({ css: [`${token}: ${css(r)};${cornerRule(r, \"\").ok ? \"\" : \" /* off litt's corners */\"}`] }),\n            control: (label, onChange) => (\n              <Slider\n                label={label}\n                value={stop(r)}\n                onValueChange={(v) => onChange(v === PILL_STOP ? RADIUS_PILL : v)}\n                min={0}\n                max={PILL_STOP}\n                step={2}\n                marks={[0, control, surface, PILL_STOP]}\n                home={stop(start)}\n                formatValue={(v) => (v === PILL_STOP ? \"Pill\" : px(v))}\n              />\n            ),\n          };\n        }),\n      );\n    },\n\n    space(initial, named) {\n      const max = Math.max(named?.max ?? 64, initial);\n      const fit = (v: unknown) => (finite(v) ? clamp(Math.round(v), 0, max) : undefined);\n      const start = fit(initial) ?? 0;\n      const css = (s: number) => {\n        const step = SPACE_SCALE.find(([v]) => v === s);\n        return step ? `var(${step[1]})` : s === 0 ? \"0\" : px(s);\n      };\n      return field<string>(\n        spec(\"space\", named, [start, max], (raw) => {\n          const s = fit(raw) ?? start;\n          return {\n            raw: s,\n            read: () => css(s),\n            rules: (label) => [spaceRule(s, label)],\n            copy: ({ token }) => ({ css: [`${token}: ${css(s)};${spaceRule(s, \"\").ok ? \"\" : \" /* off the spacing scale */\"}`] }),\n            control: (label, onChange) => (\n              <Slider\n                label={label}\n                value={s}\n                onValueChange={onChange}\n                min={0}\n                max={max}\n                step={4}\n                marks={[0, ...SPACE_SCALE.map(([v]) => v).filter((v) => v <= max)]}\n                home={start}\n                formatValue={px}\n              />\n            ),\n          };\n        }),\n      );\n    },\n\n    spring(initial: SpringTier | Spring, named?: Named & { tier?: SpringTier }) {\n      const tier = typeof initial === \"string\" ? initial : (named?.tier ?? \"expressive\");\n      const litt = locked.dial[tier];\n      const fit = (v: unknown): Spring | undefined =>\n        isRecord(v) && finite(v.time) && finite(v.bounce)\n          ? { time: +clamp(v.time, ...SPRING_TIME).toFixed(2), bounce: +clamp(v.bounce, ...SPRING_BOUNCE).toFixed(2) }\n          : undefined;\n      const start = fit(typeof initial === \"string\" ? litt : initial) ?? litt;\n      return field<Transition>(\n        spec(\n          \"spring\",\n          named,\n          [tier, start],\n          (raw) => {\n            const s = fit(raw) ?? start;\n            const onTier = same(s, litt);\n            return {\n              raw: s,\n              read: ({ reduced, still }) => (reduced ? still : { type: \"spring\", visualDuration: s.time, bounce: s.bounce }), // litt-allow: the spring being tuned is the caller's value\n              rules: (label) => springRules(s, tier).map((r) => ({ ok: r.ok, text: `${label} ${r.text.charAt(0).toLowerCase()}${r.text.slice(1)}` })),\n              copy: ({ token }) => {\n                if (onTier) return { css: [`${token}-duration: ${TIER_VARS[tier].duration};`, `${token}-ease: ${TIER_VARS[tier].easing};`] };\n                const out = toCss(s);\n                return { css: [`${token}: ${ms(s.time)} ${num(s.bounce)}; /* time and bounce, off litt's ${tier} spring */`, `${token}-duration: ${out.duration}ms;`, `${token}-ease: ${out.easing};`] };\n              },\n              summary: () => `${ms(s.time)}, ${num(s.bounce)}`,\n              /* The fold's row says the field's label, so the editor inside is just the Spring. */\n              control: (_, onChange) => (\n                <SpringEditor label=\"Spring\" value={s} onValueChange={onChange} tier={tier} checks={false} defaultMark={litt} maxTime={SPRING_TIME[1]} maxBounce={SPRING_BOUNCE[1]} height={128} preview={false} />\n              ),\n            };\n          },\n          \"open\",\n        ),\n      );\n    },\n\n    easing(initial = \"out\", named) {\n      const out = locked.easeOut;\n      const fit = (v: unknown): CubicBezier | undefined => {\n        if (!Array.isArray(v) || v.length !== 4 || !v.every(finite)) return undefined;\n        const [x1, y1, x2, y2] = v.filter(finite);\n        return [clamp(x1, 0, 1), clamp(y1, -0.25, 1.25), clamp(x2, 0, 1), clamp(y2, -0.25, 1.25)];\n      };\n      const start = fit(initial === \"out\" ? out : initial) ?? out;\n      return field<CubicBezier>(\n        spec(\n          \"easing\",\n          named,\n          start,\n          (raw) => {\n            const curve = fit(raw) ?? start;\n            const isOut = same(curve, out);\n            return {\n              raw: curve,\n              read: () => curve,\n              rules: () => [],\n              copy: ({ token }) => ({ css: [`${token}: ${isOut ? EASE_OUT_VAR : toCubicBezier(curve)};`] }),\n              summary: () => (isOut ? \"Ease out\" : \"Your own\"),\n              control: (_, onChange) => <CurveEditor label=\"Easing\" value={curve} onValueChange={onChange} defaultMark={out} height={160} preview={false} />,\n            };\n          },\n          \"closed\",\n        ),\n      );\n    },\n\n    toggle(initial = false, named) {\n      return field<boolean>(\n        spec(\"toggle\", named, [initial, named?.description], (raw) => {\n          const on = typeof raw === \"boolean\" ? raw : initial;\n          return {\n            raw: on,\n            read: () => on,\n            rules: () => [],\n            copy: ({ key }) => ({ prop: `${key} ${on}` }),\n            control: (label, onChange) => <Switch label={label} description={named?.description} align=\"end\" checked={on} onCheckedChange={onChange} />,\n          };\n        }),\n      );\n    },\n\n    choice(options, initial, named) {\n      const start = options.find((o) => o === initial) ?? options[0];\n      return field(\n        spec(\"choice\", named, [options, start], (raw) => {\n          const picked = options.find((o) => o === raw) ?? start;\n          const items = options.map((o) => ({ value: o, label: o }));\n          return {\n            raw: picked,\n            read: () => picked,\n            rules: () => [],\n            copy: ({ key }) => ({ prop: `${key} ${JSON.stringify(picked)}` }),\n            control: (label, onChange) =>\n              options.length <= 3 ? (\n                <Segmented legend={label} showLegend block size=\"sm\" options={items} value={picked} onValueChange={onChange} />\n              ) : (\n                <SelectField label={label} size=\"sm\" options={items} value={picked} onValueChange={onChange} />\n              ),\n          };\n        }),\n      );\n    },\n\n    number(initial, named) {\n      const min = Math.min(named?.min ?? 0, initial);\n      const max = Math.max(named?.max ?? 100, initial);\n      const step = named?.step ?? 1;\n      const unit = named?.unit ?? \"\";\n      const fit = (v: unknown) => (finite(v) ? clamp(v, min, max) : undefined);\n      const start = fit(initial) ?? min;\n      return field<number>(\n        spec(\"number\", named, [start, min, max, step, unit], (raw) => {\n          const n = fit(raw) ?? start;\n          return {\n            raw: n,\n            read: () => n,\n            rules: () => [],\n            copy: ({ key, token }) => (unit ? { css: [`${token}: ${num(n)}${unit};`] } : { prop: `${key} ${num(n)}` }),\n            control: (label, onChange) => (\n              <Slider label={label} value={n} onValueChange={onChange} min={min} max={max} step={step} home={start} formatValue={(v) => `${num(v)}${unit}`} />\n            ),\n          };\n        }),\n      );\n    },\n\n    action(label, run) {\n      return { action: { label, run } };\n    },\n  };\n}\n\n/* ---------- one schema ---------- */\n\ntype Field = { readonly key: string; readonly label: string; readonly spec: Spec };\ntype Compiled = {\n  readonly fields: readonly Field[];\n  readonly actions: readonly { readonly key: string; readonly label: string }[];\n  readonly defaults: TuneRaw;\n  /** The keys and their kinds: a saved version only goes back to the fields it was saved from. */\n  readonly shape: string;\n  readonly sig: string;\n};\ntype Handlers = { readonly [key: string]: () => void };\n\nconst isAction = (x: TuneField<unknown> | TuneAction): x is TuneAction => \"action\" in x;\nconst kebab = (s: string) =>\n  s\n    .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-|-$/g, \"\");\nconst words = (key: string) => {\n  const w = key.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").replace(/[-_]+/g, \" \").toLowerCase();\n  return w.charAt(0).toUpperCase() + w.slice(1);\n};\n/** A short, stable name for a shape, for the versions' storage key. */\nconst hash = (s: string) => {\n  let h = 5381;\n  for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i);\n  return (h >>> 0).toString(36);\n};\n\nfunction compile(schema: TuneSchema): Compiled {\n  const fields: Field[] = [];\n  const actions: { key: string; label: string }[] = [];\n  for (const [key, x] of Object.entries(schema)) {\n    if (isAction(x)) actions.push({ key, label: x.action.label });\n    else fields.push({ key, label: x.spec.label ?? words(key), spec: x.spec });\n  }\n  const shape = fields.map((f) => `${f.key}:${f.spec.kind}`).join(\",\");\n  return {\n    fields,\n    actions,\n    defaults: Object.fromEntries(fields.map((f) => [f.key, f.spec.bind(undefined).raw])),\n    shape,\n    sig: JSON.stringify([shape, fields.map((f) => [f.label, f.spec.sig]), actions]),\n  };\n}\n\nconst handlersOf = (schema: TuneSchema): Handlers => Object.fromEntries(Object.entries(schema).flatMap(([key, x]) => (isAction(x) ? [[key, x.action.run]] : [])));\n\n/** The caller's values, each field read from what the panel keeps, or its default. */\nfunction readAll<S extends TuneSchema>(c: Compiled, value: TuneRaw | undefined, motion: Still): TuneValues<S> {\n  const out: { [key: string]: unknown } = {};\n  for (const f of c.fields) out[f.key] = f.spec.bind(value?.[f.key]).read(motion);\n  return out as TuneValues<S>; // the one cast: an object built key by key meets the mapped type\n}\n\nfunction tokensOf(name: string, c: Compiled, value: TuneRaw): string {\n  const prefix = `--${kebab(name)}`;\n  const css: string[] = [];\n  const props: string[] = [];\n  for (const f of c.fields) {\n    const out = f.spec.bind(value[f.key]).copy({ key: f.key, token: `${prefix}-${kebab(f.key)}` });\n    if (\"css\" in out) css.push(...out.css.map((line) => `  ${line}`));\n    else props.push(out.prop);\n  }\n  return [\n    `/* ${name}, from litt tune. Paste into your CSS. */`,\n    ...(css.length ? [\":root {\", ...css, \"}\"] : []),\n    ...(props.length ? [`/* Props: ${props.join(\", \")}. */`] : []),\n  ].join(\"\\n\");\n}\n\n/* ---------- one root's values ---------- */\n\ntype Owner = { readonly compiled: Compiled; readonly handlers: { readonly current: Handlers } };\ntype Snapshot = { readonly compiled: Compiled; readonly value: TuneRaw };\ntype Entry = {\n  /** The mounted components that tune this name, in the order they arrived. The first one's fields drive the panel. */\n  readonly owners: Map<symbol, Owner>;\n  /** Only what differs from the code's defaults, so a default edited in the code shows through every untouched field. */\n  readonly overrides: Map<string, Json>;\n  snapshot: Snapshot | undefined;\n  readonly listeners: Set<() => void>;\n};\n\ntype TuneStore = ReturnType<typeof createTuneStore>;\n\n/** One TuneRoot's values by name. Components only register their fields; only the panel writes values. */\nfunction createTuneStore() {\n  const entries = new Map<string, Entry>();\n  const listListeners = new Set<() => void>();\n  let names: readonly string[] = [];\n\n  const entryOf = (name: string) => {\n    let e = entries.get(name);\n    if (!e) entries.set(name, (e = { owners: new Map(), overrides: new Map(), snapshot: undefined, listeners: new Set() }));\n    return e;\n  };\n  /** Rebuilds what everyone reads from the first owner's fields: its defaults, then each override that still fits them. */\n  const settle = (e: Entry) => {\n    const driver = e.owners.values().next().value;\n    if (!driver) return;\n    const c = driver.compiled;\n    for (const [key, v] of e.overrides) {\n      const f = c.fields.find((x) => x.key === key);\n      const fitted = f?.spec.bind(v).raw;\n      if (fitted === undefined || same(fitted, c.defaults[key])) e.overrides.delete(key);\n      else e.overrides.set(key, fitted);\n    }\n    const value = Object.fromEntries(c.fields.map((f) => [f.key, e.overrides.get(f.key) ?? c.defaults[f.key]]));\n    e.snapshot = { compiled: c, value };\n    for (const l of e.listeners) l();\n  };\n  const relist = () => {\n    const next = [...entries].filter(([, e]) => e.owners.size > 0).map(([name]) => name);\n    if (same(next, names)) return;\n    names = next;\n    for (const l of listListeners) l();\n  };\n\n  return {\n    subscribe(name: string, onChange: () => void) {\n      const e = entryOf(name);\n      e.listeners.add(onChange);\n      return () => void e.listeners.delete(onChange);\n    },\n    snapshot: (name: string) => entries.get(name)?.snapshot,\n    subscribeList(onChange: () => void) {\n      listListeners.add(onChange);\n      return () => void listListeners.delete(onChange);\n    },\n    list: () => names,\n    /** Safe to call again for the same id, as StrictMode and Fast Refresh do. Returns the way out. */\n    register(name: string, id: symbol, owner: Owner) {\n      const e = entryOf(name);\n      e.owners.set(id, owner);\n      if (e.snapshot?.compiled !== e.owners.values().next().value?.compiled) settle(e);\n      relist();\n      if (mode() === \"development\") {\n        /* After the whole commit, so a Fast Refresh that updates every copy at once does not warn. */\n        queueMicrotask(() => {\n          if (new Set([...e.owners.values()].map((o) => o.compiled.shape)).size > 1)\n            warnOnce(`fields:${name}`, `Two components tune \"${name}\" with different fields. The panel shows the first one's; give each its own name.`);\n        });\n      }\n      return () => {\n        if (e.owners.get(id) !== owner) return;\n        e.owners.delete(id);\n        if (e.owners.size > 0 && e.snapshot?.compiled !== e.owners.values().next().value?.compiled) settle(e);\n        relist();\n      };\n    },\n    /** A control's write: one field, fitted. Back at its default, it keeps nothing. */\n    set(name: string, key: string, raw: unknown) {\n      const e = entries.get(name);\n      const c = e?.snapshot?.compiled;\n      const f = c?.fields.find((x) => x.key === key);\n      if (!e || !c || !f) return;\n      e.overrides.set(key, f.spec.bind(raw).raw);\n      settle(e);\n    },\n    /** A whole value from the Inspector (a version or Reset), fitted field by field, since a saved version can be stale. */\n    replace(name: string, whole: unknown) {\n      const e = entries.get(name);\n      const c = e?.snapshot?.compiled;\n      if (!e || !c) return;\n      e.overrides.clear();\n      for (const f of c.fields) e.overrides.set(f.key, f.spec.bind(isRecord(whole) ? whole[f.key] : undefined).raw);\n      settle(e);\n    },\n    /** A press: every mounted component that declares the action runs it once. */\n    run(name: string, key: string) {\n      for (const o of entries.get(name)?.owners.values() ?? []) o.handlers.current[key]?.();\n    },\n  };\n}\n\n/* ---------- the gate ---------- */\n\n/** The bundler replaces process.env.NODE_ENV in Vite and Next.js; where nothing does, the build can't be told. */\ndeclare const process: { env: { NODE_ENV?: string } };\nfunction mode(): \"development\" | \"production\" | \"unknown\" {\n  try {\n    const env = process.env.NODE_ENV;\n    return env === \"development\" ? \"development\" : env ? \"production\" : \"unknown\";\n  } catch {\n    return \"unknown\";\n  }\n}\n\nconst warned = new Set<string>();\nfunction warnOnce(key: string, message: string) {\n  if (warned.has(key)) return;\n  warned.add(key);\n  console.warn(message);\n}\n\n/** No root above, a root that hides its panels, or a root that shows them. */\ntype Root = { readonly state: \"none\" } | { readonly state: \"off\" } | { readonly state: \"on\"; readonly store: TuneStore };\nconst TuneContext = createContext<Root>({ state: \"none\" });\n\n/* Never changes, so a snapshot that differs between server and client reads as \"after hydration\". */\nconst subscribeNever = () => () => {};\nconst NO_NAMES: readonly string[] = [];\n\n/* ---------- the hook ---------- */\n\n/**\n * Live, typed values for one component, and a panel to tune them in under the nearest TuneRoot. Without a root that\n * shows its panels (in a production build, by default), it returns the defaults.\n * @param name The panel's title, and how Copy tokens names the custom properties (\"Plan card\" starts them with plan-card). Components that share a name share one panel and its values.\n * @param schema Gets the builders as t and returns a field per value: t.radius, t.space, t.spring, t.easing, t.toggle, t.choice, t.number, and t.action for a button.\n */\nexport function useTune<S extends TuneSchema>(name: string, schema: (t: TuneKinds) => S): TuneValues<S> {\n  const root = useContext(TuneContext);\n  const store = root.state === \"on\" ? root.store : null;\n  const { locked, reduced } = useMotionDial();\n  const still = useTransition(\"instant\");\n  const t = useMemo(() => makeKinds(locked), [locked]);\n  const fresh = schema(t);\n  const next = compile(fresh);\n  /* The same fields keep the same object, so the effects below run only when the schema really changes. */\n  const compiled = useMemo(() => next, [next.sig]);\n\n  const handlers = useRef<Handlers>({});\n  useEffect(() => {\n    handlers.current = handlersOf(fresh);\n  });\n  const [id] = useState(() => Symbol(name));\n  useEffect(() => store?.register(name, id, { compiled, handlers }), [store, name, id, compiled]);\n  useEffect(() => {\n    if (root.state === \"none\" && mode() === \"development\") warnOnce(`root:${name}`, `useTune(\"${name}\") has no <TuneRoot> above it, so it returns its defaults.`);\n  }, [root.state, name]);\n\n  const subscribe = useCallback((onChange: () => void) => (store ? store.subscribe(name, onChange) : subscribeNever()), [store, name]);\n  const value = useSyncExternalStore(\n    subscribe,\n    () => store?.snapshot(name)?.value,\n    () => undefined,\n  );\n  /* still matters only while reduced, which is a dependency. */\n  return useMemo(() => readAll<S>(compiled, value, { reduced, still }), [compiled, value, reduced]);\n}\n\n/* ---------- the panels ---------- */\n\n/** One name's Inspector: short controls stacked at the top, a fold per tall editor, then the actions. */\nfunction Panel({ store, name, startFolded, className }: { store: TuneStore; name: string; startFolded: boolean; className: string }) {\n  const subscribe = useCallback((onChange: () => void) => store.subscribe(name, onChange), [store, name]);\n  const snapshot = useSyncExternalStore(\n    subscribe,\n    () => store.snapshot(name),\n    () => undefined,\n  );\n  if (!snapshot) return null;\n  const { compiled: c, value } = snapshot;\n  const bound = (f: Field) => f.spec.bind(value[f.key]);\n  const write = (f: Field) => (next: Json) => store.set(name, f.key, next);\n  const stacked = c.fields.filter((f) => !f.spec.fold);\n  const folded = c.fields.filter((f) => f.spec.fold);\n  return (\n    <Inspector\n      title={name}\n      value={value}\n      onValueChange={(v) => store.replace(name, v)}\n      defaultValue={c.defaults}\n      storageKey={`litt-tune:${name}:${hash(c.shape)}`}\n      checks={(v) => c.fields.flatMap((f) => f.spec.bind(v[f.key]).rules(f.label))}\n      tokens={(v) => tokensOf(name, c, v)}\n      defaultOpen={!startFolded}\n      className={className}\n    >\n      {stacked.length > 0 && (\n        <div className=\"grid gap-5 border-t border-litt-line px-3 pt-3 pb-4\">\n          {stacked.map((f) => (\n            <Fragment key={f.key}>{bound(f).control(f.label, write(f))}</Fragment>\n          ))}\n        </div>\n      )}\n      {folded.map((f) => (\n        <InspectorGroup key={f.key} title={f.label} summary={bound(f).summary?.()} defaultOpen={f.spec.fold === \"open\"}>\n          {bound(f).control(f.label, write(f))}\n        </InspectorGroup>\n      ))}\n      {c.actions.length > 0 && (\n        <div className=\"flex flex-wrap gap-2 border-t border-litt-line p-3\">\n          {c.actions.map((a) => (\n            <Button key={a.key} size=\"sm\" onClick={() => store.run(name, a.key)}>\n              {a.label}\n            </Button>\n          ))}\n        </div>\n      )}\n    </Inspector>\n  );\n}\n\n/* Above the page and its sticky headers, under litt's toasts (z-60); a Select opened inside comes later in the body, so it paints above. */\nconst FLOATING = \"fixed top-4 right-4 z-50 w-80 max-h-[calc(100dvh-2rem)] overflow-y-auto overscroll-contain\";\n/* On a phone the column spans the gutters at the bottom, as toasts do, and every panel starts folded, so the component stays in view. */\nconst FLOATING_COMPACT =\n  \"fixed inset-x-litt-gutter bottom-[calc(var(--litt-space-4)+env(safe-area-inset-bottom,0px))] z-50 mx-auto max-w-[40rem] max-h-[50dvh] overflow-y-auto overscroll-contain\";\n\n/** Every panel of one root, in the order their components arrived: in place, or floating over the page. */\nfunction Dock({ store, floating, className }: { store: TuneStore; floating: boolean; className: string }) {\n  const names = useSyncExternalStore(store.subscribeList, store.list, () => NO_NAMES);\n  const { compact } = useDevice();\n  if (names.length === 0) return null;\n  const place = !floating ? \"w-full max-w-80\" : compact ? FLOATING_COMPACT : FLOATING;\n  return (\n    <div className={`grid content-start gap-3 ${place} ${className}`}>\n      {names.map((name) => (\n        <Panel key={name} store={store} name={name} startFolded={floating && compact} className={floating && compact ? \"max-w-none\" : \"\"} />\n      ))}\n    </div>\n  );\n}\n\nexport type TuneRootProps = {\n  /** The part of the app whose useTune calls this root shows. It renders as it is. */\n  children: ReactNode;\n  /** Shows the panels. By default only in a development build; true shows them in production too, false hides them anywhere. */\n  enabled?: boolean;\n  /** Draws the panels in place, after the children, instead of floating top right. */\n  inline?: boolean;\n  /** Extra classes for the panels' column, such as where it sits in a grid when inline. */\n  className?: string;\n};\n\n/** Holds the tuned values of the components inside it and draws a panel per name. Mount it once, around the app. */\nexport function TuneRoot({ children, enabled, inline = false, className = \"\" }: TuneRootProps) {\n  const [store] = useState(createTuneStore);\n  const on = enabled ?? mode() === \"development\";\n  const root = useMemo<Root>(() => (on ? { state: \"on\", store } : { state: \"off\" }), [on, store]);\n  const hydrated = useSyncExternalStore(subscribeNever, () => true, () => false);\n  useEffect(() => {\n    if (enabled === undefined && mode() === \"unknown\") warnOnce(\"mode\", \"TuneRoot can't tell a development build from production here, so it hides its panels. Pass enabled to show them.\");\n  }, [enabled]);\n  return (\n    <TuneContext value={root}>\n      {children}\n      {on && inline && <Dock store={store} floating={false} className={className} />}\n      {on && !inline && hydrated && createPortal(<Dock store={store} floating className={className} />, document.body)}\n    </TuneContext>\n  );\n}\n",
      "target": "components/litt/tune.tsx"
    }
  ],
  "docs": "Import useTune and TuneRoot from components/litt/tune. Wrap the app in TuneRoot once, then call useTune with a name and fields from t (radius, space, spring, easing, toggle, choice, number, action). Pass enabled to show the panels in a production build, and inline to draw them after the children instead of floating. The code ships wherever tune is imported, hidden or not, so paste the copied tokens and remove the call once the values fit.",
  "meta": {
    "version": "2026.11.01",
    "released": "2026-11-01",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
