@ambientcss/components 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +35 -0
  2. package/dist/index.cjs +1563 -384
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +763 -48
  5. package/dist/index.d.ts +763 -48
  6. package/dist/index.js +1524 -385
  7. package/dist/index.js.map +1 -1
  8. package/dist/styles.css +1102 -256
  9. package/package.json +2 -2
  10. package/src/components/AmbientButton.tsx +25 -25
  11. package/src/components/AmbientFader.tsx +33 -115
  12. package/src/components/AmbientKnob.tsx +64 -222
  13. package/src/components/AmbientPanel.tsx +8 -2
  14. package/src/components/AmbientProvider.tsx +3 -0
  15. package/src/components/AmbientRack.tsx +32 -0
  16. package/src/components/AmbientSelect.tsx +40 -0
  17. package/src/components/AmbientSlider.tsx +32 -113
  18. package/src/components/AmbientSwitch.tsx +58 -58
  19. package/src/controls/AmbientBank.tsx +138 -0
  20. package/src/controls/AmbientLatch.tsx +78 -0
  21. package/src/controls/AmbientPress.tsx +74 -0
  22. package/src/controls/AmbientRotary.tsx +94 -0
  23. package/src/controls/AmbientTravel.tsx +83 -0
  24. package/src/core/context.tsx +46 -0
  25. package/src/core/controllable.ts +33 -0
  26. package/src/core/dev.ts +15 -0
  27. package/src/core/frames.tsx +63 -0
  28. package/src/core/kit.tsx +107 -0
  29. package/src/core/material.ts +27 -0
  30. package/src/core/numeric.ts +88 -0
  31. package/src/core/types.ts +116 -0
  32. package/src/core/useBank.ts +163 -0
  33. package/src/core/useLatch.ts +54 -0
  34. package/src/core/usePress.ts +120 -0
  35. package/src/core/useRotary.ts +253 -0
  36. package/src/core/useTravel.ts +141 -0
  37. package/src/index.ts +127 -4
  38. package/src/kits/console.tsx +80 -0
  39. package/src/kits/grounded.tsx +113 -0
  40. package/src/parts/bank.tsx +32 -0
  41. package/src/parts/console.tsx +99 -0
  42. package/src/parts/knob.tsx +203 -0
  43. package/src/parts/latch.tsx +33 -0
  44. package/src/parts/press.tsx +56 -0
  45. package/src/parts/travel.tsx +70 -0
  46. package/src/styles.css +1102 -256
package/dist/index.cjs CHANGED
@@ -20,13 +20,53 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AmbientBank: () => AmbientBank,
23
24
  AmbientButton: () => AmbientButton,
24
25
  AmbientFader: () => AmbientFader,
26
+ AmbientKitProvider: () => AmbientKitProvider,
25
27
  AmbientKnob: () => AmbientKnob,
28
+ AmbientLatch: () => AmbientLatch,
26
29
  AmbientPanel: () => AmbientPanel,
30
+ AmbientPress: () => AmbientPress,
27
31
  AmbientProvider: () => AmbientProvider,
32
+ AmbientRack: () => AmbientRack,
33
+ AmbientRotary: () => AmbientRotary,
34
+ AmbientSelect: () => AmbientSelect,
28
35
  AmbientSlider: () => AmbientSlider,
29
- AmbientSwitch: () => AmbientSwitch
36
+ AmbientSwitch: () => AmbientSwitch,
37
+ AmbientTravel: () => AmbientTravel,
38
+ ButtonCap: () => ButtonCap,
39
+ ConsoleBar: () => ConsoleBar,
40
+ ConsoleKnob: () => ConsoleKnob,
41
+ ConsoleMarks: () => ConsoleMarks,
42
+ ConsoleToggle: () => ConsoleToggle,
43
+ ConsoleWell: () => ConsoleWell,
44
+ FaderCap: () => FaderCap,
45
+ IndicatorBar: () => IndicatorBar,
46
+ IndicatorDot: () => IndicatorDot,
47
+ KeyCap: () => KeyCap,
48
+ KeyLens: () => KeyLens,
49
+ KnobBody: () => KnobBody,
50
+ KnurledFace: () => KnurledFace,
51
+ Led: () => Led,
52
+ ScaleRing: () => ScaleRing,
53
+ SliderThumb: () => SliderThumb,
54
+ SwitchPill: () => SwitchPill,
55
+ SwitchTrack: () => SwitchTrack,
56
+ ToggleThumb: () => ToggleThumb,
57
+ ToggleTrack: () => ToggleTrack,
58
+ TravelTrack: () => TravelTrack,
59
+ consoleKit: () => consoleKit,
60
+ groundedKit: () => groundedKit,
61
+ useBank: () => useBank,
62
+ useBankKey: () => useBankKey,
63
+ useControlState: () => useControlState,
64
+ useDress: () => useDress,
65
+ useKit: () => useKit,
66
+ useLatch: () => useLatch,
67
+ usePress: () => usePress,
68
+ useRotary: () => useRotary,
69
+ useTravel: () => useTravel
30
70
  });
31
71
  module.exports = __toCommonJS(index_exports);
32
72
 
