@bearmenu/ui 0.8.7 → 0.8.8

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 (81) hide show
  1. package/dist/{chunk-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
  2. package/dist/chunk-AHL45DD2.js +145 -0
  3. package/dist/chunk-IYDH5IU6.js +66 -0
  4. package/dist/chunk-OPVKL4VS.js +18 -0
  5. package/dist/components/alert.d.ts +2 -2
  6. package/dist/components/badge.d.ts +1 -1
  7. package/dist/components/category-deck.js +10 -1
  8. package/dist/components/coverflow.d.ts +11 -3
  9. package/dist/components/coverflow.js +34 -51
  10. package/dist/components/date-input.d.ts +1 -1
  11. package/dist/components/drift-rail.d.ts +24 -10
  12. package/dist/components/drift-rail.js +1 -1
  13. package/dist/components/event-card.d.ts +8 -0
  14. package/dist/components/event-card.js +63 -33
  15. package/dist/components/event-compact-row.d.ts +32 -5
  16. package/dist/components/event-compact-row.js +119 -38
  17. package/dist/components/event-day-header.js +3 -1
  18. package/dist/components/event-day-separator.d.ts +7 -1
  19. package/dist/components/event-day-separator.js +3 -2
  20. package/dist/components/event-hub-hero.d.ts +25 -4
  21. package/dist/components/event-hub-hero.js +89 -19
  22. package/dist/components/event-list-row.d.ts +19 -6
  23. package/dist/components/event-list-row.js +78 -34
  24. package/dist/components/hour-band.d.ts +38 -40
  25. package/dist/components/hour-band.js +55 -27
  26. package/dist/components/hour-group.js +1 -1
  27. package/dist/components/poster-tape.js +1 -1
  28. package/dist/components/poster-wall.d.ts +3 -3
  29. package/dist/components/poster-wall.js +73 -17
  30. package/dist/components/section-header.d.ts +16 -1
  31. package/dist/components/section-header.js +26 -7
  32. package/dist/components/snap-rail.d.ts +34 -0
  33. package/dist/components/snap-rail.js +51 -0
  34. package/dist/components/tag.d.ts +2 -2
  35. package/dist/components/text.d.ts +1 -1
  36. package/dist/components/timeline-band.d.ts +20 -2
  37. package/dist/components/timeline-band.js +1 -1
  38. package/dist/components/timeline-playhead.d.ts +26 -0
  39. package/dist/components/timeline-playhead.js +25 -0
  40. package/dist/components/weekend-panel.d.ts +5 -4
  41. package/dist/components/weekend-panel.js +85 -71
  42. package/dist/hooks/use-drag-swipe.d.ts +44 -0
  43. package/dist/hooks/use-drag-swipe.js +2 -0
  44. package/dist/hooks/use-media-query.d.ts +17 -0
  45. package/dist/hooks/use-media-query.js +2 -0
  46. package/package.json +1 -1
  47. package/src/components/category-deck.tsx +19 -8
  48. package/src/components/coverflow.test.tsx +28 -1
  49. package/src/components/coverflow.tsx +37 -43
  50. package/src/components/drift-rail.tsx +167 -42
  51. package/src/components/event-card.tsx +55 -10
  52. package/src/components/event-compact-row.stories.tsx +30 -0
  53. package/src/components/event-compact-row.test.tsx +49 -0
  54. package/src/components/event-compact-row.tsx +161 -37
  55. package/src/components/event-day-header.tsx +3 -1
  56. package/src/components/event-day-separator.tsx +8 -1
  57. package/src/components/event-hub-hero.stories.tsx +30 -0
  58. package/src/components/event-hub-hero.tsx +102 -30
  59. package/src/components/event-list-row.stories.tsx +34 -0
  60. package/src/components/event-list-row.test.tsx +53 -0
  61. package/src/components/event-list-row.tsx +135 -87
  62. package/src/components/hour-band.stories.tsx +28 -5
  63. package/src/components/hour-band.tsx +90 -70
  64. package/src/components/poster-wall.test.tsx +28 -0
  65. package/src/components/poster-wall.tsx +96 -23
  66. package/src/components/section-header.stories.tsx +24 -0
  67. package/src/components/section-header.test.tsx +28 -0
  68. package/src/components/section-header.tsx +45 -7
  69. package/src/components/snap-rail.stories.tsx +79 -0
  70. package/src/components/snap-rail.test.tsx +18 -0
  71. package/src/components/snap-rail.tsx +67 -0
  72. package/src/components/timeline-band.tsx +52 -12
  73. package/src/components/timeline-playhead.stories.tsx +30 -0
  74. package/src/components/timeline-playhead.test.tsx +11 -0
  75. package/src/components/timeline-playhead.tsx +46 -0
  76. package/src/components/weekend-panel.test.tsx +43 -0
  77. package/src/components/weekend-panel.tsx +142 -108
  78. package/src/hooks/use-drag-swipe.test.tsx +66 -0
  79. package/src/hooks/use-drag-swipe.ts +105 -0
  80. package/src/hooks/use-media-query.ts +31 -0
  81. package/dist/chunk-GPH6L4DX.js +0 -55
@@ -1,10 +1,19 @@
1
1
  import { Separator } from './chunk-WSRWFA6K.js';
2
2
  import { Skeleton } from './chunk-QXHMPJ35.js';
3
3
  import { cn } from './chunk-FEK5XGZW.js';
4
+ import { Radio } from 'lucide-react';
4
5
  import { jsxs, jsx } from 'react/jsx-runtime';
5
6
 
6
7
  var LIVE_PUCK_PULSE_CLASS = "bm-timeline-live-puck-pulse";
7
- function TimelineHourPuck({ time, isLive, liveLabel, className }) {
8
+ var TIMELINE_MOBILE_GUTTER_CLASS = "w-[58px]";
9
+ function TimelineHourPuck({
10
+ time,
11
+ isLive,
12
+ liveLabel,
13
+ variant = "station",
14
+ className
15
+ }) {
16
+ const isChip = variant === "chip";
8
17
  return /* @__PURE__ */ jsxs("span", { className: cn("relative flex justify-center", className), children: [
9
18
  isLive && /* @__PURE__ */ jsx("style", { children: `
10
19
  @keyframes ${LIVE_PUCK_PULSE_CLASS} {
@@ -18,23 +27,33 @@ function TimelineHourPuck({ time, isLive, liveLabel, className }) {
18
27
  .${LIVE_PUCK_PULSE_CLASS} { animation: none; }
19
28
  }
20
29
  ` }),
21
- /* @__PURE__ */ jsx(
30
+ /* @__PURE__ */ jsxs(
22
31
  "span",
23
32
  {
24
33
  className: cn(
25
- "whitespace-nowrap rounded-[7px] border bg-background px-[7px] py-[3px]",
26
- "font-mono text-[12.5px] font-medium tabular-nums",
27
- isLive ? cn("border-success/40 text-success", LIVE_PUCK_PULSE_CLASS) : "border-border text-foreground"
34
+ "whitespace-nowrap font-mono tabular-nums",
35
+ isChip ? cn(
36
+ "inline-flex h-6 min-w-[58px] items-center justify-center gap-1 rounded-[7px] px-1 text-[12px] font-medium",
37
+ isLive ? cn("bg-success/10 text-success", LIVE_PUCK_PULSE_CLASS) : "bg-muted text-foreground"
38
+ ) : cn(
39
+ "rounded-[7px] border bg-background px-[7px] py-[3px] text-[12.5px] font-medium",
40
+ isLive ? cn("border-success/40 text-success", LIVE_PUCK_PULSE_CLASS) : "border-border text-foreground"
41
+ )
28
42
  ),
29
- children: time
43
+ children: [
44
+ isChip && isLive && /* @__PURE__ */ jsx(Radio, { className: "h-2.5 w-2.5 shrink-0", "aria-hidden": true }),
45
+ time
46
+ ]
30
47
  }
31
48
  ),
32
49
  isLive && !!liveLabel && /* @__PURE__ */ jsx(
33
50
  "span",
34
51
  {
35
52
  className: cn(
36
- "absolute top-[calc(100%+6px)] whitespace-nowrap bg-background px-1",
37
- "text-[9.5px] font-extrabold tracking-[0.08em] text-success"
53
+ isChip ? "sr-only" : cn(
54
+ "absolute top-[calc(100%+6px)] whitespace-nowrap bg-background px-1",
55
+ "text-[9.5px] font-extrabold tracking-[0.08em] text-success"
56
+ )
38
57
  ),
39
58
  children: liveLabel
40
59
  }
@@ -82,4 +101,4 @@ function TimelineBandSkeleton({ className }) {
82
101
  ] });
83
102
  }
84
103
 
85
- export { TimelineBand, TimelineBandSkeleton, TimelineHourPuck };
104
+ export { TIMELINE_MOBILE_GUTTER_CLASS, TimelineBand, TimelineBandSkeleton, TimelineHourPuck };
@@ -0,0 +1,145 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { useRef, useState, useEffect } from 'react';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ var DRIFT_RAIL_DEFAULT_DURATION_SECONDS = 42;
6
+ var DRIFT_RAIL_RESUME_DELAY_MS = 3e3;
7
+ function DriftRail({
8
+ children,
9
+ reverse = false,
10
+ durationSeconds = DRIFT_RAIL_DEFAULT_DURATION_SECONDS,
11
+ pauseOnHover = true,
12
+ className
13
+ }) {
14
+ const scrollerRef = useRef(null);
15
+ const trackRef = useRef(null);
16
+ const [isHovered, setIsHovered] = useState(false);
17
+ const [inView, setInView] = useState(true);
18
+ const [reducedMotion, setReducedMotion] = useState(false);
19
+ const heldUntil = useRef(0);
20
+ const mouseDrag = useRef(null);
21
+ const position = useRef(0);
22
+ useEffect(() => {
23
+ if (typeof window === "undefined" || !window.matchMedia) return;
24
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
25
+ setReducedMotion(query.matches);
26
+ const onChange = () => setReducedMotion(query.matches);
27
+ query.addEventListener("change", onChange);
28
+ return () => query.removeEventListener("change", onChange);
29
+ }, []);
30
+ useEffect(() => {
31
+ const node = scrollerRef.current;
32
+ if (!node || typeof IntersectionObserver === "undefined") return void 0;
33
+ const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
34
+ threshold: 0
35
+ });
36
+ observer.observe(node);
37
+ return () => observer.disconnect();
38
+ }, []);
39
+ useEffect(() => {
40
+ const scroller = scrollerRef.current;
41
+ const track = trackRef.current;
42
+ if (!scroller || !track) return void 0;
43
+ let frame = 0;
44
+ let last = performance.now();
45
+ const drifting = !reducedMotion && inView && !(pauseOnHover && isHovered);
46
+ if (reverse && scroller.scrollLeft === 0) scroller.scrollLeft = track.scrollWidth / 2;
47
+ position.current = scroller.scrollLeft;
48
+ const tick = (now) => {
49
+ const half = track.scrollWidth / 2;
50
+ if (half > 0) {
51
+ const driftingNow = drifting && now >= heldUntil.current;
52
+ if (driftingNow) {
53
+ const perSecond = half / durationSeconds;
54
+ const delta = perSecond * (now - last) / 1e3;
55
+ position.current += reverse ? -delta : delta;
56
+ } else {
57
+ position.current = scroller.scrollLeft;
58
+ }
59
+ let wrapped = false;
60
+ if (position.current >= half) {
61
+ position.current -= half;
62
+ wrapped = true;
63
+ } else if (position.current < 0) {
64
+ position.current += half;
65
+ wrapped = true;
66
+ }
67
+ if (driftingNow || wrapped) scroller.scrollLeft = position.current;
68
+ }
69
+ last = now;
70
+ frame = requestAnimationFrame(tick);
71
+ };
72
+ frame = requestAnimationFrame(tick);
73
+ return () => cancelAnimationFrame(frame);
74
+ }, [durationSeconds, inView, isHovered, pauseOnHover, reducedMotion, reverse]);
75
+ const hold = () => {
76
+ heldUntil.current = Number.POSITIVE_INFINITY;
77
+ };
78
+ const release = () => {
79
+ heldUntil.current = performance.now() + DRIFT_RAIL_RESUME_DELAY_MS;
80
+ };
81
+ const onPointerDown = (event) => {
82
+ hold();
83
+ if (event.pointerType !== "mouse" || event.button !== 0 || !scrollerRef.current) return;
84
+ mouseDrag.current = {
85
+ startX: event.clientX,
86
+ startScroll: scrollerRef.current.scrollLeft,
87
+ pointerId: event.pointerId
88
+ };
89
+ };
90
+ const onPointerMove = (event) => {
91
+ var _a, _b, _c, _d;
92
+ const drag = mouseDrag.current;
93
+ if (!drag || event.pointerId !== drag.pointerId || !scrollerRef.current) return;
94
+ const dx = event.clientX - drag.startX;
95
+ if (Math.abs(dx) > 4 && !((_b = (_a = event.currentTarget).hasPointerCapture) == null ? void 0 : _b.call(_a, event.pointerId))) {
96
+ (_d = (_c = event.currentTarget).setPointerCapture) == null ? void 0 : _d.call(_c, event.pointerId);
97
+ event.currentTarget.dataset.dragging = "true";
98
+ }
99
+ scrollerRef.current.scrollLeft = drag.startScroll - dx;
100
+ };
101
+ const onPointerEnd = (event) => {
102
+ var _a, _b;
103
+ release();
104
+ const drag = mouseDrag.current;
105
+ mouseDrag.current = null;
106
+ if (drag && ((_b = (_a = event.currentTarget).hasPointerCapture) == null ? void 0 : _b.call(_a, event.pointerId))) {
107
+ event.currentTarget.releasePointerCapture(event.pointerId);
108
+ }
109
+ delete event.currentTarget.dataset.dragging;
110
+ };
111
+ const onClickCapture = (event) => {
112
+ if (event.currentTarget.dataset.dragging === "true") {
113
+ event.preventDefault();
114
+ event.stopPropagation();
115
+ }
116
+ };
117
+ return /* @__PURE__ */ jsx(
118
+ "div",
119
+ {
120
+ ref: scrollerRef,
121
+ onMouseEnter: () => setIsHovered(true),
122
+ onMouseLeave: () => setIsHovered(false),
123
+ onPointerDown,
124
+ onPointerMove,
125
+ onPointerUp: onPointerEnd,
126
+ onPointerCancel: onPointerEnd,
127
+ onWheel: () => {
128
+ release();
129
+ },
130
+ onDragStart: (event) => event.preventDefault(),
131
+ onClickCapture,
132
+ className: cn(
133
+ "overflow-x-auto overflow-y-hidden py-1.5 scrollbar-hide [touch-action:pan-x]",
134
+ "cursor-grab select-none data-[dragging=true]:cursor-grabbing",
135
+ className
136
+ ),
137
+ children: /* @__PURE__ */ jsxs("div", { ref: trackRef, className: "flex w-max items-stretch gap-3.5", children: [
138
+ children,
139
+ /* @__PURE__ */ jsx("div", { className: "contents", "aria-hidden": "true", children })
140
+ ] })
141
+ }
142
+ );
143
+ }
144
+
145
+ export { DriftRail };
@@ -0,0 +1,66 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+
3
+ function useDragSwipe({ threshold = 40, onSwipe, disabled }) {
4
+ const [dragX, setDragX] = useState(0);
5
+ const startX = useRef(null);
6
+ const pointerId = useRef(null);
7
+ const didDrag = useRef(false);
8
+ const onPointerDown = useCallback(
9
+ (event) => {
10
+ if (disabled || event.button !== 0) return;
11
+ startX.current = event.clientX;
12
+ pointerId.current = event.pointerId;
13
+ didDrag.current = false;
14
+ },
15
+ [disabled]
16
+ );
17
+ const onPointerMove = useCallback((event) => {
18
+ var _a, _b;
19
+ if (startX.current === null || event.pointerId !== pointerId.current) return;
20
+ const dx = event.clientX - startX.current;
21
+ if (!didDrag.current && Math.abs(dx) > 6) {
22
+ didDrag.current = true;
23
+ (_b = (_a = event.currentTarget).setPointerCapture) == null ? void 0 : _b.call(_a, event.pointerId);
24
+ }
25
+ if (didDrag.current) setDragX(dx);
26
+ }, []);
27
+ const end = useCallback(
28
+ (event, cancelled) => {
29
+ var _a, _b;
30
+ if (startX.current === null || event.pointerId !== pointerId.current) return;
31
+ const dx = event.clientX - startX.current;
32
+ startX.current = null;
33
+ pointerId.current = null;
34
+ if ((_b = (_a = event.currentTarget).hasPointerCapture) == null ? void 0 : _b.call(_a, event.pointerId)) {
35
+ event.currentTarget.releasePointerCapture(event.pointerId);
36
+ }
37
+ setDragX(0);
38
+ if (!cancelled && didDrag.current && Math.abs(dx) >= threshold) {
39
+ onSwipe(dx < 0 ? 1 : -1);
40
+ }
41
+ },
42
+ [onSwipe, threshold]
43
+ );
44
+ const onPointerUp = useCallback((event) => end(event, false), [end]);
45
+ const onPointerCancel = useCallback(
46
+ (event) => end(event, true),
47
+ [end]
48
+ );
49
+ const onDragStart = useCallback((event) => {
50
+ event.preventDefault();
51
+ }, []);
52
+ const onClickCapture = useCallback((event) => {
53
+ if (didDrag.current) {
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ didDrag.current = false;
57
+ }
58
+ }, []);
59
+ return {
60
+ dragX,
61
+ isDragging: dragX !== 0,
62
+ handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onClickCapture, onDragStart }
63
+ };
64
+ }
65
+
66
+ export { useDragSwipe };
@@ -0,0 +1,18 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ function useMediaQuery(query) {
4
+ return useSyncExternalStore(
5
+ (onChange) => {
6
+ if (typeof window.matchMedia !== "function") return () => {
7
+ };
8
+ const mql = window.matchMedia(query);
9
+ mql.addEventListener("change", onChange);
10
+ return () => mql.removeEventListener("change", onChange);
11
+ },
12
+ () => typeof window.matchMedia === "function" ? window.matchMedia(query).matches : false,
13
+ () => false
14
+ );
15
+ }
16
+ var DESKTOP_MEDIA_QUERY = "(min-width: 1024px)";
17
+
18
+ export { DESKTOP_MEDIA_QUERY, useMediaQuery };
@@ -3,10 +3,10 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const alertVariants: (props?: ({
6
- variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
6
+ variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
7
7
  } & class_variance_authority_types.ClassProp) | undefined) => string;
8
8
  declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & VariantProps<(props?: ({
9
- variant?: "default" | "destructive" | "success" | "warning" | null | undefined;
9
+ variant?: "success" | "warning" | "default" | "destructive" | null | undefined;
10
10
  } & class_variance_authority_types.ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
11
11
  declare const AlertTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
12
12
  declare const AlertDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import { VariantProps } from 'class-variance-authority';
5
5
 
6
6
  declare const badgeVariants: (props?: ({
7
- variant?: "default" | "destructive" | "outline" | "secondary" | "glass" | "overlay" | "brown" | "success" | "warning" | null | undefined;
7
+ variant?: "success" | "warning" | "default" | "destructive" | "outline" | "secondary" | "glass" | "overlay" | "brown" | null | undefined;
8
8
  live?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
10
10
  interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
@@ -57,7 +57,10 @@ function CategoryDeck({
57
57
  {
58
58
  className: cn(
59
59
  "absolute left-1/2 top-0 aspect-[4/5] w-24 overflow-hidden rounded-[10px] bg-muted",
60
+ // Keyed to --foreground so the shadow is ink in light mode; in
61
+ // dark mode that same 18% is a light halo, so it drops to 7%.
60
62
  "shadow-[0_10px_26px_color-mix(in_oklch,var(--foreground)_18%,transparent)]",
63
+ "dark:shadow-[0_10px_26px_color-mix(in_oklch,var(--foreground)_7%,transparent)]",
61
64
  "sm:w-[120px]",
62
65
  transform.rest,
63
66
  transform.spread,
@@ -86,7 +89,13 @@ function CategoryDeck({
86
89
  /* @__PURE__ */ jsx("div", { className: "truncate text-[14.5px] font-extrabold tracking-[-0.015em] sm:text-base", children: name }),
87
90
  /* @__PURE__ */ jsx("div", { className: "truncate font-mono text-[11.5px] text-muted-foreground", children: meta })
88
91
  ] }),
89
- /* @__PURE__ */ jsx(ChevronRight, { className: "ml-auto h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
92
+ /* @__PURE__ */ jsx(
93
+ ChevronRight,
94
+ {
95
+ className: "ml-auto h-4 w-4 shrink-0 text-muted-foreground",
96
+ "aria-hidden": true
97
+ }
98
+ )
90
99
  ] })
91
100
  ] });
92
101
  const rootClasses = cn(
@@ -3,9 +3,17 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  /**
4
4
  * Coverflow — the busiest-events browser: a 3D perspective lane with a large
5
5
  * lead poster flanked by up to three shrinking, desaturating posters on each
6
- * side. The lane itself drifts a few pixels back and forth
7
- * (`prefers-reduced-motion` stops this outright; an `IntersectionObserver`
8
- * pauses it off-screen).
6
+ * side.
7
+ *
8
+ * NOTHING MOVES ON ITS OWN. The lane used to drift ±40px on a loop; it was
9
+ * removed at product request — a stage that sways is read as a carousel that
10
+ * is about to advance, and the reader waits for it instead of touching it.
11
+ * The stage is driven by the reader: a horizontal drag (touch, pen or mouse)
12
+ * steps the ring one card in the drag's direction and the lane follows the
13
+ * finger a little on the way, so the gesture has weight. The prev/next arrows
14
+ * stay for desktop and keyboard; below `lg` they are hidden so nothing sits
15
+ * over the lead card — the swipe is the control there, and every card is
16
+ * still a focusable, named button for keyboard and screen-reader users.
9
17
  *
10
18
  * IT IS A RING, NOT A LANE. A card's slot comes from `ringOffset` — the
11
19
  * SIGNED SHORTEST WAY ROUND the list, not the raw `index - activeIndex`. With
@@ -1,7 +1,8 @@
1
+ import { useDragSwipe } from '../chunk-IYDH5IU6.js';
1
2
  import { EventPosterFallback } from '../chunk-FDITZ2VM.js';
2
3
  import { cn } from '../chunk-FEK5XGZW.js';
3
- import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
- import { useId, useRef, useState, useEffect } from 'react';
4
+ import { __spreadProps, __spreadValues, __objRest } from '../chunk-3GWWZKX7.js';
5
+ import { useId, useState } from 'react';
5
6
  import { ChevronLeft, ChevronRight, Gift } from 'lucide-react';
6
7
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
7
8
 
@@ -21,10 +22,8 @@ var COVERFLOW_CARD_FOCUS_CLASS = "focus-visible:outline-none focus-visible:ring-
21
22
  var COVERFLOW_SIDE_SHADOW_CLASS = "shadow-[0_11px_27px_oklch(0.3_0.02_60/0.22)]";
22
23
  var COVERFLOW_LEAD_SHADOW_CLASS = "shadow-[0_24px_56px_oklch(0.28_0.03_45/0.32)]";
23
24
  var COVERFLOW_PILL_CLASS = "inline-flex h-6 max-w-[85%] items-center gap-1 rounded-full px-2 text-[11px] font-semibold";
24
- var COVERFLOW_DRIFT_DURATION_SECONDS = 26;
25
- var COVERFLOW_DRIFT_PIXELS = 40;
26
25
  var COVERFLOW_LEAD_Z_INDEX = 6;
27
- var COVERFLOW_ARROW_CLASS = "absolute top-1/2 z-20 flex h-11 w-11 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full bg-background/85 text-foreground shadow-[var(--shadow-overlay)] backdrop-blur-sm transition-colors duration-150 hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none";
26
+ var COVERFLOW_ARROW_CLASS = "absolute top-1/2 z-20 hidden h-11 w-11 -translate-y-1/2 lg:flex cursor-pointer items-center justify-center rounded-full bg-background/85 text-foreground shadow-[var(--shadow-overlay)] backdrop-blur-sm transition-colors duration-150 hover:bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none";
28
27
  function wrapIndex(index, length) {
29
28
  return (index % length + length) % length;
30
29
  }
@@ -141,7 +140,6 @@ function Coverflow({
141
140
  }) {
142
141
  const scopeId = useId().replace(/[^a-zA-Z0-9]/g, "");
143
142
  const scopeClass = `coverflow-${scopeId}`;
144
- const containerRef = useRef(null);
145
143
  const length = items.length;
146
144
  const active = length > 0 ? wrapIndex(activeIndex, length) : 0;
147
145
  const [step, setStep] = useState({ from: active, to: active, length });
@@ -150,21 +148,13 @@ function Coverflow({
150
148
  step.length === length ? { from: step.to, to: active, length } : { from: active, to: active, length }
151
149
  );
152
150
  }
153
- useEffect(() => {
154
- const node = containerRef.current;
155
- if (!node || typeof IntersectionObserver === "undefined") return void 0;
156
- const observer = new IntersectionObserver(
157
- ([entry]) => {
158
- node.dataset.offscreen = entry.isIntersecting ? "false" : "true";
159
- },
160
- { threshold: 0 }
161
- );
162
- observer.observe(node);
163
- return () => observer.disconnect();
164
- }, []);
151
+ const goTo = (index) => onActiveIndexChange == null ? void 0 : onActiveIndexChange(wrapIndex(index, length));
152
+ const { dragX, isDragging, handlers } = useDragSwipe({
153
+ onSwipe: (direction) => goTo(active + direction),
154
+ disabled: length < 2
155
+ });
165
156
  if (length === 0) return null;
166
157
  const activeItem = items[active];
167
- const goTo = (index) => onActiveIndexChange == null ? void 0 : onActiveIndexChange(wrapIndex(index, length));
168
158
  const activeDelta = ringOffset(step.to - step.from, length);
169
159
  const visibleCards = items.map((item, index) => {
170
160
  const offset = ringOffset(index - active, length);
@@ -208,28 +198,11 @@ function Coverflow({
208
198
  --cf-tier-base: 0.5714;
209
199
  }
210
200
  }
211
- .${scopeClass} .coverflow-lane {
212
- animation: ${scopeClass}-drift ${COVERFLOW_DRIFT_DURATION_SECONDS}s ease-in-out infinite alternate;
213
- }
214
- .${scopeClass}[data-offscreen="true"] .coverflow-lane {
215
- animation-play-state: paused;
216
- }
217
- @keyframes ${scopeClass}-drift {
218
- from { transform: translateX(-${COVERFLOW_DRIFT_PIXELS}px); }
219
- to { transform: translateX(${COVERFLOW_DRIFT_PIXELS}px); }
220
- }
221
- @media (prefers-reduced-motion: reduce) {
222
- .${scopeClass} .coverflow-lane {
223
- animation: none;
224
- transform: none;
225
- }
226
- }
227
201
  ` }),
228
202
  /* @__PURE__ */ jsxs(
229
203
  "div",
230
- {
231
- ref: containerRef,
232
- className: "relative flex h-[300px] items-center justify-center overflow-x-clip overflow-y-visible [perspective:900px] sm:h-[380px] sm:[perspective:1400px]",
204
+ __spreadProps(__spreadValues({}, handlers), {
205
+ className: "relative flex h-[300px] select-none items-center justify-center overflow-x-clip overflow-y-visible [touch-action:pan-y] [perspective:900px] sm:h-[380px] sm:[perspective:1400px]",
233
206
  children: [
234
207
  /* @__PURE__ */ jsx(
235
208
  "div",
@@ -267,21 +240,31 @@ function Coverflow({
267
240
  }
268
241
  )
269
242
  ] }),
270
- /* @__PURE__ */ jsx("div", { className: "coverflow-lane relative h-full w-full [transform-style:preserve-3d] motion-reduce:[animation:none]", children: visibleCards.map(({ item, index, offset, suppressTransition }) => /* @__PURE__ */ jsx(
271
- CoverflowCard,
243
+ /* @__PURE__ */ jsx(
244
+ "div",
272
245
  {
273
- item,
274
- index,
275
- offset,
276
- suppressTransition,
277
- freeLabel,
278
- onActivate: goTo,
279
- linkComponent: LinkComponent
280
- },
281
- item.id
282
- )) })
246
+ className: cn(
247
+ "coverflow-lane relative h-full w-full [transform-style:preserve-3d]",
248
+ !isDragging && "transition-transform duration-300 ease-out motion-reduce:transition-none"
249
+ ),
250
+ style: { transform: `translateX(${dragX / 3}px)` },
251
+ children: visibleCards.map(({ item, index, offset, suppressTransition }) => /* @__PURE__ */ jsx(
252
+ CoverflowCard,
253
+ {
254
+ item,
255
+ index,
256
+ offset,
257
+ suppressTransition,
258
+ freeLabel,
259
+ onActivate: goTo,
260
+ linkComponent: LinkComponent
261
+ },
262
+ item.id
263
+ ))
264
+ }
265
+ )
283
266
  ]
284
- }
267
+ })
285
268
  ),
286
269
  /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-4 text-center", children: [
287
270
  /* @__PURE__ */ jsxs("div", { "aria-live": "polite", children: [
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
 
3
- declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size"> & {
3
+ declare const DateInput: React.ForwardRefExoticComponent<Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "type"> & {
4
4
  /** Native input type. Defaults to "date". */
5
5
  type?: "date" | "datetime-local" | "time" | "month" | "week";
6
6
  size?: "sm" | "default" | "lg";
@@ -2,22 +2,36 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
3
  /**
4
4
  * DriftRail — an infinite horizontal marquee for logos, tag chips or small
5
- * poster tiles. The caller passes one set of children; the component renders
6
- * a second, `aria-hidden` copy right after it so a `translateX(-50%)` loop is
7
- * seamless. Because the loop depends on the duplicate being pixel-identical
8
- * to the first pass, keep `children` a fixed, ordered list — no per-render
9
- * randomisation.
5
+ * poster tiles that the reader can take hold of. The caller passes one set of
6
+ * children; the component renders a second, `aria-hidden` copy right after it
7
+ * so the loop is seamless. Because the loop depends on the duplicate being
8
+ * pixel-identical to the first pass, keep `children` a fixed, ordered list —
9
+ * no per-render randomisation.
10
10
  *
11
- * Motion is scoped to this instance via a generated id (no shared stylesheet,
12
- * no class collisions between multiple rails on one page) and is paused two
13
- * ways, per the source design: an `IntersectionObserver` stops the loop the
14
- * moment the rail scrolls off-screen, and `prefers-reduced-motion: reduce`
15
- * removes the animation outright rather than merely slowing it.
11
+ * IT IS A SCROLLER THAT DRIFTS, NOT AN ANIMATION. It used to be a CSS
12
+ * `translateX` keyframe, which is cheap but cannot be grabbed: a finger on it
13
+ * did nothing, and a marquee that ignores the hand on it reads as a broken
14
+ * scroller. Now the rail is a real horizontal scroll container with the
15
+ * scrollbar hidden, and a `requestAnimationFrame` loop advances `scrollLeft`
16
+ * at `width / durationSeconds` per second. When the scroll position passes
17
+ * the first copy it is wrapped back by exactly one copy's width — the two
18
+ * copies are identical, so the jump is invisible — and the same wrap holds
19
+ * while the reader scrolls, in either direction, so there is no end to hit.
20
+ *
21
+ * THE READER WINS. A touch scrolls natively (momentum and all); a mouse drag
22
+ * scrolls the rail by the drag distance; a wheel works because it is a
23
+ * scroller. Any of those stops the drift, and it resumes a few seconds after
24
+ * the last interaction. Hover pauses it too (`pauseOnHover`), and an
25
+ * `IntersectionObserver` stops the loop off-screen so the page is not doing
26
+ * work nobody can see. `prefers-reduced-motion` removes the drift entirely;
27
+ * the rail stays scrollable by hand, which is more than the static strip it
28
+ * used to collapse to.
16
29
  */
17
30
  type DriftRailProps = {
18
31
  children: React.ReactNode;
19
32
  /** Runs the loop right-to-left instead of left-to-right. */
20
33
  reverse?: boolean;
34
+ /** Seconds for one full pass of the children. */
21
35
  durationSeconds?: number;
22
36
  /** Pauses the loop while the pointer hovers the rail. */
23
37
  pauseOnHover?: boolean;
@@ -1,3 +1,3 @@
1
- export { DriftRail } from '../chunk-GPH6L4DX.js';
1
+ export { DriftRail } from '../chunk-AHL45DD2.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-3GWWZKX7.js';
@@ -61,6 +61,14 @@ type EventCardProps = {
61
61
  liveStatus?: string;
62
62
  /** Hide the day/time line — the timeline supplies its own hour heading. */
63
63
  hideWhen?: boolean;
64
+ /**
65
+ * Already over — dims the poster, mutes the title and shows the `endedLabel`
66
+ * chip. Never row-level opacity, which composites the text below AA. Same
67
+ * contract as `EventCompactRow`.
68
+ */
69
+ isEnded?: boolean;
70
+ /** Chip text for an ended event, e.g. "Ended". Required to render the chip. */
71
+ endedLabel?: string;
64
72
  /** Overrides the day label when the card appears outside its own day section. */
65
73
  dayLabel?: string;
66
74
  /** Append the entry fee to the meta line. Off by default, matching the design's base card. */