@ambientcss/components 2.1.0 → 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 (45) hide show
  1. package/README.md +35 -0
  2. package/dist/index.cjs +1540 -391
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +749 -52
  5. package/dist/index.d.ts +749 -52
  6. package/dist/index.js +1501 -391
  7. package/dist/index.js.map +1 -1
  8. package/dist/styles.css +998 -291
  9. package/package.json +2 -2
  10. package/src/components/AmbientButton.tsx +23 -27
  11. package/src/components/AmbientFader.tsx +29 -115
  12. package/src/components/AmbientKnob.tsx +58 -220
  13. package/src/components/AmbientPanel.tsx +8 -2
  14. package/src/components/AmbientProvider.tsx +3 -0
  15. package/src/components/AmbientSelect.tsx +40 -0
  16. package/src/components/AmbientSlider.tsx +28 -113
  17. package/src/components/AmbientSwitch.tsx +58 -58
  18. package/src/controls/AmbientBank.tsx +138 -0
  19. package/src/controls/AmbientLatch.tsx +78 -0
  20. package/src/controls/AmbientPress.tsx +74 -0
  21. package/src/controls/AmbientRotary.tsx +94 -0
  22. package/src/controls/AmbientTravel.tsx +83 -0
  23. package/src/core/context.tsx +46 -0
  24. package/src/core/controllable.ts +33 -0
  25. package/src/core/dev.ts +15 -0
  26. package/src/core/frames.tsx +63 -0
  27. package/src/core/kit.tsx +107 -0
  28. package/src/core/material.ts +27 -0
  29. package/src/core/numeric.ts +88 -0
  30. package/src/core/types.ts +116 -0
  31. package/src/core/useBank.ts +163 -0
  32. package/src/core/useLatch.ts +54 -0
  33. package/src/core/usePress.ts +120 -0
  34. package/src/core/useRotary.ts +253 -0
  35. package/src/core/useTravel.ts +141 -0
  36. package/src/index.ts +122 -2
  37. package/src/kits/console.tsx +80 -0
  38. package/src/kits/grounded.tsx +113 -0
  39. package/src/parts/bank.tsx +32 -0
  40. package/src/parts/console.tsx +99 -0
  41. package/src/parts/knob.tsx +203 -0
  42. package/src/parts/latch.tsx +33 -0
  43. package/src/parts/press.tsx +56 -0
  44. package/src/parts/travel.tsx +70 -0
  45. package/src/styles.css +998 -291
package/dist/index.cjs CHANGED
@@ -20,14 +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,
28
32
  AmbientRack: () => AmbientRack,
33
+ AmbientRotary: () => AmbientRotary,
34
+ AmbientSelect: () => AmbientSelect,
29
35
  AmbientSlider: () => AmbientSlider,
30
- 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
31
70
  });
32
71
  module.exports = __toCommonJS(index_exports);
33
72
 