@@ -74,7 +114,7 @@ function AmbientPanel({ className, material = "matte", ...props }) {
74
114
  {
75
115
  className: cn(
76
116
  "ambient amb-surface amb-chamfer amb-elevation-2 ambx-panel",
77
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass",
117
+ `amb-mat-${material}`,
78
118
  className
79
119
  ),
80
120
  ...props
@@ -82,492 +122,1631 @@ function AmbientPanel({ className, material = "matte", ...props }) {
82
122
  );
83
123
  }
84
124
 
85
- // src/components/AmbientButton.tsx
125
+ // src/components/AmbientRack.tsx
86
126
  var import_jsx_runtime3 = require("react/jsx-runtime");
87
- function AmbientButton({
127
+ function AmbientRack({
88
128
  className,
89
- children,
90
- material = "matte",
91
- shape = "pill",
129
+ gap = "normal",
130
+ direction = "row",
92
131
  ...props
93
132
  }) {
94
133
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
95
- "button",
134
+ "div",
96
135
  {
97
- type: "button",
98
136
  className: cn(
99
- "amb-button amb-groove ambx-button",
100
- shape === "round" && "amb-button-round",
101
- shape === "square" && "amb-button-square",
137
+ "ambx-rack",
138
+ gap === "tight" && "ambx-rack-tight",
139
+ gap === "loose" && "ambx-rack-loose",
140
+ direction === "column" && "ambx-rack-column",
102
141
  className
103
142
  ),
104
- ...props,
105
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
106
- "span",
107
- {
108
- className: cn(
109
- "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
110
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass"
111
- ),
112
- children
113
- }
114
- )
143
+ ...props
115
144
  }
116
145
  );
117
146
  }
118
147
 
119
- // src/components/AmbientSwitch.tsx
148
+ // src/controls/AmbientPress.tsx
149
+ var import_react5 = require("react");
150
+
151
+ // src/core/context.tsx
120
152
  var import_react = require("react");
153
+ var ControlStateContext = (0, import_react.createContext)(null);
154
+ var ControlStateProvider = ControlStateContext.Provider;
155
+ function useControlState() {
156
+ const state = (0, import_react.useContext)(ControlStateContext);
157
+ if (!state) {
158
+ throw new Error(
159
+ "useControlState() must be called from inside a control's parts. Pass the component through `parts` on AmbientRotary, AmbientTravel, AmbientPress, AmbientLatch or AmbientBank."
160
+ );
161
+ }
162
+ return state;
163
+ }
164
+ var BankKeyContext = (0, import_react.createContext)(null);
165
+ var BankKeyProvider = BankKeyContext.Provider;
166
+ function useBankKey() {
167
+ const key = (0, import_react.useContext)(BankKeyContext);
168
+ if (!key) {
169
+ throw new Error(
170
+ "useBankKey() must be called from inside a bank key's parts. Pass the component through `keyParts` on AmbientBank."
171
+ );
172
+ }
173
+ return key;
174
+ }
175
+
176
+ // src/core/frames.tsx
177
+ var import_react2 = require("react");
178
+
179
+ // src/core/dev.ts
180
+ var isDev = (() => {
181
+ try {
182
+ return process.env.NODE_ENV !== "production";
183
+ } catch {
184
+ return false;
185
+ }
186
+ })();
187
+
188
+ // src/core/types.ts
189
+ var FRAME_ORDER = ["panel", "base", "actuator", "fixture"];
190
+ function stateStyle(state) {
191
+ return {
192
+ "--ambx-value": state.value,
193
+ "--ambx-percent": state.percent,
194
+ "--ambx-angle": `${state.angle}deg`,
195
+ "--ambx-travel-start": `${state.travelStart}deg`,
196
+ "--ambx-travel-sweep": `${state.travelSweep}deg`,
197
+ "--ambx-detents": state.detents
198
+ };
199
+ }
200
+ function stateData(state) {
201
+ return {
202
+ "data-dragging": state.dragging ? "" : void 0,
203
+ "data-disabled": state.disabled ? "" : void 0,
204
+ "data-at-min": state.atMin ? "" : void 0,
205
+ "data-at-max": state.atMax ? "" : void 0
206
+ };
207
+ }
208
+ function sizeProps(family, size) {
209
+ if (size === void 0) return {};
210
+ if (size === "sm" || size === "md" || size === "lg") {
211
+ return { className: `ambx-${family}-${size}` };
212
+ }
213
+ return { style: { "--ambx-size": size } };
214
+ }
215
+
216
+ // src/core/frames.tsx
121
217
  var import_jsx_runtime4 = require("react/jsx-runtime");
122
- function AmbientSwitch({
218
+ function Frames({ parts }) {
219
+ if (!parts) return null;
220
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: FRAME_ORDER.map(
221
+ (name) => parts[name] == null ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { "data-frame": name, className: `ambx-frame ambx-frame-${name}`, children: parts[name] }, name)
222
+ ) });
223
+ }
224
+ var FOCUSABLE = "a[href], button, input, select, textarea, [tabindex], [contenteditable=true],[role=button], [role=checkbox], [role=radio], [role=slider], [role=switch], [role=link]";
225
+ function useDevPartCheck(ref, control) {
226
+ const warned2 = (0, import_react2.useRef)(false);
227
+ (0, import_react2.useEffect)(() => {
228
+ if (!isDev || warned2.current) return;
229
+ const root = ref.current;
230
+ if (!root) return;
231
+ const offenders = root.querySelectorAll(`[data-frame] :is(${FOCUSABLE})`);
232
+ if (offenders.length === 0) return;
233
+ warned2.current = true;
234
+ const tags = Array.from(offenders, (node) => `<${node.tagName.toLowerCase()}>`).join(", ");
235
+ console.warn(
236
+ `[@ambientcss/components] ${control}: a part contains a focusable element (${tags}). Parts are presentational \u2014 the control root already owns the role, the tab stop and the keyboard handler, so this creates a second tab stop and usually a conflicting role.`
237
+ );
238
+ }, [ref, control]);
239
+ }
240
+
241
+ // src/core/usePress.ts
242
+ var import_react4 = require("react");
243
+
244
+ // src/core/controllable.ts
245
+ var import_react3 = require("react");
246
+ function useControllableValue(controlled, defaultValue, onChange) {
247
+ const isControlled = controlled !== void 0;
248
+ const [internal, setInternal] = (0, import_react3.useState)(defaultValue);
249
+ const onChangeRef = (0, import_react3.useRef)(onChange);
250
+ onChangeRef.current = onChange;
251
+ const controlledRef = (0, import_react3.useRef)(isControlled);
252
+ controlledRef.current = isControlled;
253
+ const set = (0, import_react3.useCallback)((next) => {
254
+ if (!controlledRef.current) setInternal(next);
255
+ onChangeRef.current?.(next);
256
+ }, []);
257
+ return [isControlled ? controlled : internal, set];
258
+ }
259
+
260
+ // src/core/usePress.ts
261
+ function usePress(options) {
262
+ const {
263
+ mode = "momentary",
264
+ repeatDelay = 400,
265
+ repeatInterval = 60,
266
+ disabled = false
267
+ } = options;
268
+ const [on, setOn] = useControllableValue(
269
+ options.value,
270
+ options.defaultValue ?? false,
271
+ options.onChange
272
+ );
273
+ const [held, setHeld] = (0, import_react4.useState)(false);
274
+ const onPressRef = (0, import_react4.useRef)(options.onPress);
275
+ onPressRef.current = options.onPress;
276
+ const timers = (0, import_react4.useRef)(
277
+ {}
278
+ );
279
+ const stopRepeat = (0, import_react4.useCallback)(() => {
280
+ if (timers.current.delay) clearTimeout(timers.current.delay);
281
+ if (timers.current.tick) clearInterval(timers.current.tick);
282
+ timers.current = {};
283
+ }, []);
284
+ (0, import_react4.useEffect)(() => stopRepeat, [stopRepeat]);
285
+ const activate = (0, import_react4.useCallback)(() => {
286
+ if (mode === "toggle") setOn(!on);
287
+ onPressRef.current?.();
288
+ }, [mode, on, setOn]);
289
+ const pressed = mode === "toggle" ? on : held;
290
+ const state = (0, import_react4.useMemo)(
291
+ () => ({
292
+ value: pressed ? 1 : 0,
293
+ min: 0,
294
+ max: 1,
295
+ percent: pressed ? 1 : 0,
296
+ angle: 0,
297
+ travelStart: 0,
298
+ travelSweep: 0,
299
+ detents: 2,
300
+ dragging: held,
301
+ disabled,
302
+ atMin: !pressed,
303
+ atMax: pressed
304
+ }),
305
+ [pressed, held, disabled]
306
+ );
307
+ const rootProps = {
308
+ type: "button",
309
+ disabled,
310
+ "aria-pressed": mode === "toggle" ? on : void 0,
311
+ style: stateStyle(state),
312
+ "data-pressed": pressed ? "" : void 0,
313
+ "data-mode": mode,
314
+ ...stateData(state),
315
+ onPointerDown: (event) => {
316
+ if (disabled || event.button !== 0) return;
317
+ setHeld(true);
318
+ if (mode !== "repeat") return;
319
+ onPressRef.current?.();
320
+ timers.current.delay = setTimeout(() => {
321
+ timers.current.tick = setInterval(() => onPressRef.current?.(), repeatInterval);
322
+ }, repeatDelay);
323
+ },
324
+ onPointerUp: () => {
325
+ setHeld(false);
326
+ stopRepeat();
327
+ },
328
+ onPointerCancel: () => {
329
+ setHeld(false);
330
+ stopRepeat();
331
+ },
332
+ onPointerLeave: () => {
333
+ setHeld(false);
334
+ stopRepeat();
335
+ },
336
+ onClick: () => {
337
+ if (mode !== "repeat") activate();
338
+ }
339
+ };
340
+ return { state, rootProps, pressed, on, setOn };
341
+ }
342
+
343
+ // src/controls/AmbientPress.tsx
344
+ var import_jsx_runtime5 = require("react/jsx-runtime");
345
+ function AmbientPress({
346
+ parts,
347
+ size,
123
348
  className,
124
- checked,
125
- defaultChecked,
126
- onCheckedChange,
127
- onClick,
128
- size = "md",
129
- label,
130
- led,
349
+ mode,
350
+ value,
351
+ defaultValue,
352
+ onChange,
353
+ onPress,
354
+ repeatDelay,
355
+ repeatInterval,
356
+ disabled,
131
357
  children,
132
- ...props
358
+ ...rest
133
359
  }) {
134
- const labelId = (0, import_react.useId)();
135
- const isControlled = checked !== void 0;
136
- const [internalChecked, setInternalChecked] = (0, import_react.useState)(defaultChecked ?? false);
137
- const active = isControlled ? checked ?? false : internalChecked;
138
- const button = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
360
+ const ref = (0, import_react5.useRef)(null);
361
+ const { state, rootProps } = usePress({
362
+ mode,
363
+ value,
364
+ defaultValue,
365
+ onChange,
366
+ onPress,
367
+ repeatDelay,
368
+ repeatInterval,
369
+ disabled
370
+ });
371
+ useDevPartCheck(ref, "AmbientPress");
372
+ const sized = sizeProps("press", size);
373
+ const { style: restStyle, onClick, ...restProps } = rest;
374
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
139
375
  "button",
140
376
  {
141
- type: "button",
142
- role: "switch",
143
- "aria-checked": active,
144
- "aria-labelledby": label ? labelId : props["aria-labelledby"],
377
+ ...restProps,
378
+ ...rootProps,
379
+ ref,
380
+ className: cn("ambx-control ambx-press", sized.className, className),
381
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
382
+ onClick: (event) => {
383
+ rootProps.onClick();
384
+ onClick?.(event);
385
+ },
386
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(ControlStateProvider, { value: state, children: [
387
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Frames, { parts }),
388
+ children
389
+ ] })
390
+ }
391
+ );
392
+ }
393
+
394
+ // src/core/kit.tsx
395
+ var import_react6 = require("react");
396
+ var import_jsx_runtime6 = require("react/jsx-runtime");
397
+ var KitContext = (0, import_react6.createContext)(null);
398
+ function AmbientKitProvider({ kit, children }) {
399
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(KitContext.Provider, { value: kit, children });
400
+ }
401
+ function useKit() {
402
+ return (0, import_react6.useContext)(KitContext);
403
+ }
404
+ var warned = /* @__PURE__ */ new Set();
405
+ function useDress(family, look, fallback) {
406
+ const kit = useKit();
407
+ const dressed = kit?.[family];
408
+ if (isDev && kit && dressed) {
409
+ const honoured = kit.looks?.[family];
410
+ if (honoured) {
411
+ for (const key of Object.keys(look)) {
412
+ if (look[key] === void 0 || honoured.includes(key)) continue;
413
+ const id = `${kit.name}:${family}:${key}`;
414
+ if (warned.has(id)) continue;
415
+ warned.add(id);
416
+ console.warn(
417
+ `[@ambientcss/components] the "${kit.name}" kit's ${family} does not use \`${key}\` \u2014 it is a look prop from another kit's vocabulary, so it will have no effect here.`
418
+ );
419
+ }
420
+ }
421
+ }
422
+ return {
423
+ dress: (dressed ?? fallback)(look),
424
+ /* Defaults come from the kit that actually dressed the control: a kit
425
+ that falls through to grounded for a family has no say in how that
426
+ family behaves either. */
427
+ defaults: dressed ? kit?.defaults?.[family] : void 0
428
+ };
429
+ }
430
+
431
+ // src/parts/knob.tsx
432
+ var import_react7 = require("react");
433
+ var import_jsx_runtime7 = require("react/jsx-runtime");
434
+ var KNURLS = {
435
+ /* 48 ribs — the referent lineup's fine knurl (referents.py knob_cap /
436
+ knob_wheel) rather than the 36 of its coarse one, because a band a tenth
437
+ of the radius wide reads as a machined grip only if the ribs are finer
438
+ than it is; at 36 the same band came out a bottle cap. */
439
+ standard: { teeth: 48, depth: 9e-3, sharpness: 1.6, band: 0.05 }
440
+ };
441
+ var KNURL_BAND = KNURLS.standard.band;
442
+ var SEAM = 5e-3;
443
+ var STEPS = 6;
444
+ function knurlPath({ teeth, depth, sharpness, band }) {
445
+ const outer = 0.5;
446
+ const inner = outer - band - SEAM;
447
+ const segs = teeth * STEPS;
448
+ const pts = [];
449
+ for (let i = 0; i < segs; i++) {
450
+ const t = i / segs * Math.PI * 2;
451
+ const r2 = outer - depth * (0.5 + 0.5 * Math.cos(teeth * t)) ** sharpness;
452
+ pts.push(
453
+ `${(0.5 + r2 * Math.cos(t)).toFixed(4)} ${(0.5 + r2 * Math.sin(t)).toFixed(4)}`
454
+ );
455
+ }
456
+ const l = (0.5 - inner).toFixed(4);
457
+ const r = (0.5 + inner).toFixed(4);
458
+ const hole = `M${l} 0.5 A${inner} ${inner} 0 0 0 ${r} 0.5 A${inner} ${inner} 0 0 0 ${l} 0.5 Z`;
459
+ return `M${pts.join(" L")} Z ${hole}`;
460
+ }
461
+ var KNURL_PATH = knurlPath(KNURLS.standard);
462
+ function KnobBody({
463
+ material,
464
+ flush = false,
465
+ className
466
+ }) {
467
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
468
+ "span",
469
+ {
145
470
  className: cn(
146
- "amb-switch",
147
- active && "amb-switch-on",
148
- "ambx-switch",
149
- `ambx-switch-${size}`,
471
+ "amb-knob-body ambient amb-thickness-2 amb-surface",
472
+ material && `amb-mat-${material}`,
150
473
  className
151
474
  ),
152
- onClick: (event) => {
153
- const next = !active;
154
- if (!isControlled) setInternalChecked(next);
155
- onCheckedChange?.(next);
156
- onClick?.(event);
475
+ style: flush ? void 0 : { inset: `${KNURL_BAND * 100}%` }
476
+ }
477
+ );
478
+ }
479
+ function KnurledFace({
480
+ material,
481
+ color,
482
+ className
483
+ }) {
484
+ const id = `amb-knurl-${(0, import_react7.useId)().replace(/:/g, "")}`;
485
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
486
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("svg", { width: 0, height: 0, style: { position: "absolute" }, "aria-hidden": true, focusable: false, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("clipPath", { id, clipPathUnits: "objectBoundingBox", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: KNURL_PATH, clipRule: "evenodd" }) }) }) }),
487
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
488
+ "span",
489
+ {
490
+ className: cn("amb-knob-face", material && `amb-mat-${material}`, className),
491
+ style: {
492
+ clipPath: `url(#${id})`,
493
+ /* Not a paint colour: --amb-albedo is the ribs' REFLECTANCE, so a
494
+ dark knurl still takes the scene's exposure, the lamp's cast and
495
+ the rim's own --amb-shade step, and still goes dark when the
496
+ lights do. Inline, so it beats the albedo a micro-relief material
497
+ would otherwise set on this element — an explicit colour wins
498
+ over the finish's own, and the finish keeps its grain. */
499
+ ...color ? { "--amb-albedo": color } : null
500
+ }
501
+ }
502
+ )
503
+ ] });
504
+ }
505
+ function IndicatorDot({ className }) {
506
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: cn("amb-knob-indicator-circle", className) });
507
+ }
508
+ function IndicatorBar({ className }) {
509
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: cn("amb-knob-indicator-rectangle", className) });
510
+ }
511
+ function ScaleRing({ count = 13, className, children }) {
512
+ const { travelStart, travelSweep } = useControlState();
513
+ const divisions = Math.max(1, count - 1);
514
+ const angles = count <= 1 ? [travelStart] : Array.from({ length: count }, (_, i) => travelStart + i / divisions * travelSweep);
515
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: cn("amb-knob-marker-ring", className), "aria-hidden": true, children: [
516
+ angles.map((angle) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
517
+ "span",
518
+ {
519
+ className: "amb-knob-marker",
520
+ style: { "--amb-marker-angle": `${angle}deg` }
157
521
  },
158
- ...props,
522
+ angle
523
+ )),
524
+ children
525
+ ] });
526
+ }
527
+
528
+ // src/parts/travel.tsx
529
+ var import_jsx_runtime8 = require("react/jsx-runtime");
530
+ function TravelTrack({
531
+ depth = "slot",
532
+ className
533
+ }) {
534
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
535
+ "span",
536
+ {
537
+ className: cn(
538
+ "amb-travel-track amb-groove",
539
+ depth === "channel" && "amb-travel-track-channel",
540
+ className
541
+ )
542
+ }
543
+ );
544
+ }
545
+ function FaderCap({
546
+ material,
547
+ className
548
+ }) {
549
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
550
+ "span",
551
+ {
552
+ className: cn(
553
+ "amb-fader-thumb ambient amb-fillet",
554
+ material !== "glass" && "amb-surface-concave",
555
+ material && `amb-mat-${material}`,
556
+ className
557
+ ),
558
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "amb-fader-gripline" })
559
+ }
560
+ );
561
+ }
562
+ function SliderThumb({
563
+ material,
564
+ className
565
+ }) {
566
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
567
+ "span",
568
+ {
569
+ className: cn(
570
+ "amb-slider-thumb ambient amb-fillet",
571
+ material !== "glass" && "amb-surface-convex",
572
+ material && `amb-mat-${material}`,
573
+ className
574
+ )
575
+ }
576
+ );
577
+ }
578
+
579
+ // src/core/material.ts
580
+ function isRelief(material) {
581
+ return material === "brushed" || material === "brushed-round" || material === "blasted";
582
+ }
583
+
584
+ // src/parts/press.tsx
585
+ var import_jsx_runtime9 = require("react/jsx-runtime");
586
+ function ButtonCap({
587
+ material = "matte",
588
+ className,
589
+ children
590
+ }) {
591
+ const relief = isRelief(material);
592
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
593
+ "span",
594
+ {
595
+ className: cn(
596
+ "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
597
+ relief ? void 0 : `amb-mat-${material}`,
598
+ className
599
+ ),
159
600
  children: [
160
- led ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
601
+ relief ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
161
602
  "span",
162
603
  {
163
- className: cn("amb-led", !active && "amb-led-off"),
164
- style: typeof led === "string" ? { "--amb-led-color": led } : void 0
604
+ className: cn("ambx-cap-face ambient amb-chamfer amb-surface", `amb-mat-${material}`),
605
+ "aria-hidden": true
165
606
  }
166
607
  ) : null,
167
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "amb-switch-track amb-groove", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "amb-switch-pill ambient amb-fillet amb-surface-convex" }) }),
168
608
  children
