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