@@ -75,7 +114,7 @@ function AmbientPanel({ className, material = "matte", ...props }) {
75
114
  {
76
115
  className: cn(
77
116
  "ambient amb-surface amb-chamfer amb-elevation-2 ambx-panel",
78
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass",
117
+ `amb-mat-${material}`,
79
118
  className
80
119
  ),
81
120
  ...props
@@ -106,498 +145,1608 @@ function AmbientRack({
106
145
  );
107
146
  }
108
147
 
109
- // src/components/AmbientButton.tsx
148
+ // src/controls/AmbientPress.tsx
149
+ var import_react5 = require("react");
150
+
151
+ // src/core/context.tsx
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
110
217
  var import_jsx_runtime4 = require("react/jsx-runtime");
111
- function AmbientButton({
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,
112
348
  className,
349
+ mode,
350
+ value,
351
+ defaultValue,
352
+ onChange,
353
+ onPress,
354
+ repeatDelay,
355
+ repeatInterval,
356
+ disabled,
113
357
  children,
114
- material = "matte",
115
- shape = "pill",
116
- size = "md",
117
- ...props
358
+ ...rest
118
359
  }) {
119
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
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)(
120
375
  "button",
121
376
  {
122
- type: "button",
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
+ {
123
470
  className: cn(
124
- "amb-button amb-groove ambx-button",
125
- `ambx-button-${size}`,
126
- shape === "round" && "amb-button-round",
127
- shape === "square" && "amb-button-square",
471
+ "amb-knob-body ambient amb-thickness-2 amb-surface",
472
+ material && `amb-mat-${material}`,
128
473
  className
129
474
  ),
130
- ...props,
131
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
132
- "span",
133
- {
134
- className: cn(
135
- "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
136
- material === "matte" ? "amb-mat-matte" : material === "shiny" ? "amb-mat-shiny" : "amb-mat-glass"
137
- ),
138
- children
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
139
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` }
521
+ },
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
140
574
  )
141
575
  }
142
576
  );
143
577
  }
144
578
 
145
- // src/components/AmbientSwitch.tsx
146
- var import_react = require("react");
147
- var import_jsx_runtime5 = require("react/jsx-runtime");
148
- function AmbientSwitch({
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",
149
588
  className,
150
- checked,
151
- defaultChecked,
152
- onCheckedChange,
153
- onClick,
154
- size = "md",
155
- label,
156
- led,
157
- children,
158
- ...props
589
+ children
159
590
  }) {
160
- const labelId = (0, import_react.useId)();
161
- const isControlled = checked !== void 0;
162
- const [internalChecked, setInternalChecked] = (0, import_react.useState)(defaultChecked ?? false);
163
- const active = isControlled ? checked ?? false : internalChecked;
164
- const button = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
165
- "button",
591
+ const relief = isRelief(material);
592
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
593
+ "span",
166
594
  {
167
- type: "button",
168
- role: "switch",
169
- "aria-checked": active,
170
- "aria-labelledby": label ? labelId : props["aria-labelledby"],
171
595
  className: cn(
172
- "amb-switch",
173
- active && "amb-switch-on",
174
- "ambx-switch",
175
- `ambx-switch-${size}`,
596
+ "amb-button-cap ambient amb-chamfer amb-surface amb-heading-3",
597
+ relief ? void 0 : `amb-mat-${material}`,
176
598
  className
177
599
  ),
178
- onClick: (event) => {
179
- const next = !active;
180
- if (!isControlled) setInternalChecked(next);
181
- onCheckedChange?.(next);
182
- onClick?.(event);
183
- },
184
- ...props,
185
600
  children: [
186
- led ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
601
+ relief ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
187
602
  "span",
188
603
  {
189
- className: cn("amb-led", !active && "amb-led-off"),
190
- 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
191
606
  }
192
607
  ) : null,
193
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-switch-track amb-groove", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "amb-switch-pill ambient amb-fillet amb-surface-convex" }) }),
194
608
  children
195
609
  ]
196
610
  }
197
611
  );
198
- if (!label) return button;
199
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "ambx-stack", children: [
200
- button,
201
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { id: labelId, className: "ambx-label", children: label })
202
- ] });
203
612
  }
204
613
 
205
- // src/components/AmbientKnob.tsx
206
- var import_react2 = require("react");
207
- var import_jsx_runtime6 = require("react/jsx-runtime");
208
- var KNURLS = {
209
- /* 36 ribs — the grounded referent knob (dot and line variants) */
210
- standard: { teeth: 36, root: 0.468, rise: 0.12, fall: [0.5, 0.62] },
211
- /* 14 broad flutes with deeper roots (OP-Z-style flute variant) */
212
- flute: { teeth: 14, root: 0.44, rise: 0.08, fall: [0.72, 0.8] },
213
- /* 48-rib fine knurl, shallower (cap and wheel variants) */
214
- fine: { teeth: 48, root: 0.476, rise: 0.12, fall: [0.5, 0.62] }
215
- };
216
- function knurlPath({ teeth, root, rise, fall }) {
217
- const outer = 0.5;
218
- const pitch = Math.PI * 2 / teeth;
219
- const pts = [];
220
- for (let i = 0; i < teeth; i++) {
221
- const a = i * pitch;
222
- const tooth = [
223
- [0, root],
224
- [rise, outer],
225
- [fall[0], outer],
226
- [fall[1], root]
227
- ];
228
- for (const [frac, radius] of tooth) {
229
- const t = a + frac * pitch;
230
- pts.push(
231
- `${(0.5 + radius * Math.cos(t)).toFixed(4)} ${(0.5 + radius * Math.sin(t)).toFixed(4)}`
232
- );
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
233
632
  }
234
- }
235
- return `M${pts.join(" L")} Z`;
633
+ );
236
634
  }
237
- var KNURL_PATHS = {
238
- standard: knurlPath(KNURLS.standard),
239
- flute: knurlPath(KNURLS.flute),
240
- fine: knurlPath(KNURLS.fine)
241
- };
242
- var VARIANT_FAMILY = {
243
- dot: "standard",
244
- line: "standard",
245
- flute: "flute",
246
- cap: "fine",
247
- 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
+ }
248
731
  };
249
- function clamp(value, min, max) {
250
- 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
+ );
251
754
  }
252
- function AmbientKnob({
253
- value,
254
- min = 0,
255
- max = 100,
256
- step = 1,
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",
257
806
  label,
258
- material,
259
- variant = "dot",
807
+ className,
808
+ value,
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({
260
851
  size = "md",
852
+ led,
853
+ label,
854
+ look,
855
+ value,
856
+ defaultValue,
261
857
  onChange,
858
+ animate,
262
859
  className,
263
- ...props
860
+ ...rest
264
861
  }) {
265
- const id = (0, import_react2.useId)();
266
- const clipId = `amb-knurl-${id.replace(/:/g, "")}`;
267
- const draggingRef = (0, import_react2.useRef)(false);
268
- const knobRef = (0, import_react2.useRef)(null);
269
- const safeStep = step > 0 ? step : 1;
270
- const family = VARIANT_FAMILY[variant];
271
- const percent = (value - min) / (max - min || 1);
272
- const rotation = percent * 270 - 135;
273
- const pointerToValue = (event) => {
274
- const rect = knobRef.current.getBoundingClientRect();
275
- const cx = rect.left + rect.width / 2;
276
- const cy = rect.top + rect.height / 2;
277
- const atan2Deg = Math.atan2(event.clientY - cy, event.clientX - cx) * (180 / Math.PI);
278
- let angle = atan2Deg + 90;
279
- if (angle > 180) angle -= 360;
280
- if (angle < -135 || angle > 135) {
281
- angle = percent >= 0.5 ? 135 : -135;
282
- }
283
- const range = max - min;
284
- const raw = min + (angle + 135) / 270 * range;
285
- const snapped = Math.round(raw / safeStep) * safeStep;
286
- 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]);
287
934
  };
288
- const setValue = (nextValue) => {
289
- const snapped = Math.round(nextValue / safeStep) * safeStep;
290
- 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]);
291
940
  };
292
- const onKeyDown = (event) => {
293
- const pageStep = safeStep * 10;
941
+ const onKeyDown = (event, index) => {
942
+ const back = orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
943
+ const forward = orientation === "vertical" ? "ArrowDown" : "ArrowRight";
294
944
  switch (event.key) {
295
- case "ArrowUp":
296
- case "ArrowRight":
297
- event.preventDefault();
298
- setValue(value + safeStep);
299
- break;
300
- case "ArrowDown":
301
- case "ArrowLeft":
302
- event.preventDefault();
303
- setValue(value - safeStep);
304
- break;
305
- case "PageUp":
945
+ case back:
306
946
  event.preventDefault();
307
- setValue(value + pageStep);
947
+ step(index, -1);
308
948
  break;
309
- case "PageDown":
949
+ case forward:
310
950
  event.preventDefault();
311
- setValue(value - pageStep);
951
+ step(index, 1);
312
952
  break;
313
953
  case "Home":
314
954
  event.preventDefault();
315
- setValue(min);
955
+ edge("first");
316
956
  break;
317
957
  case "End":
318
958
  event.preventDefault();
319
- setValue(max);
959
+ edge("last");
960
+ break;
961
+ case " ":
962
+ case "Enter":
963
+ event.preventDefault();
964
+ select(items[index]);
320
965
  break;
321
966
  default:
322
967
  break;
323
968
  }
324
969
  };
325
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
326
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
327
- "div",
328
- {
329
- ref: knobRef,
330
- className: cn("amb-knob ambx-knob", `ambx-knob-${size}`),
331
- role: "slider",
332
- "aria-label": label,
333
- "aria-valuemin": min,
334
- "aria-valuemax": max,
335
- "aria-valuenow": value,
336
- "aria-labelledby": label ? id : void 0,
337
- "aria-orientation": "vertical",
338
- tabIndex: 0,
339
- onPointerDown: (event) => {
340
- event.currentTarget.setPointerCapture(event.pointerId);
341
- draggingRef.current = true;
342
- pointerToValue(event);
343
- },
344
- onPointerMove: (event) => {
345
- if (draggingRef.current) pointerToValue(event);
346
- },
347
- onPointerUp: () => {
348
- draggingRef.current = false;
349
- },
350
- onPointerCancel: () => {
351
- draggingRef.current = false;
352
- },
353
- onKeyDown,
354
- children: [
355
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: 0, height: 0, style: { position: "absolute" }, "aria-hidden": true, focusable: false, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("clipPath", { id: clipId, clipPathUnits: "objectBoundingBox", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: KNURL_PATHS[family] }) }) }) }),
356
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "amb-knob-body ambient amb-thickness-2 amb-surface" }),
357
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
358
- "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",
359
1070
  {
360
- className: cn(
361
- "ambx-knob-rotation amb-knob-face",
362
- family === "flute" && "amb-knob-face-flute",
363
- family === "fine" && "amb-knob-face-fine",
364
- variant === "cap" && "amb-knob-face-cap",
365
- material && `amb-mat-${material}`
366
- ),
367
- style: { transform: `rotate(${rotation}deg)`, clipPath: `url(#${clipId})` },
368
- children: [
369
- variant === "dot" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "amb-knob-indicator-dot" }) : null,
370
- variant === "line" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "amb-knob-indicator-line" }) : null,
371
- variant === "flute" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "amb-knob-indicator-dot amb-knob-indicator-dot-center" }) : null
372
- ]
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 }) }) })
373
1079
  }