169
609
  ]
170
610
  }
171
611
  );
172
- if (!label) return button;
173
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "ambx-stack", children: [
174
- button,
175
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { id: labelId, className: "ambx-label", children: label })
176
- ] });
177
612
  }
178
613
 
179
- // src/components/AmbientKnob.tsx
180
- var import_react2 = require("react");
181
- var import_jsx_runtime5 = require("react/jsx-runtime");
182
- var KNURLS = {
183
- /* 36 ribs — the grounded referent knob (dot and line variants) */
184
- standard: { teeth: 36, root: 0.468, rise: 0.12, fall: [0.5, 0.62] },
185
- /* 14 broad flutes with deeper roots (OP-Z-style flute variant) */
186
- flute: { teeth: 14, root: 0.44, rise: 0.08, fall: [0.72, 0.8] },
187
- /* 48-rib fine knurl, shallower (cap and wheel variants) */
188
- fine: { teeth: 48, root: 0.476, rise: 0.12, fall: [0.5, 0.62] }
189
- };
190
- function knurlPath({ teeth, root, rise, fall }) {
191
- const outer = 0.5;
192
- const pitch = Math.PI * 2 / teeth;
193
- const pts = [];
194
- for (let i = 0; i < teeth; i++) {
195
- const a = i * pitch;
196
- const tooth = [
197
- [0, root],
198
- [rise, outer],
199
- [fall[0], outer],
200
- [fall[1], root]
201
- ];
202
- for (const [frac, radius] of tooth) {
203
- const t = a + frac * pitch;
204
- pts.push(
205
- `${(0.5 + radius * Math.cos(t)).toFixed(4)} ${(0.5 + radius * Math.sin(t)).toFixed(4)}`
206
- );
614
+ // src/parts/latch.tsx
615
+ var import_jsx_runtime10 = require("react/jsx-runtime");
616
+ function SwitchTrack({ className }) {
617
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn("amb-switch-track amb-groove", className) });
618
+ }
619
+ function SwitchPill({ className }) {
620
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn("amb-switch-pill ambient amb-fillet amb-surface-convex", className) });
621
+ }
622
+ function Led({
623
+ on = true,
624
+ color,
625
+ className
626
+ }) {
627
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
628
+ "span",
629
+ {
630
+ className: cn("amb-led", !on && "amb-led-off", className),
631
+ style: color ? { "--amb-led-color": color } : void 0
207
632
  }
208
- }
209
- return `M${pts.join(" L")} Z`;
633
+ );
210
634
  }
