@kyralabs/m3e-motion 1.2.0 → 1.4.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.
package/dist/index.js CHANGED
@@ -336,6 +336,327 @@ function springToLinear(spec, options = {}) {
336
336
  return { easing: `linear(${body.join(", ")})`, durationMillis, stops, maxError };
337
337
  }
338
338
 
339
+ // src/animatable.ts
340
+ var SpringAnimatable = class {
341
+ /**
342
+ * @param value initial value, at rest.
343
+ * @param visibilityThreshold Compose's visibility threshold: 0.01 for Float animations
344
+ * (`Spring.DefaultDisplacementThreshold`), 1 for an Int one.
345
+ */
346
+ constructor(value, visibilityThreshold = DEFAULT_DISPLACEMENT_THRESHOLD) {
347
+ this.visibilityThreshold = visibilityThreshold;
348
+ this.#value = value;
349
+ this.#target = value;
350
+ }
351
+ visibilityThreshold;
352
+ #value;
353
+ #velocity = 0;
354
+ #target;
355
+ #from = 0;
356
+ #velocity0 = 0;
357
+ #start = 0;
358
+ #duration = 0;
359
+ #spec = null;
360
+ get value() {
361
+ return this.#value;
362
+ }
363
+ /** Units per second. */
364
+ get velocity() {
365
+ return this.#velocity;
366
+ }
367
+ get target() {
368
+ return this.#target;
369
+ }
370
+ get isRunning() {
371
+ return this.#spec !== null;
372
+ }
373
+ /** Jumps to `value` and stops. */
374
+ snapTo(value) {
375
+ this.#spec = null;
376
+ this.#value = value;
377
+ this.#target = value;
378
+ this.#velocity = 0;
379
+ }
380
+ /**
381
+ * Animates to `target` from the value and velocity at `now` (ms); a null spec (e.g. a spatial
382
+ * spring under reduced motion) jumps there.
383
+ */
384
+ animateTo(target, spec, now) {
385
+ this.sample(now);
386
+ if (spec === null) {
387
+ this.snapTo(target);
388
+ return;
389
+ }
390
+ this.#from = this.#value;
391
+ this.#velocity0 = this.#velocity;
392
+ this.#target = target;
393
+ this.#start = now;
394
+ this.#spec = spec;
395
+ this.#duration = springDurationMillis(
396
+ spec,
397
+ this.#from,
398
+ target,
399
+ this.#velocity0,
400
+ this.visibilityThreshold
401
+ );
402
+ if (this.#duration <= 0) this.snapTo(target);
403
+ }
404
+ /** Advances to `now` (ms) and returns the value; the animation ends once its duration is up. */
405
+ sample(now) {
406
+ const spec = this.#spec;
407
+ if (spec === null) return this.#value;
408
+ const elapsed = Math.max(0, now - this.#start);
409
+ if (elapsed >= this.#duration) {
410
+ this.snapTo(this.#target);
411
+ return this.#value;
412
+ }
413
+ this.#value = springValue(spec, elapsed, this.#from, this.#target, this.#velocity0);
414
+ this.#velocity = springVelocity(spec, elapsed, this.#from, this.#target, this.#velocity0);
415
+ return this.#value;
416
+ }
417
+ };
418
+
419
+ // src/fab-menu.ts
420
+ var animatedInt = (value) => Math.trunc(value);
421
+ var fabMenuItemVisible = (index, itemCount, stagger) => index >= itemCount - animatedInt(stagger);
422
+
423
+ // src/menu-navigation.ts
424
+ function menuNavigationTarget(key, count, current) {
425
+ if (count === 0) return null;
426
+ switch (key) {
427
+ case "ArrowDown":
428
+ return current < 0 ? 0 : (current + 1) % count;
429
+ case "ArrowUp":
430
+ return current < 0 ? count - 1 : (current - 1 + count) % count;
431
+ case "Home":
432
+ return 0;
433
+ case "End":
434
+ return count - 1;
435
+ default:
436
+ return null;
437
+ }
438
+ }
439
+
440
+ // src/toolbar-navigation.ts
441
+ function toolbarNavigationTarget(key, count, current, orientation = "horizontal", rtl = false) {
442
+ if (count === 0) return null;
443
+ let step;
444
+ switch (key) {
445
+ case "Home":
446
+ return 0;
447
+ case "End":
448
+ return count - 1;
449
+ case "ArrowRight":
450
+ case "ArrowLeft":
451
+ if (orientation !== "horizontal") return null;
452
+ step = key === "ArrowRight" !== rtl ? 1 : -1;
453
+ break;
454
+ case "ArrowDown":
455
+ case "ArrowUp":
456
+ if (orientation !== "vertical") return null;
457
+ step = key === "ArrowDown" ? 1 : -1;
458
+ break;
459
+ default:
460
+ return null;
461
+ }
462
+ if (current < 0) return step > 0 ? 0 : count - 1;
463
+ return Math.min(Math.max(current + step, 0), count - 1);
464
+ }
465
+
466
+ // src/cubic-bezier.ts
467
+ var bezier = (p1, p2, t) => 3 * (1 - t) * (1 - t) * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t;
468
+ var cubicBezier = (a, b, c, d) => (fraction) => {
469
+ if (!(fraction > 0 && fraction < 1)) return fraction;
470
+ let lo = 0;
471
+ let hi = 1;
472
+ for (let i = 0; i < 30; i++) {
473
+ const mid = (lo + hi) / 2;
474
+ if (bezier(a, c, mid) < fraction) lo = mid;
475
+ else hi = mid;
476
+ }
477
+ return bezier(b, d, (lo + hi) / 2);
478
+ };
479
+ function parseCubicBezier(css) {
480
+ const values = /cubic-bezier\(([^)]*)\)/.exec(css)?.[1]?.split(",").map(Number);
481
+ return values?.length === 4 && values.every(Number.isFinite) ? cubicBezier(...values) : null;
482
+ }
483
+
484
+ // src/app-bar-scroll.ts
485
+ function appBarScrollState(height) {
486
+ return { heightOffsetLimit: -height, heightOffset: 0, contentOffset: 0, atStart: true };
487
+ }
488
+ function withHeightOffset(state, offset) {
489
+ return { ...state, heightOffset: Math.min(Math.max(offset, state.heightOffsetLimit), 0) };
490
+ }
491
+ function withHeight(state, height) {
492
+ return withHeightOffset({ ...state, heightOffsetLimit: -height }, state.heightOffset);
493
+ }
494
+ function collapsedFraction(state) {
495
+ return state.heightOffsetLimit !== 0 ? state.heightOffset / state.heightOffsetLimit + 0 : 0;
496
+ }
497
+ function overlappedFraction(state) {
498
+ if (!state.atStart && state.contentOffset === 0) return 1;
499
+ if (state.heightOffsetLimit === 0) return 0;
500
+ const limit = state.heightOffsetLimit;
501
+ const clamped = Math.min(Math.max(limit + Math.abs(state.contentOffset), limit), 0);
502
+ return 1 - clamped / limit;
503
+ }
504
+ function isScrolledUnder(state) {
505
+ return overlappedFraction(state) > 0.01;
506
+ }
507
+ function scrollAppBar(state, behavior, scrollTop, delta) {
508
+ const next = { ...state, contentOffset: -scrollTop, atStart: scrollTop <= 0 };
509
+ if (behavior === "pinned") return next;
510
+ if (behavior === "enter-always" || delta < 0) {
511
+ return withHeightOffset(next, state.heightOffset + delta);
512
+ }
513
+ const below = (top) => Math.max(0, -state.heightOffset - top);
514
+ return withHeightOffset(next, state.heightOffset + below(scrollTop) - below(scrollTop + delta));
515
+ }
516
+ function settleTarget(state) {
517
+ const fraction = collapsedFraction(state);
518
+ if (fraction < 0.01 || fraction === 1) return null;
519
+ if (!(state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit)) return null;
520
+ return fraction < 0.5 ? 0 : state.heightOffsetLimit;
521
+ }
522
+ var TOP_TITLE_ALPHA_EASING = [0.8, 0, 0.8, 0.15];
523
+ var FAST_OUT_LINEAR_IN_EASING = [0.4, 0, 1, 1];
524
+ var topTitleAlpha = /* @__PURE__ */ cubicBezier(...TOP_TITLE_ALPHA_EASING);
525
+ var fastOutLinearIn = /* @__PURE__ */ cubicBezier(...FAST_OUT_LINEAR_IN_EASING);
526
+ function twoRowsFrame(fraction) {
527
+ return {
528
+ collapsedTitleAlpha: topTitleAlpha(fraction),
529
+ expandedTitleAlpha: 1 - fraction,
530
+ containerColorFraction: fastOutLinearIn(fraction),
531
+ collapsedTitleHidden: fraction < 0.5
532
+ };
533
+ }
534
+
535
+ // src/floating-toolbar-scroll.ts
536
+ function floatingToolbarScrollState(offsetLimit = -Number.MAX_VALUE, offset = 0, contentOffset = 0) {
537
+ return withToolbarOffset({ offsetLimit, offset: 0, contentOffset }, offset);
538
+ }
539
+ function withToolbarOffset(state, offset) {
540
+ return { ...state, offset: Math.min(Math.max(offset, state.offsetLimit), 0) };
541
+ }
542
+ function withToolbarOffsetLimit(state, distance) {
543
+ return withToolbarOffset({ ...state, offsetLimit: -Math.max(distance, 0) }, state.offset);
544
+ }
545
+ function scrollFloatingToolbar(state, delta) {
546
+ return withToolbarOffset(
547
+ { ...state, contentOffset: state.contentOffset + delta },
548
+ state.offset + delta
549
+ );
550
+ }
551
+ function toolbarCollapsedFraction(state) {
552
+ return state.offsetLimit !== 0 ? state.offset / state.offsetLimit + 0 : 0;
553
+ }
554
+ function toolbarSettleTarget(state) {
555
+ return settleTarget({
556
+ heightOffsetLimit: state.offsetLimit,
557
+ heightOffset: state.offset,
558
+ contentOffset: state.contentOffset});
559
+ }
560
+ function toolbarExpansionScroll(expanded, collapseThreshold, expandThreshold, contentOffset = 0) {
561
+ return {
562
+ contentOffset,
563
+ threshold: expanded ? contentOffset - collapseThreshold : contentOffset + expandThreshold
564
+ };
565
+ }
566
+ function scrollToolbarExpansion(state, delta, collapseThreshold, expandThreshold) {
567
+ const contentOffset = state.contentOffset + delta;
568
+ if (delta < 0 && contentOffset <= state.threshold) {
569
+ return {
570
+ state: { contentOffset, threshold: contentOffset + expandThreshold },
571
+ change: "collapse"
572
+ };
573
+ }
574
+ if (delta > 0 && contentOffset >= state.threshold) {
575
+ return {
576
+ state: { contentOffset, threshold: contentOffset - collapseThreshold },
577
+ change: "expand"
578
+ };
579
+ }
580
+ return { state: { contentOffset, threshold: state.threshold }, change: null };
581
+ }
582
+
583
+ // src/progress-indicator.ts
584
+ var line = (t, [delay, duration], easing) => t <= delay ? 0 : t >= delay + duration ? 1 : easing((t - delay) / duration);
585
+ function linearIndeterminateFractions(elapsedMillis, timing) {
586
+ const t = Math.max(0, elapsedMillis) % timing.durationMillis;
587
+ return [
588
+ line(t, timing.firstTail, timing.easing),
589
+ line(t, timing.firstHead, timing.easing),
590
+ line(t, timing.secondTail, timing.easing),
591
+ line(t, timing.secondHead, timing.easing)
592
+ ];
593
+ }
594
+ function circularIndeterminateFrame(elapsedMillis, timing) {
595
+ const d = timing.durationMillis;
596
+ const t = Math.max(0, elapsedMillis) % d;
597
+ const steps = Math.round(d / timing.additionalDelayMillis);
598
+ const step = timing.additionalRotation / steps;
599
+ const index = Math.floor(t / timing.additionalDelayMillis);
600
+ const within = t - index * timing.additionalDelayMillis;
601
+ const additional = step * index + step * Math.min(1, within / timing.additionalDurationMillis);
602
+ const half = d / 2;
603
+ const range = timing.maxProgress - timing.minProgress;
604
+ const sweep = t <= half ? timing.minProgress + range * (t / half) : timing.maxProgress - range * timing.easing((t - half) / half);
605
+ return { rotation: timing.rotation * t / d + additional, sweep };
606
+ }
607
+ function waveTravelMillis(wavelength, waveSpeed, minMillis, vertexCount = 1) {
608
+ if (!(waveSpeed > 0 && wavelength > 0)) return Number.POSITIVE_INFINITY;
609
+ return Math.max(minMillis, Math.round(wavelength / waveSpeed * 1e3 * vertexCount));
610
+ }
611
+ var waveOffset = (elapsedMillis, travelMillis) => Number.isFinite(travelMillis) ? Math.max(0, elapsedMillis) % travelMillis / travelMillis : 0;
612
+ var indicatorAmplitude = (progress, minProgress, maxProgress) => progress <= minProgress || progress >= maxProgress ? 0 : 1;
613
+ var AmplitudeAnimation = class {
614
+ #from;
615
+ #to;
616
+ #start = 0;
617
+ #tween = null;
618
+ #value;
619
+ constructor(initial) {
620
+ this.#from = initial;
621
+ this.#to = initial;
622
+ this.#value = initial;
623
+ }
624
+ get value() {
625
+ return this.#value;
626
+ }
627
+ get target() {
628
+ return this.#to;
629
+ }
630
+ get isRunning() {
631
+ return this.#tween !== null;
632
+ }
633
+ /** Advances to `now` (ms). */
634
+ sample(now) {
635
+ const tween = this.#tween;
636
+ if (tween === null) return this.#value;
637
+ const fraction = tween.durationMillis > 0 ? (now - this.#start) / tween.durationMillis : 1;
638
+ if (fraction >= 1) {
639
+ this.#tween = null;
640
+ this.#value = this.#to;
641
+ } else {
642
+ this.#value = this.#from + (this.#to - this.#from) * tween.easing(Math.max(0, fraction));
643
+ }
644
+ return this.#value;
645
+ }
646
+ /** Requests `target` at `now`; returns true when a tween started. */
647
+ update(target, now, increase, decrease) {
648
+ this.sample(now);
649
+ if (this.#tween !== null || target === this.#to) return false;
650
+ const tween = this.#value < target ? increase : decrease;
651
+ this.#from = this.#value;
652
+ this.#to = target;
653
+ this.#start = now;
654
+ this.#tween = tween;
655
+ if (tween.durationMillis <= 0) this.sample(now);
656
+ return true;
657
+ }
658
+ };
659
+
339
660
  // src/loading-indicator.ts
340
661
  var FULL_ROTATION = 360;
341
662
  var QUARTER_ROTATION = FULL_ROTATION / 4;
@@ -364,40 +685,352 @@ function indeterminateFrame(elapsedMillis, morphCount, timing) {
364
685
  };
365
686
  }
366
687
 
367
- // src/app-bar-scroll.ts
368
- function appBarScrollState(height) {
369
- return { heightOffsetLimit: -height, heightOffset: 0, contentOffset: 0, atStart: true };
688
+ // src/menu-position.ts
689
+ var round2 = Math.round;
690
+ var alignH = (bias, size, space, rtl) => round2((space - size) / 2 * (1 + (rtl ? -bias : bias)));
691
+ var alignV = (bias, size, space) => round2((space - size) / 2 * (1 + bias));
692
+ var START = -1;
693
+ var END = 1;
694
+ var TOP = -1;
695
+ var CENTER = 0;
696
+ var BOTTOM = 1;
697
+ var anchorH = (menuBias, anchorBias) => (anchor, _width, menuWidth, rtl) => anchor.left + alignH(anchorBias, 0, anchor.right - anchor.left, rtl) - alignH(menuBias, 0, menuWidth, rtl);
698
+ var anchorV = (menuBias, anchorBias) => (anchor, _height, menuHeight) => anchor.top + alignV(anchorBias, 0, anchor.bottom - anchor.top) - alignV(menuBias, 0, menuHeight);
699
+ var startToAnchorStart = anchorH(START, START);
700
+ var startToAnchorEnd = anchorH(START, END);
701
+ var endToAnchorEnd = anchorH(END, END);
702
+ var endToAnchorStart = anchorH(END, START);
703
+ var leftToWindowLeft = (_a, width, menuWidth) => alignH(START, menuWidth, width, false);
704
+ var rightToWindowRight = (_a, width, menuWidth) => alignH(END, menuWidth, width, false);
705
+ var topToAnchorBottom = anchorV(TOP, BOTTOM);
706
+ var topToAnchorTop = anchorV(TOP, TOP);
707
+ var bottomToAnchorTop = anchorV(BOTTOM, TOP);
708
+ var bottomToAnchorBottom = anchorV(BOTTOM, BOTTOM);
709
+ var centerToAnchorTop = anchorV(CENTER, TOP);
710
+ var topToWindowTop = (_a, height, menuHeight) => alignV(TOP, menuHeight, height);
711
+ var bottomToWindowBottom = (_a, height, menuHeight) => alignV(BOTTOM, menuHeight, height);
712
+ var centerX = (r) => (r.left + r.right) / 2;
713
+ var centerY = (r) => (r.top + r.bottom) / 2;
714
+ var windowX = (anchor, width) => centerX(anchor) < width / 2 ? leftToWindowLeft : rightToWindowRight;
715
+ var windowY = (anchor, height) => centerY(anchor) < height / 2 ? topToWindowTop : bottomToWindowBottom;
716
+ function candidates(position, anchor, width, height) {
717
+ const wx = windowX(anchor, width);
718
+ const wy = windowY(anchor, height);
719
+ switch (position) {
720
+ case "above":
721
+ return [
722
+ [startToAnchorStart, endToAnchorEnd, wx],
723
+ [bottomToAnchorTop, topToAnchorBottom, centerToAnchorTop, wy]
724
+ ];
725
+ // Left and Start (Right and End) list the same candidates; Start/End resolve against the
726
+ // layout direction inside the candidates, Left/Right too (Menu.kt keeps them identical).
727
+ case "left":
728
+ case "start":
729
+ return [
730
+ [endToAnchorStart, startToAnchorEnd, wx],
731
+ [topToAnchorTop, bottomToAnchorBottom, wy]
732
+ ];
733
+ case "right":
734
+ case "end":
735
+ return [
736
+ [startToAnchorEnd, endToAnchorStart, wx],
737
+ [topToAnchorTop, bottomToAnchorBottom, wy]
738
+ ];
739
+ // 'below' (unrecognized values too).
740
+ default:
741
+ return [
742
+ [startToAnchorStart, endToAnchorEnd, wx],
743
+ [topToAnchorBottom, bottomToAnchorTop, centerToAnchorTop, wy]
744
+ ];
745
+ }
370
746
  }
371
- function withHeightOffset(state, offset) {
372
- return { ...state, heightOffset: Math.min(Math.max(offset, state.heightOffsetLimit), 0) };
747
+ var coerceIn = (v, min, max) => Math.min(Math.max(v, min), max);
748
+ function pick(values, size, space, margin, center) {
749
+ for (const [i, candidate] of values.entries()) {
750
+ if (candidate >= margin && candidate + size <= space - margin) return candidate;
751
+ if (i === values.length - 1) {
752
+ return size >= space - 2 * margin ? center() : coerceIn(candidate, margin, space - margin - size);
753
+ }
754
+ }
755
+ return 0;
373
756
  }
374
- function withHeight(state, height) {
375
- return withHeightOffset({ ...state, heightOffsetLimit: -height }, state.heightOffset);
757
+ function menuTransformOrigin(anchor, menu) {
758
+ const menuWidth = menu.right - menu.left;
759
+ const menuHeight = menu.bottom - menu.top;
760
+ const originX = menu.left >= anchor.right ? 0 : menu.right <= anchor.left ? 1 : menuWidth === 0 ? 0 : ((Math.max(anchor.left, menu.left) + Math.min(anchor.right, menu.right)) / 2 - menu.left) / menuWidth;
761
+ const originY = menu.top >= anchor.bottom ? 0 : menu.bottom <= anchor.top ? 1 : menuHeight === 0 ? 0 : ((Math.max(anchor.top, menu.top) + Math.min(anchor.bottom, menu.bottom)) / 2 - menu.top) / menuHeight;
762
+ return { originX, originY };
376
763
  }
377
- function collapsedFraction(state) {
378
- return state.heightOffsetLimit !== 0 ? state.heightOffset / state.heightOffsetLimit + 0 : 0;
764
+ function menuPosition(input) {
765
+ const { anchor, window: win, menu, rtl, verticalMargin, horizontalMargin } = input;
766
+ const offset = input.offset ?? { x: 0, y: 0 };
767
+ const [xs, ys] = candidates(input.position ?? "below", anchor, win.width, win.height);
768
+ const offsetX = offset.x * (rtl ? -1 : 1);
769
+ const x = pick(
770
+ xs.map((c) => c(anchor, win.width, menu.width, rtl) + offsetX),
771
+ menu.width,
772
+ win.width,
773
+ horizontalMargin,
774
+ () => alignH(CENTER, menu.width, win.width, rtl)
775
+ );
776
+ const y = pick(
777
+ ys.map((c) => c(anchor, win.height, menu.height) + offset.y),
778
+ menu.height,
779
+ win.height,
780
+ verticalMargin,
781
+ () => alignV(CENTER, menu.height, win.height)
782
+ );
783
+ return {
784
+ x,
785
+ y,
786
+ ...menuTransformOrigin(anchor, {
787
+ left: x,
788
+ top: y,
789
+ right: x + menu.width,
790
+ bottom: y + menu.height
791
+ })
792
+ };
379
793
  }
380
- function overlappedFraction(state) {
381
- if (!state.atStart && state.contentOffset === 0) return 1;
382
- if (state.heightOffsetLimit === 0) return 0;
383
- const limit = state.heightOffsetLimit;
384
- const clamped = Math.min(Math.max(limit + Math.abs(state.contentOffset), limit), 0);
385
- return 1 - clamped / limit;
794
+
795
+ // src/search-bar.ts
796
+ var SEARCH_BAR_EXPANDED_THRESHOLD = 0.02;
797
+ var SEARCH_DROPDOWN_VISIBLE_PROGRESS = 0.1;
798
+ var TweenAnimatable = class {
799
+ #value;
800
+ #from;
801
+ #target;
802
+ #start = 0;
803
+ #spec = null;
804
+ constructor(value) {
805
+ this.#value = value;
806
+ this.#from = value;
807
+ this.#target = value;
808
+ }
809
+ get value() {
810
+ return this.#value;
811
+ }
812
+ get target() {
813
+ return this.#target;
814
+ }
815
+ get isRunning() {
816
+ return this.#spec !== null;
817
+ }
818
+ snapTo(value) {
819
+ this.#spec = null;
820
+ this.#value = value;
821
+ this.#from = value;
822
+ this.#target = value;
823
+ }
824
+ animateTo(target, spec, now) {
825
+ this.sample(now);
826
+ if (spec === null || spec.delay + spec.duration <= 0) {
827
+ this.snapTo(target);
828
+ return;
829
+ }
830
+ this.#from = this.#value;
831
+ this.#target = target;
832
+ this.#start = now;
833
+ this.#spec = spec;
834
+ }
835
+ sample(now) {
836
+ const spec = this.#spec;
837
+ if (spec === null) return this.#value;
838
+ const elapsed = now - this.#start - spec.delay;
839
+ if (elapsed >= spec.duration) {
840
+ this.snapTo(this.#target);
841
+ return this.#value;
842
+ }
843
+ const fraction = elapsed <= 0 ? 0 : spec.easing(elapsed / spec.duration);
844
+ this.#value = this.#from + (this.#target - this.#from) * fraction;
845
+ return this.#value;
846
+ }
847
+ };
848
+ var SearchBarAnimation = class {
849
+ #progress;
850
+ #content;
851
+ #slide = new SpringAnimatable(0);
852
+ #specs = null;
853
+ #dropdownShown = false;
854
+ #dropdownVisible = false;
855
+ constructor(expanded = false) {
856
+ this.#progress = new SpringAnimatable(expanded ? 1 : 0);
857
+ this.#content = new TweenAnimatable(expanded ? 1 : 0);
858
+ this.#dropdownShown = expanded;
859
+ this.#dropdownVisible = expanded;
860
+ }
861
+ /** SearchBarState.targetValue. */
862
+ get isTargetExpanded() {
863
+ return this.#progress.target === 1;
864
+ }
865
+ /** SearchBarState.currentValue: expanded until the value drops to the 0.02 tolerance. */
866
+ get isExpanded() {
867
+ return this.#progress.value > SEARCH_BAR_EXPANDED_THRESHOLD;
868
+ }
869
+ get isRunning() {
870
+ return this.#progress.isRunning || this.#content.isRunning || this.#slide.isRunning;
871
+ }
872
+ /** animateToExpanded() at `now` (ms). */
873
+ expand(specs, now) {
874
+ this.#specs = specs;
875
+ this.#progress.animateTo(1, specs.expand, now);
876
+ this.#content.animateTo(1, specs.contentFadeIn, now);
877
+ }
878
+ /** animateToCollapsed() at `now` (ms). */
879
+ collapse(specs, now) {
880
+ this.#specs = specs;
881
+ this.#content.animateTo(0, specs.contentFadeOut, now);
882
+ this.#progress.animateTo(0, specs.collapse, now);
883
+ }
884
+ /** Jumps to collapsed (0) or expanded (1), stopping every animation. */
885
+ snapTo(expanded) {
886
+ this.#progress.snapTo(expanded ? 1 : 0);
887
+ this.#content.snapTo(expanded ? 1 : 0);
888
+ this.#slide.snapTo(0);
889
+ this.#dropdownShown = expanded;
890
+ this.#dropdownVisible = expanded;
891
+ }
892
+ /**
893
+ * Advances to `now`. `dropdownHeight` (px, the drop-down's full height) enables the with-gap
894
+ * slide: AnimatedVisibility(visible = progress > 0.1 && target == Expanded) slides in from minus
895
+ * half the height on the expand spring and out to it on the collapse spring.
896
+ */
897
+ sample(now, dropdownHeight) {
898
+ const value = this.#progress.sample(now);
899
+ const progress = Math.min(Math.max(value, 0), 1);
900
+ const contentProgress = Math.min(Math.max(this.#content.sample(now), 0), 1);
901
+ if (dropdownHeight !== void 0) {
902
+ const hidden = Math.round(-dropdownHeight / 2);
903
+ const visible = progress > SEARCH_DROPDOWN_VISIBLE_PROGRESS && this.isTargetExpanded;
904
+ if (visible !== this.#dropdownVisible) {
905
+ this.#dropdownVisible = visible;
906
+ if (visible) {
907
+ if (!this.#dropdownShown) this.#slide.snapTo(hidden);
908
+ this.#dropdownShown = true;
909
+ this.#slide.animateTo(0, this.#specs?.expand ?? null, now);
910
+ } else if (this.#dropdownShown) {
911
+ this.#slide.animateTo(hidden, this.#specs?.collapse ?? null, now);
912
+ }
913
+ }
914
+ this.#slide.sample(now);
915
+ if (!visible && this.#dropdownShown && !this.#slide.isRunning) this.#dropdownShown = false;
916
+ }
917
+ return {
918
+ value,
919
+ progress,
920
+ contentProgress,
921
+ slide: this.#slide.value,
922
+ dropdownShown: this.#dropdownShown
923
+ };
924
+ }
925
+ };
926
+ var lerp = (a, b, t) => a + (b - a) * t;
927
+ function searchViewFrame(input) {
928
+ const { variant, collapsed: c, window: w, insetTop, metrics: m } = input;
929
+ const { value, progress: p } = input.sample;
930
+ const none = { radius: 0, alpha: 1, minHeight: 0, maxHeight: 0 };
931
+ if (variant === "docked" || variant === "docked-with-gap") {
932
+ const ratio = variant === "docked" ? m.dockedMaxHeightRatio : m.withGapMaxHeightRatio;
933
+ const tableMax = w.height * ratio;
934
+ const maxHeight = lerp(c.height, tableMax, Math.max(value, 0));
935
+ const minHeight = Math.min(Math.min(m.dockedMinHeight, tableMax), maxHeight);
936
+ if (variant === "docked") {
937
+ return {
938
+ surface: { ...c, radius: 0, alpha: 1, minHeight, maxHeight },
939
+ field: { x: c.x, y: c.y, width: c.width },
940
+ results: { x: c.x, y: c.y + c.height, width: c.width, maxHeight: maxHeight - c.height }
941
+ };
942
+ }
943
+ return {
944
+ surface: { x: 0, y: 0, width: w.width, height: w.height, ...none, alpha: p },
945
+ field: { x: c.x, y: c.y, width: c.width },
946
+ results: {
947
+ x: c.x,
948
+ y: c.y + c.height + m.dropdownGap,
949
+ width: c.width,
950
+ maxHeight: Math.max(0, maxHeight - c.height - m.dropdownGap)
951
+ }
952
+ };
953
+ }
954
+ const contained = variant === "full-screen-contained";
955
+ const width = contained ? w.width : lerp(c.width, w.width, p);
956
+ const height = contained ? w.height : lerp(c.height, w.height, p);
957
+ const offsetX = contained ? 0 : lerp(c.x, 0, p);
958
+ const padding = contained ? m.containedHorizontalPadding : 0;
959
+ const fieldWidth = lerp(c.width, width - 2 * padding, value);
960
+ const centerX2 = lerp(c.x + c.width / 2, offsetX + width / 2, value);
961
+ const topPadding = insetTop + (contained ? m.containedTopPadding : m.verticalPadding);
962
+ const animatedTopPadding = topPadding * Math.min(p, 1);
963
+ const bottomPadding = contained ? m.verticalPadding : m.verticalPadding * p;
964
+ const offsetY = lerp(c.y, 0, p);
965
+ const fieldY = offsetY + animatedTopPadding;
966
+ return {
967
+ surface: contained ? { x: 0, y: 0, width, height, ...none, alpha: p } : { x: offsetX, y: offsetY, width, height, ...none, radius: m.cornerRadius * (1 - p) },
968
+ field: { x: Math.round(centerX2 - fieldWidth / 2), y: fieldY, width: fieldWidth },
969
+ results: {
970
+ x: offsetX,
971
+ y: fieldY + c.height + bottomPadding,
972
+ width,
973
+ maxHeight: Math.max(0, height - (c.height + animatedTopPadding + bottomPadding))
974
+ }
975
+ };
386
976
  }
387
- function isScrolledUnder(state) {
388
- return overlappedFraction(state) > 0.01;
977
+ function searchNavigationTarget(key, enabled, active) {
978
+ const indices = enabled.flatMap((on, i) => on ? [i] : []);
979
+ const first = indices[0];
980
+ const last = indices.at(-1);
981
+ if (first === void 0 || last === void 0) return null;
982
+ const at2 = indices.indexOf(active);
983
+ if (key === "ArrowDown") return at2 === -1 ? first : indices[at2 + 1] ?? last;
984
+ if (key === "ArrowUp") return at2 === -1 ? last : at2 === 0 ? -1 : indices[at2 - 1] ?? first;
985
+ return null;
389
986
  }
390
- function scrollAppBar(state, behavior, scrollTop, delta) {
391
- const next = { ...state, contentOffset: -scrollTop, atStart: scrollTop <= 0 };
392
- return behavior === "enter-always" ? withHeightOffset(next, state.heightOffset + delta) : next;
987
+
988
+ // src/button-group.ts
989
+ function buttonGroupVisibleCount(widths, spacing, maxWidth, overflowWidth) {
990
+ const desired = widths.reduce((sum, w) => sum + w, 0) + spacing * (widths.length - 1);
991
+ if (desired <= maxWidth) return widths.length;
992
+ let remaining = maxWidth - overflowWidth;
993
+ let last = 0;
994
+ for (const width of widths) {
995
+ if (width > remaining) break;
996
+ remaining -= width + spacing;
997
+ last++;
998
+ }
999
+ return last;
393
1000
  }
394
- function settleTarget(state) {
395
- const fraction = collapsedFraction(state);
396
- if (fraction < 0.01 || fraction === 1) return null;
397
- if (!(state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit)) return null;
398
- return fraction < 0.5 ? 0 : state.heightOffsetLimit;
1001
+ function buttonGroupWidths(items, pressed, expandedRatio, visibleCount = items.length) {
1002
+ const widths = items.map((item) => item.width);
1003
+ const w = (i) => widths[i] ?? 0;
1004
+ const limit = (i) => items[i]?.compressionLimit ?? 0;
1005
+ const last = Math.min(visibleCount, items.length);
1006
+ if (items.length <= 1) return widths.slice(0, last);
1007
+ for (let index = 0; index < last; index++) {
1008
+ const value = pressed[index] ?? 0;
1009
+ if (value === 0) continue;
1010
+ let growth;
1011
+ if (index >= 1 && index < last - 1) {
1012
+ const target = value * Math.min(expandedRatio * w(index) / 2, limit(index - 1), limit(index + 1));
1013
+ const left = Math.min(target, w(index - 1));
1014
+ const right = Math.min(target, w(index + 1));
1015
+ widths[index - 1] = w(index - 1) - left;
1016
+ widths[index + 1] = w(index + 1) - right;
1017
+ growth = left + right;
1018
+ } else if (index === 0) {
1019
+ const target = value * Math.min(expandedRatio * w(index), limit(index + 1));
1020
+ const right = Math.min(target, w(index + 1));
1021
+ widths[index + 1] = w(index + 1) - right;
1022
+ growth = right;
1023
+ } else {
1024
+ const target = value * Math.min(expandedRatio * w(index), limit(index - 1));
1025
+ const left = Math.min(target, w(index - 1));
1026
+ widths[index - 1] = w(index - 1) - left;
1027
+ growth = left;
1028
+ }
1029
+ widths[index] = w(index) + growth;
1030
+ }
1031
+ return widths.slice(0, last);
399
1032
  }
400
1033
 
401
- export { DEFAULT_DISPLACEMENT_THRESHOLD, SpringSimulation, appBarScrollState, collapsedFraction, dampingCoefficient, dampingRatioFromCoefficient, estimateAnimationDurationMillis, estimateAnimationDurationMillisDouble, indeterminateFrame, initialIndeterminateFrame, isScrolledUnder, morphSpringDurationMillis, overlappedFraction, scrollAppBar, settleTarget, springDurationMillis, springToLinear, springValue, springVelocity, withHeight, withHeightOffset };
1034
+ export { AmplitudeAnimation, DEFAULT_DISPLACEMENT_THRESHOLD, FAST_OUT_LINEAR_IN_EASING, SEARCH_BAR_EXPANDED_THRESHOLD, SEARCH_DROPDOWN_VISIBLE_PROGRESS, SearchBarAnimation, SpringAnimatable, SpringSimulation, TOP_TITLE_ALPHA_EASING, TweenAnimatable, animatedInt, appBarScrollState, buttonGroupVisibleCount, buttonGroupWidths, circularIndeterminateFrame, collapsedFraction, cubicBezier, dampingCoefficient, dampingRatioFromCoefficient, estimateAnimationDurationMillis, estimateAnimationDurationMillisDouble, fabMenuItemVisible, floatingToolbarScrollState, indeterminateFrame, indicatorAmplitude, initialIndeterminateFrame, isScrolledUnder, linearIndeterminateFractions, menuNavigationTarget, menuPosition, menuTransformOrigin, morphSpringDurationMillis, overlappedFraction, parseCubicBezier, scrollAppBar, scrollFloatingToolbar, scrollToolbarExpansion, searchNavigationTarget, searchViewFrame, settleTarget, springDurationMillis, springToLinear, springValue, springVelocity, toolbarCollapsedFraction, toolbarExpansionScroll, toolbarNavigationTarget, toolbarSettleTarget, twoRowsFrame, waveOffset, waveTravelMillis, withHeight, withHeightOffset, withToolbarOffset, withToolbarOffsetLimit };
402
1035
  //# sourceMappingURL=index.js.map
403
1036
  //# sourceMappingURL=index.js.map