374
- )
375
- ]
376
- }
377
- ),
378
- label ? /* @__PURE__ */ (0, import_jsx_runtime6.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 })
379
1089
  ] });
380
1090
  }
381
1091
 
382
- // src/components/AmbientFader.tsx
383
- var import_react3 = require("react");
384
- var import_jsx_runtime7 = require("react/jsx-runtime");
385
- 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) {
386
1115
  return Math.min(max, Math.max(min, value));
387
1116
  }
388
- function AmbientFader({
389
- value,
390
- min = 0,
391
- max = 100,
392
- step = 1,
393
- label,
394
- material,
395
- size = "md",
396
- onChange,
397
- className,
398
- ...props
399
- }) {
400
- const id = (0, import_react3.useId)();
401
- const trackRef = (0, import_react3.useRef)(null);
402
- const safeStep = step > 0 ? step : 1;
403
- const percent = (value - min) / (max - min || 1) * 100;
404
- const updateFromClientY = (clientY) => {
405
- const track = trackRef.current;
406
- if (!track) return;
407
- const rect = track.getBoundingClientRect();
408
- const ratio = 1 - (clientY - rect.top) / rect.height;
409
- const nextValue = min + clamp2(ratio, 0, 1) * (max - min);
410
- const snapped = Math.round(nextValue / safeStep) * safeStep;
411
- onChange?.(clamp2(snapped, min, max));
412
- };
413
- const setValue = (nextValue) => {
414
- const snapped = Math.round(nextValue / safeStep) * safeStep;
415
- onChange?.(clamp2(snapped, min, max));
416
- };
417
- const onKeyDown = (event) => {
418
- 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
+ };
419
1139
  switch (event.key) {
420
1140
  case "ArrowUp":
421
1141
  case "ArrowRight":
422
- event.preventDefault();
423
- setValue(value + safeStep);
424
- break;
1142
+ set(value + step * dir);
1143
+ return true;
425
1144
  case "ArrowDown":
426
1145
  case "ArrowLeft":
427
- event.preventDefault();
428
- setValue(value - safeStep);
429
- break;
1146
+ set(value - step * dir);
1147
+ return true;
430
1148
  case "PageUp":
431
- event.preventDefault();
432
- setValue(value + pageStep);
433
- break;
1149
+ set(value + step * 10 * dir);
1150
+ return true;
434
1151
  case "PageDown":
435
- event.preventDefault();
436
- setValue(value - pageStep);
437
- break;
1152
+ set(value - step * 10 * dir);
1153
+ return true;
438
1154
  case "Home":
439
- event.preventDefault();
440
- setValue(min);
441
- break;
1155
+ set(min);
1156
+ return true;
442
1157
  case "End":
443
- event.preventDefault();
444
- setValue(max);
445
- break;
1158
+ set(max);
1159
+ return true;
446
1160
  default:
447
- break;
1161
+ return false;
448
1162
  }
449
1163
  };