211
- var KNURL_PATHS = {
212
- standard: knurlPath(KNURLS.standard),
213
- flute: knurlPath(KNURLS.flute),
214
- fine: knurlPath(KNURLS.fine)
215
- };
216
- var VARIANT_FAMILY = {
217
- dot: "standard",
218
- line: "standard",
219
- flute: "flute",
220
- cap: "fine",
221
- wheel: "fine"
635
+
636
+ // src/parts/bank.tsx
637
+ var import_jsx_runtime11 = require("react/jsx-runtime");
638
+ function KeyLens({ className }) {
639
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn("amb-select-lens", className) });
640
+ }
641
+ function KeyCap({
642
+ className,
643
+ children
644
+ }) {
645
+ const { option } = useBankKey();
646
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn("amb-select-cap ambient amb-chamfer amb-mat-glass", className), children: children ?? option.label ?? option.value });
647
+ }
648
+
649
+ // src/kits/grounded.tsx
650
+ var import_jsx_runtime12 = require("react/jsx-runtime");
651
+ var FULL_MARKERS = 13;
652
+ function rotary(look) {
653
+ const material = look.material;
654
+ const knurling = look.knurling !== false;
655
+ const knurlColor = look.knurlColor;
656
+ const markers = look.markers ?? "none";
657
+ const indicator = look.indicator ?? "circle";
658
+ return {
659
+ /* The full ring reaches past the knob's own box, so its layout clearance
660
+ has to be reserved — and only the kit knows it put a ring there. */
661
+ className: cn("amb-knob", markers === "full" && "amb-knob-markers-full"),
662
+ parts: {
663
+ panel: markers === "none" ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ScaleRing, { count: markers === "ends" ? 2 : FULL_MARKERS }),
664
+ /* `material` goes on every element that paints. The cap always does —
665
+ it is the knob's top face either way — and the knurl ring does too
666
+ when there is one, so a knurled knob's rim and cap are the same
667
+ material rather than the rim being the only thing wearing it.
668
+
669
+ `knurlColor` is the one thing the ring may hold on its own: a
670
+ two-tone knob — dark grip round a pale cap — is a real piece of
671
+ hardware, and the cap has no matching prop because the cap's colour
672
+ is the control's colour, set the ordinary way with --amb-albedo. */
673
+ base: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(KnobBody, { flush: !knurling, material }),
674
+ actuator: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
675
+ knurling ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(KnurledFace, { material, color: knurlColor }) : null,
676
+ indicator === "circle" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IndicatorDot, {}) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(IndicatorBar, {})
677
+ ] })
678
+ }
679
+ };
680
+ }
681
+ function travel(look) {
682
+ const material = look.material;
683
+ const upright = look.orientation === "vertical";
684
+ return {
685
+ className: upright ? "amb-fader" : "amb-slider",
686
+ parts: {
687
+ base: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TravelTrack, { depth: upright ? "slot" : "channel" }),
688
+ actuator: upright ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(FaderCap, { material }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SliderThumb, { material })
689
+ }
690
+ };
691
+ }
692
+ function press(look) {
693
+ const shape = look.shape ?? "pill";
694
+ return {
695
+ className: cn(
696
+ "amb-button amb-groove",
697
+ shape === "round" && "amb-button-round",
698
+ shape === "square" && "amb-button-square"
699
+ ),
700
+ parts: {
701
+ actuator: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ButtonCap, { material: look.material ?? "matte", children: look.children })
702
+ }
703
+ };
704
+ }
705
+ function latch() {
706
+ return {
707
+ className: "amb-switch",
708
+ parts: { base: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SwitchTrack, {}), actuator: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SwitchPill, {}) }
709
+ };
710
+ }
711
+ function bank() {
712
+ return {
713
+ className: "amb-select amb-groove",
714
+ parts: { base: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(KeyLens, {}), actuator: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(KeyCap, {}) }
715
+ };
716
+ }
717
+ var groundedKit = {
718
+ name: "grounded",
719
+ rotary,
720
+ travel,
721
+ press,
722
+ latch,
723
+ bank,
724
+ looks: {
725
+ rotary: ["material", "knurling", "knurlColor", "markers", "indicator"],
726
+ travel: ["material", "orientation"],
727
+ press: ["material", "shape", "children"],
728
+ latch: [],
729
+ bank: []
730
+ }
222
731
  };
223
- function clamp(value, min, max) {
224
- return Math.min(max, Math.max(min, value));
732
+
733
+ // src/components/AmbientButton.tsx
734
+ var import_jsx_runtime13 = require("react/jsx-runtime");
735
+ function AmbientButton({
736
+ className,
737
+ children,
738
+ look,
739
+ material = "matte",
740
+ shape = "pill",
741
+ size = "md",
742
+ ...rest
743
+ }) {
744
+ const { dress } = useDress("press", { material, shape, children, ...look }, groundedKit.press);
745
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
746
+ AmbientPress,
747
+ {
748
+ ...rest,
749
+ size,
750
+ className: cn(dress.className, className),
751
+ parts: dress.parts
752
+ }
753
+ );
225
754
  }