450
- const onPointerMove = (event) => {
451
- if (event.buttons !== 1) return;
452
- updateFromClientY(event.clientY);
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;
453
1226
  };
454
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
455
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
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
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));
1449
+ };
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
+ }
1510
+ };
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)(
456
1553
  "div",
457
1554
  {
458
- className: cn("amb-fader amb-groove ambx-fader", `ambx-fader-${size}`),
459
- ref: trackRef,
460
- role: "slider",
461
- "aria-label": label,
462
- "aria-labelledby": label ? id : void 0,
463
- "aria-valuemin": min,
464
- "aria-valuemax": max,
465
- "aria-valuenow": value,
466
- "aria-orientation": "vertical",
467
- tabIndex: 0,
468
- onPointerDown: (event) => {
469
- event.currentTarget.setPointerCapture(event.pointerId);
470
- updateFromClientY(event.clientY);
471
- },
472
- onPointerMove,
473
- onKeyDown,
474
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
475
- "div",
476
- {
477
- className: cn("amb-fader-thumb ambient amb-fillet ambx-fader-thumb", material !== "glass" && "amb-surface-concave", material && `amb-mat-${material}`),
478
- style: { top: `${100 - percent}%` },
479
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "amb-fader-gripline" })
480
- }
481
- )
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 }) })
482
1567
  }