226
- function AmbientKnob({
755
+
756
+ // src/components/AmbientSwitch.tsx
757
+ var import_react10 = require("react");
758
+
759
+ // src/controls/AmbientLatch.tsx
760
+ var import_react9 = require("react");
761
+
762
+ // src/core/useLatch.ts
763
+ var import_react8 = require("react");
764
+ function useLatch(options) {
765
+ const { disabled = false } = options;
766
+ const [on, setOn] = useControllableValue(
767
+ options.value,
768
+ options.defaultValue ?? false,
769
+ options.onChange
770
+ );
771
+ const state = (0, import_react8.useMemo)(
772
+ () => ({
773
+ value: on ? 1 : 0,
774
+ min: 0,
775
+ max: 1,
776
+ percent: on ? 1 : 0,
777
+ angle: 0,
778
+ travelStart: 0,
779
+ travelSweep: 0,
780
+ detents: 2,
781
+ dragging: false,
782
+ disabled,
783
+ atMin: !on,
784
+ atMax: on
785
+ }),
786
+ [on, disabled]
787
+ );
788
+ const rootProps = {
789
+ type: "button",
790
+ role: "switch",
791
+ "aria-checked": on,
792
+ disabled,
793
+ style: stateStyle(state),
794
+ ...stateData(state),
795
+ onClick: () => setOn(!on)
796
+ };
797
+ return { state, rootProps, on, setOn };
798
+ }
799
+
800
+ // src/controls/AmbientLatch.tsx
801
+ var import_jsx_runtime14 = require("react/jsx-runtime");
802
+ function AmbientLatch({
803
+ parts,
804
+ size,
805
+ animate = "auto",
806
+ label,
807
+ className,
227
808
  value,
228
- min = 0,
229
- max = 100,
230
- step = 1,
809
+ defaultValue,
810
+ onChange,
811
+ disabled,
812
+ children,
813
+ ...rest
814
+ }) {
815
+ const labelId = (0, import_react9.useId)();
816
+ const ref = (0, import_react9.useRef)(null);
817
+ const { state, rootProps } = useLatch({ value, defaultValue, onChange, disabled });
818
+ useDevPartCheck(ref, "AmbientLatch");
819
+ const sized = sizeProps("latch", size);
820
+ const { style: restStyle, onClick, ...restProps } = rest;
821
+ const control = /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
822
+ "button",
823
+ {
824
+ ...restProps,
825
+ ...rootProps,
826
+ ref,
827
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
828
+ "data-animate": animate,
829
+ className: cn("ambx-control ambx-latch", sized.className, className),
830
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
831
+ onClick: (event) => {
832
+ rootProps.onClick();
833
+ onClick?.(event);
834
+ },
835
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(ControlStateProvider, { value: state, children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Frames, { parts }),
837
+ children
838
+ ] })
839
+ }
840
+ );
841
+ if (!label) return control;
842
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ambx-stack", children: [
843
+ control,
844
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { id: labelId, className: "ambx-label", children: label })
845
+ ] });
846
+ }
847
+
848
+ // src/components/AmbientSwitch.tsx
849
+ var import_jsx_runtime15 = require("react/jsx-runtime");
850
+ function AmbientSwitch({
851
+ size = "md",
852
+ led,
231
853
  label,
232
- material,
233
- variant = "dot",
854
+ look,
855
+ value,
856
+ defaultValue,
234
857
  onChange,
858
+ animate,
235
859
  className,
236
- ...props
860
+ ...rest
237
861
  }) {
238
- const id = (0, import_react2.useId)();
239
- const clipId = `amb-knurl-${id.replace(/:/g, "")}`;
240
- const draggingRef = (0, import_react2.useRef)(false);
241
- const knobRef = (0, import_react2.useRef)(null);
242
- const safeStep = step > 0 ? step : 1;
243
- const family = VARIANT_FAMILY[variant];
244
- const percent = (value - min) / (max - min || 1);
245
- const rotation = percent * 270 - 135;
246
- const pointerToValue = (event) => {
247
- const rect = knobRef.current.getBoundingClientRect();
248
- const cx = rect.left + rect.width / 2;
249
- const cy = rect.top + rect.height / 2;
250
- const atan2Deg = Math.atan2(event.clientY - cy, event.clientX - cx) * (180 / Math.PI);
251
- let angle = atan2Deg + 90;
252
- if (angle > 180) angle -= 360;
253
- if (angle < -135 || angle > 135) {
254
- angle = percent >= 0.5 ? 135 : -135;
255
- }
256
- const range = max - min;
257
- const raw = min + (angle + 135) / 270 * range;
258
- const snapped = Math.round(raw / safeStep) * safeStep;
259
- onChange?.(clamp(snapped, min, max));
862
+ const labelId = (0, import_react10.useId)();
863
+ const [on, setOn] = useControllableValue(value, defaultValue ?? false, onChange);
864
+ const { dress, defaults } = useDress("latch", { ...look }, groundedKit.latch);
865
+ const control = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
866
+ AmbientLatch,
867
+ {
868
+ ...rest,
869
+ value: on,
870
+ onChange: setOn,
871
+ size,
872
+ animate: animate ?? defaults?.animate,
873
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
874
+ className: cn(dress.className, className),
875
+ parts: dress.parts
876
+ }
877
+ );
878
+ if (!led && !label) return control;
879
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ambx-stack", children: [
880
+ led ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "ambx-switch-mount", children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Led, { on, ...typeof led === "string" ? { color: led } : null }),
882
+ control
883
+ ] }) : control,
884
+ label ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { id: labelId, className: "ambx-label", children: label }) : null
885
+ ] });
886
+ }
887
+
888
+ // src/controls/AmbientBank.tsx
889
+ var import_react12 = require("react");
890
+
891
+ // src/core/useBank.ts
892
+ var import_react11 = require("react");
893
+ function toArray(value) {
894
+ if (value === void 0) return [];
895
+ return Array.isArray(value) ? value : [value];
896
+ }
897
+ function useBank(options) {
898
+ const { options: items, multiple = false, orientation = "vertical", disabled = false } = options;
899
+ const [raw, setRaw] = useControllableValue(
900
+ options.value,
901
+ options.defaultValue ?? (multiple ? [] : ""),
902
+ options.onChange
903
+ );
904
+ const selected = toArray(raw);
905
+ const keyRefs = (0, import_react11.useRef)([]);
906
+ const commit2 = (0, import_react11.useCallback)(
907
+ (next) => setRaw(multiple ? next : next[0] ?? ""),
908
+ [setRaw, multiple]
909
+ );
910
+ const select = (0, import_react11.useCallback)(
911
+ (option) => {
912
+ if (!option || option.disabled || disabled) return;
913
+ if (!multiple) return commit2([option.value]);
914
+ commit2(
915
+ selected.includes(option.value) ? selected.filter((v) => v !== option.value) : [...selected, option.value]
916
+ );
917
+ },
918
+ [commit2, multiple, selected, disabled]
919
+ );
920
+ const enabled = items.filter((o) => !o.disabled);
921
+ const litIndex = items.findIndex((o) => selected.includes(o.value) && !o.disabled);
922
+ const firstEnabled = items.findIndex((o) => !o.disabled);
923
+ const tabStop = multiple ? -1 : litIndex >= 0 ? litIndex : firstEnabled;
924
+ const focusAt = (index) => keyRefs.current[index]?.focus();
925
+ const step = (from, delta) => {
926
+ if (enabled.length === 0) return;
927
+ let i = from;
928
+ for (let guard = 0; guard < items.length; guard += 1) {
929
+ i = (i + delta + items.length) % items.length;
930
+ if (!items[i]?.disabled) break;
931
+ }
932
+ focusAt(i);
933
+ if (!multiple) select(items[i]);
260
934
  };
261
- const setValue = (nextValue) => {
262
- const snapped = Math.round(nextValue / safeStep) * safeStep;
263
- onChange?.(clamp(snapped, min, max));
935
+ const edge = (which) => {
936
+ const i = which === "first" ? firstEnabled : items.map((o) => !o.disabled).lastIndexOf(true);
937
+ if (i < 0) return;
938
+ focusAt(i);
939
+ if (!multiple) select(items[i]);
264
940
  };
265
- const onKeyDown = (event) => {
266
- const pageStep = safeStep * 10;
941
+ const onKeyDown = (event, index) => {
942
+ const back = orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
943
+ const forward = orientation === "vertical" ? "ArrowDown" : "ArrowRight";
267
944
  switch (event.key) {
268
- case "ArrowUp":
269
- case "ArrowRight":
270
- event.preventDefault();
271
- setValue(value + safeStep);
272
- break;
273
- case "ArrowDown":
274
- case "ArrowLeft":
945
+ case back:
275
946
  event.preventDefault();
276
- setValue(value - safeStep);
947
+ step(index, -1);
277
948
  break;
278
- case "PageUp":
949
+ case forward:
279
950
  event.preventDefault();
280
- setValue(value + pageStep);
281
- break;
282
- case "PageDown":
283
- event.preventDefault();
284
- setValue(value - pageStep);
951
+ step(index, 1);
285
952
  break;
286
953
  case "Home":
287
954
  event.preventDefault();
288
- setValue(min);
955
+ edge("first");
289
956
  break;
290
957
  case "End":
291
958
  event.preventDefault();
292
- setValue(max);
959
+ edge("last");
960
+ break;
961
+ case " ":
962
+ case "Enter":
963
+ event.preventDefault();
964
+ select(items[index]);
293
965
  break;
294
966
  default:
295
967
  break;
296
968
  }
297
969
  };
298
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
299
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
300
- "div",
301
- {
302
- ref: knobRef,
303
- className: "amb-knob ambx-knob",
304
- role: "slider",
305
- "aria-label": label,
306
- "aria-valuemin": min,
307
- "aria-valuemax": max,
308
- "aria-valuenow": value,
309
- "aria-labelledby": label ? id : void 0,
310
- "aria-orientation": "vertical",
311
- tabIndex: 0,
312
- onPointerDown: (event) => {
313
- event.currentTarget.setPointerCapture(event.pointerId);
314
- draggingRef.current = true;
315
- pointerToValue(event);
316
- },
317
- onPointerMove: (event) => {
318
- if (draggingRef.current) pointerToValue(event);
319
- },
320
- onPointerUp: () => {
321
- draggingRef.current = false;
322
- },
323
- onPointerCancel: () => {
324
- draggingRef.current = false;
325
- },
326
- onKeyDown,
327
- children: [
328
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: 0, height: 0, style: { position: "absolute" }, "aria-hidden": true, focusable: false, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("clipPath", { id: clipId, clipPathUnits: "objectBoundingBox", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: KNURL_PATHS[family] }) }) }) }),
329
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-knob-body ambient amb-thickness-2 amb-surface" }),
330
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
331
- "div",
970
+ const rootProps = {
971
+ role: multiple ? "group" : "radiogroup",
972
+ "aria-orientation": multiple ? void 0 : orientation,
973
+ "aria-disabled": disabled || void 0,
974
+ "data-orientation": orientation,
975
+ "data-disabled": disabled ? "" : void 0
976
+ };
977
+ const keyProps = (option, index) => {
978
+ const on = selected.includes(option.value);
979
+ return {
980
+ key: option.value,
981
+ type: "button",
982
+ ref: (node) => {
983
+ keyRefs.current[index] = node;
984
+ },
985
+ role: multiple ? "checkbox" : "radio",
986
+ "aria-checked": on,
987
+ "aria-label": option.ariaLabel,
988
+ title: option.ariaLabel,
989
+ disabled: option.disabled || disabled,
990
+ tabIndex: multiple ? 0 : index === tabStop ? 0 : -1,
991
+ "data-on": on ? "" : void 0,
992
+ onClick: () => select(option),
993
+ onKeyDown: (event) => onKeyDown(event, index)
994
+ };
995
+ };
996
+ return { selected, select, rootProps, keyProps };
997
+ }
998
+
999
+ // src/controls/AmbientBank.tsx
1000
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1001
+ function keyState(on, disabled) {
1002
+ return {
1003
+ value: on ? 1 : 0,
1004
+ min: 0,
1005
+ max: 1,
1006
+ percent: on ? 1 : 0,
1007
+ angle: 0,
1008
+ travelStart: 0,
1009
+ travelSweep: 0,
1010
+ detents: 2,
1011
+ dragging: false,
1012
+ disabled,
1013
+ atMin: !on,
1014
+ atMax: on
1015
+ };
1016
+ }
1017
+ function AmbientBank({
1018
+ options,
1019
+ keyParts,
1020
+ parts,
1021
+ size,
1022
+ color,
1023
+ label,
1024
+ renderKey,
1025
+ className,
1026
+ style,
1027
+ value,
1028
+ defaultValue,
1029
+ onChange,
1030
+ multiple,
1031
+ orientation = "vertical",
1032
+ disabled = false,
1033
+ ...rest
1034
+ }) {
1035
+ const labelId = (0, import_react12.useId)();
1036
+ const { selected, rootProps, keyProps } = useBank({
1037
+ options,
1038
+ value,
1039
+ defaultValue,
1040
+ onChange,
1041
+ multiple,
1042
+ orientation,
1043
+ disabled
1044
+ });
1045
+ const sized = sizeProps("bank", size);
1046
+ const bank2 = /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
1047
+ "div",
1048
+ {
1049
+ ...rest,
1050
+ ...rootProps,
1051
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1052
+ className: cn(
1053
+ "ambx-control ambx-bank",
1054
+ `ambx-bank-${orientation}`,
1055
+ sized.className,
1056
+ className
1057
+ ),
1058
+ style: {
1059
+ ...color ? { "--amb-led-color": color } : null,
1060
+ ...sized.style,
1061
+ ...style
1062
+ },
1063
+ children: [
1064
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Frames, { parts }),
1065
+ options.map((option, index) => {
1066
+ const on = selected.includes(option.value);
1067
+ const ks = keyState(on, option.disabled || disabled);
1068
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1069
+ "button",
332
1070
  {
333
- className: cn(
334
- "ambx-knob-rotation amb-knob-face",
335
- family === "flute" && "amb-knob-face-flute",
336
- family === "fine" && "amb-knob-face-fine",
337
- variant === "cap" && "amb-knob-face-cap",
338
- material && `amb-mat-${material}`
339
- ),
340
- style: { transform: `rotate(${rotation}deg)`, clipPath: `url(#${clipId})` },
341
- children: [
342
- variant === "dot" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-knob-indicator-dot" }) : null,
343
- variant === "line" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-knob-indicator-line" }) : null,
344
- variant === "flute" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-knob-indicator-dot amb-knob-indicator-dot-center" }) : null
345
- ]
1071
+ ...keyProps(option, index),
1072
+ ...stateData(ks),
1073
+ className: "ambx-key",
1074
+ style: {
1075
+ ...stateStyle(ks),
1076
+ ...option.color ? { "--amb-led-color": option.color } : null
1077
+ },
1078
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ControlStateProvider, { value: ks, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(BankKeyProvider, { value: { option, on, index }, children: renderKey ? renderKey(option, on) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Frames, { parts: keyParts }) }) })
346
1079
  }
347
- )
348
- ]
349
- }
350
- ),
351
- label ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { id, className: "ambx-label", children: label }) : null
1080
+ );
1081
+ })
1082
+ ]
1083
+ }
1084
+ );
1085
+ if (!label) return bank2;
1086
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ambx-stack", children: [
1087
+ bank2,
1088
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { id: labelId, className: "ambx-label", children: label })
352
1089
  ] });
353
1090
  }
354
1091
 
355
- // src/components/AmbientFader.tsx
356
- var import_react3 = require("react");
357
- var import_jsx_runtime6 = require("react/jsx-runtime");
358
- function clamp2(value, min, max) {
1092
+ // src/components/AmbientSelect.tsx
1093
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1094
+ function AmbientSelect({ size = "md", look, className, ...rest }) {
1095
+ const { dress } = useDress("bank", { ...look }, groundedKit.bank);
1096
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1097
+ AmbientBank,
1098
+ {
1099
+ ...rest,
1100
+ size,
1101
+ className: cn(dress.className, className),
1102
+ keyParts: dress.parts
1103
+ }
1104
+ );
1105
+ }
1106
+
1107
+ // src/controls/AmbientRotary.tsx
1108
+ var import_react14 = require("react");
1109
+
1110
+ // src/core/useRotary.ts
1111
+ var import_react13 = require("react");
1112
+
1113
+ // src/core/numeric.ts
1114
+ function clamp(value, min, max) {
359
1115
  return Math.min(max, Math.max(min, value));
360
1116
  }
361
- function AmbientFader({
362
- value,
363
- min = 0,
364
- max = 100,
365
- step = 1,
366
- label,
367
- material,
368
- onChange,
369
- className,
370
- ...props
371
- }) {
372
- const id = (0, import_react3.useId)();
373
- const trackRef = (0, import_react3.useRef)(null);
374
- const safeStep = step > 0 ? step : 1;
375
- const percent = (value - min) / (max - min || 1) * 100;
376
- const updateFromClientY = (clientY) => {
377
- const track = trackRef.current;
378
- if (!track) return;
379
- const rect = track.getBoundingClientRect();
380
- const ratio = 1 - (clientY - rect.top) / rect.height;
381
- const nextValue = min + clamp2(ratio, 0, 1) * (max - min);
382
- const snapped = Math.round(nextValue / safeStep) * safeStep;
383
- onChange?.(clamp2(snapped, min, max));
384
- };
385
- const setValue = (nextValue) => {
386
- const snapped = Math.round(nextValue / safeStep) * safeStep;
387
- onChange?.(clamp2(snapped, min, max));
388
- };
389
- const onKeyDown = (event) => {
390
- const pageStep = safeStep * 10;
1117
+ function snap(value, min, step) {
1118
+ if (!(step > 0)) return value;
1119
+ return min + Math.round((value - min) / step) * step;
1120
+ }
1121
+ function normalise(value, min, max) {
1122
+ return (value - min) / (max - min || 1);
1123
+ }
1124
+ function denormalise(t, min, max) {
1125
+ return min + t * (max - min);
1126
+ }
1127
+ function commit(value, min, max, step) {
1128
+ return clamp(snap(value, min, max === min ? 1 : step), min, max);
1129
+ }
1130
+ function valueKeyHandler(options) {
1131
+ return (event) => {
1132
+ const { value, min, max, step, invert, onChange, disabled } = options;
1133
+ if (disabled) return false;
1134
+ const dir = invert ? -1 : 1;
1135
+ const set = (next) => {
1136
+ event.preventDefault();
1137
+ onChange(commit(next, min, max, step));
1138
+ };
391
1139
  switch (event.key) {
392
1140
  case "ArrowUp":
393
1141
  case "ArrowRight":
394
- event.preventDefault();
395
- setValue(value + safeStep);
396
- break;
1142
+ set(value + step * dir);
1143
+ return true;
397
1144
  case "ArrowDown":
398
1145
  case "ArrowLeft":
399
- event.preventDefault();
400
- setValue(value - safeStep);
401
- break;
1146
+ set(value - step * dir);
1147
+ return true;
402
1148
  case "PageUp":
403
- event.preventDefault();
404
- setValue(value + pageStep);
405
- break;
1149
+ set(value + step * 10 * dir);
1150
+ return true;
406
1151
  case "PageDown":
407
- event.preventDefault();
408
- setValue(value - pageStep);
409
- break;
1152
+ set(value - step * 10 * dir);
1153
+ return true;
410
1154
  case "Home":
411
- event.preventDefault();
412
- setValue(min);
413
- break;
1155
+ set(min);
1156
+ return true;
414
1157
  case "End":
415
- event.preventDefault();
416
- setValue(max);
417
- break;
1158
+ set(max);
1159
+ return true;
418
1160
  default:
419
- break;
1161
+ return false;
1162
+ }
1163
+ };
1164
+ }
1165
+
1166
+ // src/core/useRotary.ts
1167
+ var DEFAULT_TRAVEL = { start: -135, sweep: 270 };
1168
+ function wrapDeg(deg) {
1169
+ return ((deg + 180) % 360 + 360) % 360 - 180;
1170
+ }
1171
+ function resolveTravel(travel2) {
1172
+ if (travel2 === void 0) return DEFAULT_TRAVEL;
1173
+ if (typeof travel2 === "number") return { start: -travel2 / 2, sweep: travel2 };
1174
+ return travel2;
1175
+ }
1176
+ var warnedFullTurn = false;
1177
+ function capturePointer(target, pointerId) {
1178
+ try {
1179
+ target.setPointerCapture?.(pointerId);
1180
+ } catch {
1181
+ }
1182
+ }
1183
+ function useRotary(options) {
1184
+ const {
1185
+ min = 0,
1186
+ max = 100,
1187
+ step = 1,
1188
+ dragDistance = 200,
1189
+ wrap = false,
1190
+ disabled = false
1191
+ } = options;
1192
+ const { start, sweep } = resolveTravel(options.travel);
1193
+ let input = options.input ?? "drag";
1194
+ if (input === "angle" && Math.abs(sweep) >= 360) {
1195
+ if (isDev && !warnedFullTurn) {
1196
+ warnedFullTurn = true;
1197
+ console.warn(
1198
+ `[@ambientcss/components] input="angle" needs a sweep under a full turn (got ${sweep}deg): an absolute angle is ambiguous at 360deg, where the first and last values share a screen position. Falling back to "drag".`
1199
+ );
1200
+ }
1201
+ input = "drag";
1202
+ }
1203
+ const [value, setValue] = useControllableValue(
1204
+ options.value,
1205
+ options.defaultValue ?? min,
1206
+ options.onChange
1207
+ );
1208
+ const [dragging, setDragging] = (0, import_react13.useState)(false);
1209
+ const draggingRef = (0, import_react13.useRef)(false);
1210
+ const rootRef = (0, import_react13.useRef)(null);
1211
+ const drag = (0, import_react13.useRef)({ value: 0, clientY: 0, angle: 0, accum: 0 });
1212
+ const range = max - min;
1213
+ const keyStep = step > 0 ? step : range / 100 || 1;
1214
+ const percent = clamp(normalise(value, min, max), 0, 1);
1215
+ const angle = start + percent * sweep;
1216
+ const set = (0, import_react13.useCallback)(
1217
+ (next) => setValue(commit(next, min, max, step)),
1218
+ [setValue, min, max, step]
1219
+ );
1220
+ const pointerAngle = (event) => {
1221
+ const rect = rootRef.current?.getBoundingClientRect();
1222
+ if (!rect) return null;
1223
+ const dx = event.clientX - (rect.left + rect.width / 2);
1224
+ const dy = event.clientY - (rect.top + rect.height / 2);
1225
+ return Math.atan2(dy, dx) * (180 / Math.PI) + 90;
1226
+ };
1227
+ const fromAngle = (event) => {
1228
+ const raw = pointerAngle(event);
1229
+ if (raw === null) return;
1230
+ const mid = start + sweep / 2;
1231
+ let a = mid + wrapDeg(raw - mid);
1232
+ const end = start + sweep;
1233
+ if (a < Math.min(start, end) || a > Math.max(start, end)) {
1234
+ const toStart = Math.abs(wrapDeg(a - start));
1235
+ const toEnd = Math.abs(wrapDeg(a - end));
1236
+ a = toStart <= toEnd ? start : end;
1237
+ }
1238
+ set(denormalise((a - start) / (sweep || 1), min, max));
1239
+ };
1240
+ const fromDrag = (event) => {
1241
+ const dy = drag.current.clientY - event.clientY;
1242
+ drag.current.clientY = event.clientY;
1243
+ drag.current.accum += dy * (event.shiftKey ? 0.25 : 1);
1244
+ set(drag.current.value + drag.current.accum / dragDistance * range);
1245
+ };
1246
+ const fromDelta = (event) => {
1247
+ const raw = pointerAngle(event);
1248
+ if (raw === null) return;
1249
+ drag.current.accum += wrapDeg(raw - drag.current.angle);
1250
+ drag.current.angle = raw;
1251
+ const next = drag.current.value + drag.current.accum / (sweep || 360) * range;
1252
+ set(wrap && range > 0 ? min + ((next - min) % range + range) % range : next);
1253
+ };
1254
+ const track = (event) => {
1255
+ if (input === "angle") fromAngle(event);
1256
+ else if (input === "delta") fromDelta(event);
1257
+ else fromDrag(event);
1258
+ };
1259
+ const state = (0, import_react13.useMemo)(
1260
+ () => ({
1261
+ value,
1262
+ min,
1263
+ max,
1264
+ percent,
1265
+ angle,
1266
+ travelStart: start,
1267
+ travelSweep: sweep,
1268
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1269
+ dragging,
1270
+ disabled,
1271
+ atMin: value <= min,
1272
+ atMax: value >= max
1273
+ }),
1274
+ [value, min, max, percent, angle, start, sweep, options.detents, step, range, dragging, disabled]
1275
+ );
1276
+ const onKeyDown = valueKeyHandler({ value, min, max, step: keyStep, disabled, onChange: setValue });
1277
+ const rootProps = {
1278
+ ref: rootRef,
1279
+ role: "slider",
1280
+ "aria-valuemin": min,
1281
+ "aria-valuemax": max,
1282
+ "aria-valuenow": value,
1283
+ "aria-orientation": "vertical",
1284
+ "aria-disabled": disabled || void 0,
1285
+ tabIndex: disabled ? -1 : 0,
1286
+ style: stateStyle(state),
1287
+ ...stateData(state),
1288
+ onPointerDown: (event) => {
1289
+ if (disabled || event.button !== 0) return;
1290
+ capturePointer(event.currentTarget, event.pointerId);
1291
+ draggingRef.current = true;
1292
+ setDragging(true);
1293
+ drag.current = {
1294
+ value,
1295
+ clientY: event.clientY,
1296
+ angle: pointerAngle(event) ?? 0,
1297
+ accum: 0
1298
+ };
1299
+ if (input === "angle") fromAngle(event);
1300
+ },
1301
+ onPointerMove: (event) => {
1302
+ if (!draggingRef.current || disabled) return;
1303
+ track(event);
1304
+ },
1305
+ onPointerUp: () => {
1306
+ draggingRef.current = false;
1307
+ setDragging(false);
1308
+ },
1309
+ onPointerCancel: () => {
1310
+ draggingRef.current = false;
1311
+ setDragging(false);
1312
+ },
1313
+ onKeyDown: (event) => {
1314
+ onKeyDown(event);
1315
+ }
1316
+ };
1317
+ return { state, rootProps, setValue: set };
1318
+ }
1319
+
1320
+ // src/controls/AmbientRotary.tsx
1321
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1322
+ function AmbientRotary({
1323
+ parts,
1324
+ size,
1325
+ animate = "auto",
1326
+ label,
1327
+ className,
1328
+ value,
1329
+ defaultValue,
1330
+ min,
1331
+ max,
1332
+ step,
1333
+ detents,
1334
+ travel: travel2,
1335
+ input,
1336
+ dragDistance,
1337
+ wrap,
1338
+ disabled,
1339
+ onChange,
1340
+ ...rest
1341
+ }) {
1342
+ const labelId = (0, import_react14.useId)();
1343
+ const stackRef = (0, import_react14.useRef)(null);
1344
+ const { state, rootProps } = useRotary({
1345
+ value,
1346
+ defaultValue,
1347
+ min,
1348
+ max,
1349
+ step,
1350
+ detents,
1351
+ travel: travel2,
1352
+ input,
1353
+ dragDistance,
1354
+ wrap,
1355
+ disabled,
1356
+ onChange
1357
+ });
1358
+ useDevPartCheck(stackRef, "AmbientRotary");
1359
+ const sized = sizeProps("rotary", size);
1360
+ const { style: restStyle, ...restProps } = rest;
1361
+ const control = /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
1362
+ "div",
1363
+ {
1364
+ ...restProps,
1365
+ ...rootProps,
1366
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1367
+ "data-animate": animate,
1368
+ className: cn("ambx-control ambx-rotary", sized.className, className),
1369
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1370
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ControlStateProvider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Frames, { parts }) })
1371
+ }
1372
+ );
1373
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "ambx-stack", ref: stackRef, children: [
1374
+ control,
1375
+ label ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { id: labelId, className: "ambx-label", children: label }) : null
1376
+ ] });
1377
+ }
1378
+
1379
+ // src/components/AmbientKnob.tsx
1380
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1381
+ function AmbientKnob({
1382
+ material,
1383
+ knurling,
1384
+ knurlColor,
1385
+ markers,
1386
+ indicator,
1387
+ look,
1388
+ size = "md",
1389
+ className,
1390
+ travel: travel2,
1391
+ input,
1392
+ animate,
1393
+ ...rest
1394
+ }) {
1395
+ const { dress, defaults } = useDress(
1396
+ "rotary",
1397
+ { material, knurling, knurlColor, markers, indicator, ...look },
1398
+ groundedKit.rotary
1399
+ );
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
1401
+ AmbientRotary,
1402
+ {
1403
+ ...rest,
1404
+ travel: travel2 ?? defaults?.travel,
1405
+ input: input ?? defaults?.input,
1406
+ animate: animate ?? defaults?.animate,
1407
+ size,
1408
+ className: cn(dress.className, className),
1409
+ parts: dress.parts
420
1410
  }
1411
+ );
1412
+ }
1413
+
1414
+ // src/controls/AmbientTravel.tsx
1415
+ var import_react16 = require("react");
1416
+
1417
+ // src/core/useTravel.ts
1418
+ var import_react15 = require("react");
1419
+ function useTravel(options) {
1420
+ const {
1421
+ min = 0,
1422
+ max = 100,
1423
+ step = 1,
1424
+ orientation = "horizontal",
1425
+ invert = false,
1426
+ disabled = false
1427
+ } = options;
1428
+ const [value, setValue] = useControllableValue(
1429
+ options.value,
1430
+ options.defaultValue ?? min,
1431
+ options.onChange
1432
+ );
1433
+ const [dragging, setDragging] = (0, import_react15.useState)(false);
1434
+ const draggingRef = (0, import_react15.useRef)(false);
1435
+ const rootRef = (0, import_react15.useRef)(null);
1436
+ const vertical = orientation === "vertical";
1437
+ const range = max - min;
1438
+ const keyStep = step > 0 ? step : range / 100 || 1;
1439
+ const percent = clamp(normalise(value, min, max), 0, 1);
1440
+ const set = (0, import_react15.useCallback)(
1441
+ (next) => setValue(commit(next, min, max, step)),
1442
+ [setValue, min, max, step]
1443
+ );
1444
+ const track = (event) => {
1445
+ const rect = rootRef.current?.getBoundingClientRect();
1446
+ if (!rect) return;
1447
+ const raw = vertical ? 1 - (event.clientY - rect.top) / (rect.height || 1) : (event.clientX - rect.left) / (rect.width || 1);
1448
+ set(denormalise(clamp(invert ? 1 - raw : raw, 0, 1), min, max));
421
1449
  };
422
- const onPointerMove = (event) => {
423
- if (event.buttons !== 1) return;
424
- updateFromClientY(event.clientY);
1450
+ const state = (0, import_react15.useMemo)(
1451
+ () => ({
1452
+ value,
1453
+ min,
1454
+ max,
1455
+ percent,
1456
+ angle: 0,
1457
+ travelStart: 0,
1458
+ travelSweep: 0,
1459
+ detents: options.detents ?? (step > 0 && range > 0 ? Math.round(range / step) : 0),
1460
+ dragging,
1461
+ disabled,
1462
+ atMin: value <= min,
1463
+ atMax: value >= max
1464
+ }),
1465
+ [value, min, max, percent, options.detents, step, range, dragging, disabled]
1466
+ );
1467
+ const onKeyDown = valueKeyHandler({
1468
+ value,
1469
+ min,
1470
+ max,
1471
+ step: keyStep,
1472
+ disabled,
1473
+ invert,
1474
+ onChange: setValue
1475
+ });
1476
+ const rootProps = {
1477
+ ref: rootRef,
1478
+ role: "slider",
1479
+ "aria-valuemin": min,
1480
+ "aria-valuemax": max,
1481
+ "aria-valuenow": value,
1482
+ "aria-orientation": orientation,
1483
+ "aria-disabled": disabled || void 0,
1484
+ tabIndex: disabled ? -1 : 0,
1485
+ style: stateStyle(state),
1486
+ "data-orientation": orientation,
1487
+ ...stateData(state),
1488
+ onPointerDown: (event) => {
1489
+ if (disabled || event.button !== 0) return;
1490
+ capturePointer(event.currentTarget, event.pointerId);
1491
+ draggingRef.current = true;
1492
+ setDragging(true);
1493
+ track(event);
1494
+ },
1495
+ onPointerMove: (event) => {
1496
+ if (!draggingRef.current || disabled) return;
1497
+ track(event);
1498
+ },
1499
+ onPointerUp: () => {
1500
+ draggingRef.current = false;
1501
+ setDragging(false);
1502
+ },
1503
+ onPointerCancel: () => {
1504
+ draggingRef.current = false;
1505
+ setDragging(false);
1506
+ },
1507
+ onKeyDown: (event) => {
1508
+ onKeyDown(event);
1509
+ }
425
1510
  };
426
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
427
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1511
+ return { state, rootProps, setValue: set };
1512
+ }
1513
+
1514
+ // src/controls/AmbientTravel.tsx
1515
+ var import_jsx_runtime20 = require("react/jsx-runtime");
1516
+ function AmbientTravel({
1517
+ parts,
1518
+ size,
1519
+ animate = "auto",
1520
+ label,
1521
+ className,
1522
+ value,
1523
+ defaultValue,
1524
+ min,
1525
+ max,
1526
+ step,
1527
+ detents,
1528
+ orientation = "horizontal",
1529
+ invert,
1530
+ disabled,
1531
+ onChange,
1532
+ ...rest
1533
+ }) {
1534
+ const labelId = (0, import_react16.useId)();
1535
+ const stackRef = (0, import_react16.useRef)(null);
1536
+ const { state, rootProps } = useTravel({
1537
+ value,
1538
+ defaultValue,
1539
+ min,
1540
+ max,
1541
+ step,
1542
+ detents,
1543
+ orientation,
1544
+ invert,
1545
+ disabled,
1546
+ onChange
1547
+ });
1548
+ useDevPartCheck(stackRef, "AmbientTravel");
1549
+ const sized = sizeProps("travel", size);
1550
+ const { style: restStyle, ...restProps } = rest;
1551
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "ambx-stack", ref: stackRef, children: [
1552
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
428
1553
  "div",
429
1554
  {
430
- className: "amb-fader amb-groove ambx-fader",
431
- ref: trackRef,
432
- role: "slider",
433
- "aria-label": label,
434
- "aria-labelledby": label ? id : void 0,
435
- "aria-valuemin": min,
436
- "aria-valuemax": max,
437
- "aria-valuenow": value,
438
- "aria-orientation": "vertical",
439
- tabIndex: 0,
440
- onPointerDown: (event) => {
441
- event.currentTarget.setPointerCapture(event.pointerId);
442
- updateFromClientY(event.clientY);
443
- },
444
- onPointerMove,
445
- onKeyDown,
446
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
447
- "div",
448
- {
449
- className: cn("amb-fader-thumb ambient amb-fillet ambx-fader-thumb", material !== "glass" && "amb-surface-concave", material && `amb-mat-${material}`),
450
- style: { top: `${100 - percent}%` },
451
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "amb-fader-gripline" })
452
- }
453
- )
1555
+ ...restProps,
1556
+ ...rootProps,
1557
+ "aria-labelledby": label ? labelId : rest["aria-labelledby"],
1558
+ "data-animate": animate,
1559
+ className: cn(
1560
+ "ambx-control ambx-travel",
1561
+ `ambx-travel-${orientation}`,
1562
+ sized.className,
1563
+ className
1564
+ ),
1565
+ style: { ...rootProps.style, ...sized.style, ...restStyle },
1566
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ControlStateProvider, { value: state, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Frames, { parts }) })
454
1567
  }