483
1568
  ),
484
- label ? /* @__PURE__ */ (0, import_jsx_runtime7.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
485
1570
  ] });
486
1571
  }
487
1572
 
488
- // src/components/AmbientSlider.tsx
489
- var import_react4 = require("react");
490
- var import_jsx_runtime8 = require("react/jsx-runtime");
491
- function clamp3(value, min, max) {
492
- 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
+ );
493
1599
  }
1600
+
1601
+ // src/components/AmbientSlider.tsx
1602
+ var import_jsx_runtime22 = require("react/jsx-runtime");
494
1603
  function AmbientSlider({
495
- value,
496
- min = 0,
497
- max = 100,
498
- step = 1,
499
- label,
500
1604
  material,
1605
+ look,
501
1606
  size = "md",
502
- onChange,
1607
+ animate,
503
1608
  className,
504
- ...props
1609
+ ...rest
505
1610
  }) {
506
- const id = (0, import_react4.useId)();
507
- const trackRef = (0, import_react4.useRef)(null);
508
- const safeStep = step > 0 ? step : 1;
509
- const percent = (value - min) / (max - min || 1) * 100;
510
- const updateFromClientX = (clientX) => {
511
- const track = trackRef.current;
512
- if (!track) return;
513
- const rect = track.getBoundingClientRect();
514
- const ratio = (clientX - rect.left) / rect.width;
515
- const nextValue = min + clamp3(ratio, 0, 1) * (max - min);
516
- const snapped = Math.round(nextValue / safeStep) * safeStep;
517
- onChange?.(clamp3(snapped, min, max));
518
- };
519
- const setValue = (nextValue) => {
520
- const snapped = Math.round(nextValue / safeStep) * safeStep;
521
- onChange?.(clamp3(snapped, min, max));
522
- };
523
- const onKeyDown = (event) => {
524
- const pageStep = safeStep * 10;
525
- switch (event.key) {
526
- case "ArrowRight":
527
- case "ArrowUp":
528
- event.preventDefault();
529
- setValue(value + safeStep);
530
- break;
531
- case "ArrowLeft":
532
- case "ArrowDown":
533
- event.preventDefault();
534
- setValue(value - safeStep);
535
- break;
536
- case "PageUp":
537
- event.preventDefault();
538
- setValue(value + pageStep);
539
- break;
540
- case "PageDown":
541
- event.preventDefault();
542
- setValue(value - pageStep);
543
- break;
544
- case "Home":
545
- event.preventDefault();
546
- setValue(min);
547
- break;
548
- case "End":
549
- event.preventDefault();
550
- setValue(max);
551
- break;
552
- default:
553
- 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, {})
554
1672
  }
555
1673
  };