455
1568
  ),
456
- label ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { id, className: "ambx-label", children: label }) : null
1569
+ label ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { id: labelId, className: "ambx-label", children: label }) : null
457
1570
  ] });
458
1571
  }
459
1572
 
460
- // src/components/AmbientSlider.tsx
461
- var import_react4 = require("react");
462
- var import_jsx_runtime7 = require("react/jsx-runtime");
463
- function clamp3(value, min, max) {
464
- return Math.min(max, Math.max(min, value));
1573
+ // src/components/AmbientFader.tsx
1574
+ var import_jsx_runtime21 = require("react/jsx-runtime");
1575
+ function AmbientFader({
1576
+ material,
1577
+ look,
1578
+ size = "md",
1579
+ animate,
1580
+ className,
1581
+ ...rest
1582
+ }) {
1583
+ const { dress, defaults } = useDress(
1584
+ "travel",
1585
+ { material, orientation: "vertical", ...look },
1586
+ groundedKit.travel
1587
+ );
1588
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
1589
+ AmbientTravel,
1590
+ {
1591
+ ...rest,
1592
+ orientation: "vertical",
1593
+ size,
1594
+ animate: animate ?? defaults?.animate,
1595
+ className: cn(dress.className, className),
1596
+ parts: dress.parts
1597
+ }
1598
+ );
465
1599
  }