556
- const onPointerMove = (event) => {
557
- if (event.buttons !== 1) return;
558
- 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, {}) }
559
1679
  };
560
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: cn("ambx-stack", className), ...props, children: [
561
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
562
- "div",
563
- {
564
- className: cn("amb-slider amb-groove ambx-slider", `ambx-slider-${size}`),
565
- ref: trackRef,
566
- role: "slider",
567
- "aria-label": label,
568
- "aria-labelledby": label ? id : void 0,
569
- "aria-valuemin": min,
570
- "aria-valuemax": max,
571
- "aria-valuenow": value,
572
- "aria-orientation": "horizontal",
573
- tabIndex: 0,
574
- onPointerDown: (event) => {
575
- event.currentTarget.setPointerCapture(event.pointerId);
576
- updateFromClientX(event.clientX);
577
- },
578
- onPointerMove,
579
- onKeyDown,
580
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
581
- "div",
582
- {
583
- className: cn("amb-slider-thumb ambient amb-fillet ambx-slider-thumb", material !== "glass" && "amb-surface-convex", material && `amb-mat-${material}`),
584
- style: { left: `${percent}%` }
585
- }
586
- )
587
- }
588
- ),
589
- label ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { id, className: "ambx-label", children: label }) : null
590
- ] });
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 });
591
1701
  }
592
1702
  // Annotate the CommonJS export names for ESM import in node:
593
1703
  0 && (module.exports = {
1704
+ AmbientBank,
594
1705
  AmbientButton,
595
1706
  AmbientFader,
1707
+ AmbientKitProvider,
596
1708
  AmbientKnob,
1709
+ AmbientLatch,
597
1710
  AmbientPanel,
1711
+ AmbientPress,
598
1712
  AmbientProvider,
599
1713
  AmbientRack,
1714
+ AmbientRotary,
1715
+ AmbientSelect,
600
1716
  AmbientSlider,
601
- 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
602
1751
  });
603
1752
  //# sourceMappingURL=index.cjs.map