1600
+
1601
+ // src/components/AmbientSlider.tsx
1602
+ var import_jsx_runtime22 = require("react/jsx-runtime");
466
1603
  function AmbientSlider({
467
- value,
468
- min = 0,
469
- max = 100,
470
- step = 1,
471
- label,
472
1604
  material,
473
- onChange,
1605
+ look,
1606
+ size = "md",
1607
+ animate,
474
1608
  className,
475
- ...props
1609
+ ...rest
476
1610
  }) {
477
- const id = (0, import_react4.useId)();
478
- const trackRef = (0, import_react4.useRef)(null);
479
- const safeStep = step > 0 ? step : 1;
480
- const percent = (value - min) / (max - min || 1) * 100;
481
- const updateFromClientX = (clientX) => {
482
- const track = trackRef.current;
483
- if (!track) return;
484
- const rect = track.getBoundingClientRect();
485
- const ratio = (clientX - rect.left) / rect.width;
486
- const nextValue = min + clamp3(ratio, 0, 1) * (max - min);
487
- const snapped = Math.round(nextValue / safeStep) * safeStep;
488
- onChange?.(clamp3(snapped, min, max));
489
- };
490
- const setValue = (nextValue) => {
491
- const snapped = Math.round(nextValue / safeStep) * safeStep;
492
- onChange?.(clamp3(snapped, min, max));
493
- };
494
- const onKeyDown = (event) => {
495
- const pageStep = safeStep * 10;
496
- switch (event.key) {
497
- case "ArrowRight":
498
- case "ArrowUp":
499
- event.preventDefault();
500
- setValue(value + safeStep);
501
- break;
502
- case "ArrowLeft":
503
- case "ArrowDown":
504
- event.preventDefault();
505
- setValue(value - safeStep);
506
- break;
507
- case "PageUp":
508
- event.preventDefault();
509
- setValue(value + pageStep);
510
- break;
511
- case "PageDown":
512
- event.preventDefault();
513
- setValue(value - pageStep);
514
- break;
515
- case "Home":
516
- event.preventDefault();
517
- setValue(min);
518
- break;
519
- case "End":
520
- event.preventDefault();
521
- setValue(max);
522
- break;
523
- default:
524
- break;
1611
+ const { dress, defaults } = useDress(
1612
+ "travel",
1613
+ { material, orientation: "horizontal", ...look },
1614
+ groundedKit.travel
1615
+ );
1616
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
1617
+ AmbientTravel,
1618
+ {
1619
+ ...rest,
1620
+ orientation: "horizontal",
1621
+ size,
1622
+ animate: animate ?? defaults?.animate,
1623
+ className: cn(dress.className, className),
1624
+ parts: dress.parts
1625
+ }
1626
+ );
1627
+ }
1628
+
1629
+ // src/parts/console.tsx
1630
+ var import_jsx_runtime23 = require("react/jsx-runtime");
1631
+ function ConsoleWell({ className }) {
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: cn("amb-console-housing amb-groove", className), children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-face amb-surface" }) });
1633
+ }
1634
+ function ConsoleBar({ className }) {
1635
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: cn("amb-console-bar", className), children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-bar-body ambient amb-surface amb-chamfer amb-thickness-2", children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-indicator" }) }) });
1636
+ }
1637
+ function ConsoleMarks({
1638
+ mark = true,
1639
+ legend = true,
1640
+ className
1641
+ }) {
1642
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { className: cn("amb-console-marks", className), "aria-hidden": true, children: [
1643
+ mark ? /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-mark" }) : null,
1644
+ legend ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
1645
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-legend amb-console-legend-min", children: "\u2212" }),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "amb-console-legend amb-console-legend-max", children: "+" })
1647
+ ] }) : null
1648
+ ] });
1649
+ }
1650
+ function ToggleTrack({ className }) {
1651
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: cn("amb-console-track amb-groove", className) });
1652
+ }
1653
+ function ToggleThumb({ className }) {
1654
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: cn("amb-console-thumb ambient amb-thickness-2", className) });
1655
+ }
1656
+
1657
+ // src/kits/console.tsx
1658
+ var import_jsx_runtime24 = require("react/jsx-runtime");
1659
+ function rotary2(look) {
1660
+ const mark = look.mark !== false;
1661
+ const legend = look.legend !== false;
1662
+ return {
1663
+ className: cn(
1664
+ "amb-console-knob",
1665
+ mark && "amb-console-knob-marked",
1666
+ legend && "amb-console-knob-legended"
1667
+ ),
1668
+ parts: {
1669
+ panel: mark || legend ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ConsoleMarks, { mark, legend }) : null,
1670
+ base: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ConsoleWell, {}),
1671
+ actuator: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ConsoleBar, {})
525
1672
  }
526
1673
  };
527
- const onPointerMove = (event) => {
528
- if (event.buttons !== 1) return;
529
- updateFromClientX(event.clientX);
1674
+ }
1675
+ function latch2() {
1676
+ return {
1677
+ className: "amb-console-toggle",
1678
+ parts: { base: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ToggleTrack, {}), actuator: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ToggleThumb, {}) }
530
1679
  };
531
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
532
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
533
- "div",
534
- {
535
- className: "amb-slider amb-groove ambx-slider",
536
- ref: trackRef,
537
- role: "slider",
538
- "aria-label": label,
539
- "aria-labelledby": label ? id : void 0,
540
- "aria-valuemin": min,
541
- "aria-valuemax": max,
542
- "aria-valuenow": value,
543
- "aria-orientation": "horizontal",
544
- tabIndex: 0,
545
- onPointerDown: (event) => {
546
- event.currentTarget.setPointerCapture(event.pointerId);
547
- updateFromClientX(event.clientX);
548
- },
549
- onPointerMove,
550
- onKeyDown,
551
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
552
- "div",
553
- {
554
- className: cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`),
555
- style: { left: `${percent}%` }
556
- }
557
- )
558
- }
559
- ),
560
- label ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { id, className: "ambx-label", children: label }) : null
561
- ] });
1680
+ }
1681
+ var consoleKit = {
1682
+ name: "console",
1683
+ rotary: rotary2,
1684
+ latch: latch2,
1685
+ /* A visual identity may say how its controls feel to turn. The desk knob
1686
+ this is drawn from is an absolute-position pot with a centre detent, so
1687
+ it grabs where you press rather than tracking a drag. */
1688
+ defaults: {
1689
+ rotary: { input: "angle", travel: 280 }
1690
+ },
1691
+ looks: {
1692
+ rotary: ["mark", "legend"],
1693
+ latch: []
1694
+ }
1695
+ };
1696
+ function ConsoleKnob({ mark, legend, ...rest }) {
1697
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(AmbientKnob, { ...rest, look: { mark, legend } });
1698
+ }
1699
+ function ConsoleToggle(props) {
1700
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(AmbientSwitch, { ...props });
562
1701
  }
563
1702
  // Annotate the CommonJS export names for ESM import in node:
564
1703
  0 && (module.exports = {
1704
+ AmbientBank,
565
1705
  AmbientButton,
566
1706
  AmbientFader,
1707
+ AmbientKitProvider,
567
1708
  AmbientKnob,
1709
+ AmbientLatch,
568
1710
  AmbientPanel,
1711
+ AmbientPress,
569
1712
  AmbientProvider,
1713
+ AmbientRack,
1714
+ AmbientRotary,
1715
+ AmbientSelect,
570
1716
  AmbientSlider,
571
- AmbientSwitch
1717
+ AmbientSwitch,
1718
+ AmbientTravel,
1719
+ ButtonCap,
1720
+ ConsoleBar,
1721
+ ConsoleKnob,
1722
+ ConsoleMarks,
1723
+ ConsoleToggle,
1724
+ ConsoleWell,
1725
+ FaderCap,
1726
+ IndicatorBar,
1727
+ IndicatorDot,
1728
+ KeyCap,
1729
+ KeyLens,
1730
+ KnobBody,
1731
+ KnurledFace,
1732
+ Led,
1733
+ ScaleRing,
1734
+ SliderThumb,
1735
+ SwitchPill,
1736
+ SwitchTrack,
1737
+ ToggleThumb,
1738
+ ToggleTrack,
1739
+ TravelTrack,
1740
+ consoleKit,
1741
+ groundedKit,
1742
+ useBank,
1743
+ useBankKey,
1744
+ useControlState,
1745
+ useDress,
1746
+ useKit,
1747
+ useLatch,
1748
+ usePress,
1749
+ useRotary,
1750
+ useTravel
572
1751
  });
573
1752
  //# sourceMappingURL=index.cjs.map