@bearmenu/ui 0.8.7 → 0.8.9

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 (179) hide show
  1. package/dist/{chunk-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
  2. package/dist/chunk-62ES6N32.js +27 -0
  3. package/dist/chunk-AHL45DD2.js +145 -0
  4. package/dist/chunk-AXV5O2IZ.js +58 -0
  5. package/dist/chunk-IYDH5IU6.js +66 -0
  6. package/dist/chunk-OPVKL4VS.js +18 -0
  7. package/dist/chunk-Q6QH52LJ.js +38 -0
  8. package/dist/chunk-TH6W2PWG.js +213 -0
  9. package/dist/chunk-THOVPZNN.js +37 -0
  10. package/dist/chunk-YS6BHKZO.js +92 -0
  11. package/dist/components/badge.d.ts +1 -1
  12. package/dist/components/button.d.ts +1 -1
  13. package/dist/components/category-deck.js +10 -1
  14. package/dist/components/command.d.ts +7 -7
  15. package/dist/components/concept-compare.d.ts +73 -0
  16. package/dist/components/concept-compare.js +162 -0
  17. package/dist/components/coverflow.d.ts +11 -3
  18. package/dist/components/coverflow.js +34 -51
  19. package/dist/components/date-input.d.ts +1 -1
  20. package/dist/components/drift-rail.d.ts +24 -10
  21. package/dist/components/drift-rail.js +1 -1
  22. package/dist/components/event-card.d.ts +8 -0
  23. package/dist/components/event-card.js +64 -34
  24. package/dist/components/event-compact-row.d.ts +33 -6
  25. package/dist/components/event-compact-row.js +120 -39
  26. package/dist/components/event-day-header.js +3 -1
  27. package/dist/components/event-day-separator.d.ts +7 -1
  28. package/dist/components/event-day-separator.js +3 -2
  29. package/dist/components/event-detail-panel.d.ts +171 -25
  30. package/dist/components/event-detail-panel.js +471 -174
  31. package/dist/components/event-hub-hero.d.ts +25 -4
  32. package/dist/components/event-hub-hero.js +89 -19
  33. package/dist/components/event-list-row-wide.js +1 -1
  34. package/dist/components/event-list-row.d.ts +19 -6
  35. package/dist/components/event-list-row.js +79 -35
  36. package/dist/components/hour-band.d.ts +38 -40
  37. package/dist/components/hour-band.js +55 -27
  38. package/dist/components/hour-group.js +1 -1
  39. package/dist/components/hub-surface.d.ts +27 -0
  40. package/dist/components/hub-surface.js +20 -0
  41. package/dist/components/menu-family-card.d.ts +59 -0
  42. package/dist/components/menu-family-card.js +121 -0
  43. package/dist/components/menu-field.d.ts +57 -0
  44. package/dist/components/menu-field.js +3 -0
  45. package/dist/components/menu-hub-hero.d.ts +65 -0
  46. package/dist/components/menu-hub-hero.js +103 -0
  47. package/dist/components/menu-intent-tiles.d.ts +31 -0
  48. package/dist/components/menu-intent-tiles.js +50 -0
  49. package/dist/components/menu-item.d.ts +127 -0
  50. package/dist/components/menu-item.js +5 -0
  51. package/dist/components/menu-mocks.d.ts +55 -0
  52. package/dist/components/menu-mocks.js +379 -0
  53. package/dist/components/menu-types.d.ts +245 -0
  54. package/dist/components/menu-types.js +1 -0
  55. package/dist/components/motion-icon-set.js +15 -3
  56. package/dist/components/open-now-list.d.ts +40 -0
  57. package/dist/components/open-now-list.js +69 -0
  58. package/dist/components/place-types.d.ts +112 -1
  59. package/dist/components/poster-tape.js +1 -1
  60. package/dist/components/poster-wall.d.ts +3 -3
  61. package/dist/components/poster-wall.js +73 -17
  62. package/dist/components/ranked-venue-list.d.ts +49 -0
  63. package/dist/components/ranked-venue-list.js +87 -0
  64. package/dist/components/section-header.d.ts +16 -1
  65. package/dist/components/section-header.js +26 -7
  66. package/dist/components/signature-chip.d.ts +37 -0
  67. package/dist/components/signature-chip.js +4 -0
  68. package/dist/components/signature-line-list.d.ts +45 -0
  69. package/dist/components/signature-line-list.js +99 -0
  70. package/dist/components/snap-rail.d.ts +54 -0
  71. package/dist/components/snap-rail.js +109 -0
  72. package/dist/components/spinner.d.ts +2 -2
  73. package/dist/components/stat-banner.d.ts +47 -0
  74. package/dist/components/stat-banner.js +61 -0
  75. package/dist/components/text.d.ts +3 -3
  76. package/dist/components/timeline-band.d.ts +20 -2
  77. package/dist/components/timeline-band.js +1 -1
  78. package/dist/components/timeline-playhead.d.ts +26 -0
  79. package/dist/components/timeline-playhead.js +25 -0
  80. package/dist/components/venue-busyness-strip.d.ts +35 -0
  81. package/dist/components/venue-busyness-strip.js +37 -0
  82. package/dist/components/venue-grade-dimensions.d.ts +36 -0
  83. package/dist/components/venue-grade-dimensions.js +3 -0
  84. package/dist/components/venue-quotes.d.ts +32 -0
  85. package/dist/components/venue-quotes.js +3 -0
  86. package/dist/components/venue-signature-block.d.ts +39 -0
  87. package/dist/components/venue-signature-block.js +6 -0
  88. package/dist/components/venue-spotlight.d.ts +74 -0
  89. package/dist/components/venue-spotlight.js +138 -0
  90. package/dist/components/venue-wall.d.ts +41 -0
  91. package/dist/components/venue-wall.js +118 -0
  92. package/dist/components/weekday-theme-banner.d.ts +70 -0
  93. package/dist/components/weekday-theme-banner.js +127 -0
  94. package/dist/components/weekend-panel.d.ts +5 -4
  95. package/dist/components/weekend-panel.js +85 -71
  96. package/dist/hooks/use-drag-swipe.d.ts +44 -0
  97. package/dist/hooks/use-drag-swipe.js +2 -0
  98. package/dist/hooks/use-media-query.d.ts +17 -0
  99. package/dist/hooks/use-media-query.js +2 -0
  100. package/package.json +1 -1
  101. package/src/components/category-deck.tsx +19 -8
  102. package/src/components/colour-dictionary.stories.tsx +227 -0
  103. package/src/components/concept-compare.stories.tsx +104 -0
  104. package/src/components/concept-compare.tsx +276 -0
  105. package/src/components/coverflow.test.tsx +28 -1
  106. package/src/components/coverflow.tsx +37 -43
  107. package/src/components/drift-rail.tsx +167 -42
  108. package/src/components/event-card.tsx +55 -10
  109. package/src/components/event-compact-row.stories.tsx +30 -0
  110. package/src/components/event-compact-row.test.tsx +49 -0
  111. package/src/components/event-compact-row.tsx +162 -38
  112. package/src/components/event-day-header.tsx +3 -1
  113. package/src/components/event-day-separator.tsx +8 -1
  114. package/src/components/event-detail-panel.stories.tsx +205 -97
  115. package/src/components/event-detail-panel.test.tsx +181 -0
  116. package/src/components/event-detail-panel.tsx +703 -185
  117. package/src/components/event-hub-hero.stories.tsx +30 -0
  118. package/src/components/event-hub-hero.tsx +102 -30
  119. package/src/components/event-list-row.stories.tsx +34 -0
  120. package/src/components/event-list-row.test.tsx +53 -0
  121. package/src/components/event-list-row.tsx +135 -87
  122. package/src/components/hour-band.stories.tsx +28 -5
  123. package/src/components/hour-band.tsx +90 -70
  124. package/src/components/hub-surface.stories.tsx +68 -0
  125. package/src/components/hub-surface.tsx +40 -0
  126. package/src/components/menu-family-card.stories.tsx +112 -0
  127. package/src/components/menu-family-card.tsx +211 -0
  128. package/src/components/menu-field.stories.tsx +74 -0
  129. package/src/components/menu-field.tsx +199 -0
  130. package/src/components/menu-hub-hero.stories.tsx +92 -0
  131. package/src/components/menu-hub-hero.tsx +187 -0
  132. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  133. package/src/components/menu-intent-tiles.tsx +76 -0
  134. package/src/components/menu-item.tsx +416 -0
  135. package/src/components/menu-mocks.ts +417 -0
  136. package/src/components/menu-types.ts +259 -0
  137. package/src/components/motion-icon-set.tsx +30 -3
  138. package/src/components/open-now-list.stories.tsx +76 -0
  139. package/src/components/open-now-list.tsx +127 -0
  140. package/src/components/place-types.ts +112 -0
  141. package/src/components/poster-wall.test.tsx +28 -0
  142. package/src/components/poster-wall.tsx +96 -23
  143. package/src/components/ranked-venue-list.stories.tsx +80 -0
  144. package/src/components/ranked-venue-list.tsx +177 -0
  145. package/src/components/section-header.stories.tsx +24 -0
  146. package/src/components/section-header.test.tsx +28 -0
  147. package/src/components/section-header.tsx +45 -7
  148. package/src/components/signature-chip.tsx +77 -0
  149. package/src/components/signature-line-list.stories.tsx +85 -0
  150. package/src/components/signature-line-list.tsx +136 -0
  151. package/src/components/snap-rail.stories.tsx +79 -0
  152. package/src/components/snap-rail.test.tsx +18 -0
  153. package/src/components/snap-rail.tsx +148 -0
  154. package/src/components/stat-banner.stories.tsx +100 -0
  155. package/src/components/stat-banner.tsx +117 -0
  156. package/src/components/timeline-band.tsx +52 -12
  157. package/src/components/timeline-playhead.stories.tsx +30 -0
  158. package/src/components/timeline-playhead.test.tsx +11 -0
  159. package/src/components/timeline-playhead.tsx +46 -0
  160. package/src/components/venue-busyness-strip.stories.tsx +66 -0
  161. package/src/components/venue-busyness-strip.tsx +64 -0
  162. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  163. package/src/components/venue-grade-dimensions.tsx +91 -0
  164. package/src/components/venue-quotes.stories.tsx +59 -0
  165. package/src/components/venue-quotes.tsx +64 -0
  166. package/src/components/venue-signature-block.stories.tsx +68 -0
  167. package/src/components/venue-signature-block.tsx +71 -0
  168. package/src/components/venue-spotlight.stories.tsx +141 -0
  169. package/src/components/venue-spotlight.tsx +236 -0
  170. package/src/components/venue-wall.stories.tsx +78 -0
  171. package/src/components/venue-wall.tsx +185 -0
  172. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  173. package/src/components/weekday-theme-banner.tsx +231 -0
  174. package/src/components/weekend-panel.test.tsx +43 -0
  175. package/src/components/weekend-panel.tsx +142 -108
  176. package/src/hooks/use-drag-swipe.test.tsx +66 -0
  177. package/src/hooks/use-drag-swipe.ts +105 -0
  178. package/src/hooks/use-media-query.ts +31 -0
  179. package/dist/chunk-GPH6L4DX.js +0 -55
@@ -0,0 +1,92 @@
1
+ import { cn } from './chunk-FEK5XGZW.js';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ var GHOST_ROWS = 13;
5
+ var GHOST_CENTRE = (GHOST_ROWS - 1) / 2;
6
+ var ALPHA_PEAK = 0.44;
7
+ var ALPHA_EDGE = 0;
8
+ var FALLOFF_EXPONENT = 3.4;
9
+ var GHOST_LINE_CHARS = 56;
10
+ function hashString(input) {
11
+ let hash = 5381;
12
+ for (let i = 0; i < input.length; i++) hash = hash * 33 ^ input.charCodeAt(i);
13
+ return Math.abs(hash);
14
+ }
15
+ function MenuField({
16
+ name,
17
+ gradient,
18
+ seedKey,
19
+ variant = "repeat",
20
+ className
21
+ }) {
22
+ if (variant === "display") {
23
+ const words = name.trim().split(/\s+/).filter(Boolean);
24
+ const longestWord = Math.max(1, ...words.map((word) => word.length));
25
+ const lineCount = Math.max(1, words.length);
26
+ const byWidth = (96 / (longestWord * 0.62)).toFixed(1);
27
+ const byHeight = (90 / (lineCount * 0.9)).toFixed(1);
28
+ const fontSize = `clamp(9cqw, min(19cqw, ${byWidth}cqw, ${byHeight}cqh), 19cqw)`;
29
+ return /* @__PURE__ */ jsx(
30
+ "div",
31
+ {
32
+ className: cn("relative isolate overflow-hidden", className),
33
+ style: { backgroundImage: gradient, containerType: "size" },
34
+ children: /* @__PURE__ */ jsx(
35
+ "span",
36
+ {
37
+ "aria-hidden": true,
38
+ className: "pointer-events-none absolute inset-0 flex select-none items-center justify-center p-[4cqw] text-center font-black uppercase leading-[0.9] tracking-[-0.03em] text-white/[0.26]",
39
+ style: { fontSize },
40
+ children: name
41
+ }
42
+ )
43
+ }
44
+ );
45
+ }
46
+ const unit = `${name} \xB7 `;
47
+ const repeats = Math.max(2, Math.ceil(GHOST_LINE_CHARS / Math.max(unit.length, 1)));
48
+ const ghostLine = unit.repeat(repeats);
49
+ const seed = hashString(seedKey != null ? seedKey : name);
50
+ const rows = Array.from({ length: GHOST_ROWS }, (_, row) => {
51
+ const distance = Math.abs(row - GHOST_CENTRE) / GHOST_CENTRE;
52
+ const alpha = ALPHA_EDGE + (ALPHA_PEAK - ALPHA_EDGE) * Math.pow(1 - distance, FALLOFF_EXPONENT);
53
+ return { row, alpha, offset: (seed >> row % 12) % 90 - 45 };
54
+ });
55
+ return /* @__PURE__ */ jsx(
56
+ "div",
57
+ {
58
+ className: cn("relative isolate overflow-hidden", className),
59
+ style: { backgroundImage: gradient },
60
+ children: /* @__PURE__ */ jsx(
61
+ "div",
62
+ {
63
+ "aria-hidden": true,
64
+ className: "pointer-events-none absolute left-1/2 top-1/2 flex h-[180%] w-[180%] -translate-x-1/2 -translate-y-1/2 -rotate-[11deg] select-none flex-col justify-center",
65
+ children: rows.map(({ row, alpha, offset }) => /* @__PURE__ */ jsx(
66
+ "span",
67
+ {
68
+ className: "block whitespace-nowrap text-center text-[15px] font-bold leading-[1.45] tracking-[-0.01em]",
69
+ style: {
70
+ color: `oklch(1 0 0 / ${alpha.toFixed(3)})`,
71
+ // Emboss, both edges scaled by this row's alpha: a dark edge below
72
+ // cuts the letterform into the field, a light edge above lifts its
73
+ // top. The dark edge is kept well under the fill — at parity it
74
+ // ringed every glyph and dragged the type back toward grey, which
75
+ // is the opposite of what the fill is doing.
76
+ textShadow: [
77
+ `0 1px 1px oklch(0 0 0 / ${(alpha * 0.4).toFixed(3)})`,
78
+ `0 -1px 0 oklch(1 0 0 / ${(alpha * 0.4).toFixed(3)})`
79
+ ].join(", "),
80
+ transform: `translateX(${offset}px)`
81
+ },
82
+ children: ghostLine
83
+ },
84
+ row
85
+ ))
86
+ }
87
+ )
88
+ }
89
+ );
90
+ }
91
+
92
+ export { MenuField };
@@ -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?: "default" | "secondary" | "destructive" | "outline" | "overlay" | "glass" | "success" | "warning" | "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> {
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { VariantProps } from 'class-variance-authority';
4
4
 
5
5
  declare const buttonVariants: (props?: ({
6
- variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "glass" | "overlay" | "brown" | "antagonist" | null | undefined;
6
+ variant?: "default" | "secondary" | "destructive" | "outline" | "overlay" | "glass" | "brown" | "link" | "ghost" | "antagonist" | null | undefined;
7
7
  size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
8
8
  fullWidth?: boolean | null | undefined;
9
9
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -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(
@@ -8,7 +8,7 @@ declare const Command: React.ForwardRefExoticComponent<Omit<{
8
8
  ref?: React.Ref<HTMLDivElement>;
9
9
  } & {
10
10
  asChild?: boolean;
11
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
11
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
12
12
  label?: string;
13
13
  shouldFilter?: boolean;
14
14
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -38,7 +38,7 @@ declare const CommandList: React.ForwardRefExoticComponent<Omit<{
38
38
  ref?: React.Ref<HTMLDivElement>;
39
39
  } & {
40
40
  asChild?: boolean;
41
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
41
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
42
42
  label?: string;
43
43
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
44
44
  declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
@@ -47,14 +47,14 @@ declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
47
47
  ref?: React.Ref<HTMLDivElement>;
48
48
  } & {
49
49
  asChild?: boolean;
50
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
51
51
  declare const CommandLoading: React.ForwardRefExoticComponent<Omit<{
52
52
  children?: React.ReactNode;
53
53
  } & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
54
54
  ref?: React.Ref<HTMLDivElement>;
55
55
  } & {
56
56
  asChild?: boolean;
57
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
57
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
58
58
  progress?: number;
59
59
  label?: string;
60
60
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
@@ -64,7 +64,7 @@ declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
64
64
  ref?: React.Ref<HTMLDivElement>;
65
65
  } & {
66
66
  asChild?: boolean;
67
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
67
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "heading" | "value"> & {
68
68
  heading?: React.ReactNode;
69
69
  value?: string;
70
70
  forceMount?: boolean;
@@ -73,7 +73,7 @@ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<R
73
73
  ref?: React.Ref<HTMLDivElement>;
74
74
  } & {
75
75
  asChild?: boolean;
76
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
76
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>> & {
77
77
  alwaysRender?: boolean;
78
78
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
79
79
  declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
@@ -82,7 +82,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
82
82
  ref?: React.Ref<HTMLDivElement>;
83
83
  } & {
84
84
  asChild?: boolean;
85
- }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
85
+ }, "key" | "asChild" | keyof React.HTMLAttributes<HTMLDivElement>>, "onSelect" | "disabled" | "value"> & {
86
86
  disabled?: boolean;
87
87
  onSelect?: (value: string) => void;
88
88
  value?: string;
@@ -0,0 +1,73 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuItemLabels } from './menu-item.js';
3
+ import { MenuConceptPreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
4
+ import 'react';
5
+ import './place-types.js';
6
+
7
+ /**
8
+ * ConceptCompare — one dish or drink across every kitchen that makes it, under
9
+ * whatever name each of them uses.
10
+ *
11
+ * The keystone module: a single menu can list papanași, only a catalogue can
12
+ * say that seventy kitchens make them under fifty different names between 8 and
13
+ * 44 lei. The alias column is what makes it honest — these are not the same row
14
+ * seventy times, they are seventy differently-named rows we matched, and
15
+ * showing each kitchen's own wording is the evidence for that.
16
+ *
17
+ * ## The row is place-led
18
+ *
19
+ * Each row leads with the VENUE PHOTOGRAPH, which is real and licensed. It
20
+ * replaced a price bar, and the trade is not close: the bar measured the one
21
+ * value already printed beside it in words, while the thing a reader is
22
+ * actually choosing between — the restaurant — had no image at all. Five
23
+ * restaurants that disagree about a name is the story; five bars of similar
24
+ * length is not.
25
+ *
26
+ * ## Two layouts, one component
27
+ *
28
+ * From `lg` the row is a table: rank, venue, alias, mentions, price. Below it,
29
+ * the rows become the production `<MenuItem>` — the alias as the name, the
30
+ * venue underneath — because a five-column table does not survive 390px and
31
+ * the item row already solves this exact shape everywhere else in the app.
32
+ */
33
+ /** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
34
+ type ConceptCompareLinkProps = MenuLinkProps;
35
+ type ConceptCompareFigure = {
36
+ /** Pre-formatted, e.g. "44". */
37
+ value: string;
38
+ label: string;
39
+ };
40
+ type ConceptCompareLabels = {
41
+ /** Small caps kicker, e.g. "DISH CONCEPT". */
42
+ kind: string;
43
+ /** Column heading over the aliases, e.g. "ON THEIR MENU AS". */
44
+ alias: string;
45
+ /** Under each price, e.g. "per portion". */
46
+ perPortion?: string;
47
+ /** Badge on the lowest-priced row, e.g. "cheapest shown". */
48
+ cheapest?: string;
49
+ };
50
+ type ConceptCompareProps = {
51
+ concept: MenuConceptPreview;
52
+ labels: ConceptCompareLabels;
53
+ /** Passed to the mobile `<MenuItem>` rows. */
54
+ itemLabels: MenuItemLabels;
55
+ formatPrice: (amount: number) => string;
56
+ formatCount?: (value: number) => string;
57
+ /** CSS gradient for the header swatch, when there is no photograph. */
58
+ gradient: string;
59
+ /** A real photograph of the concept, when the matched menu row carries one. */
60
+ imageUrl?: string;
61
+ figures?: ConceptCompareFigure[];
62
+ /** Destination per row, keyed by place slug. */
63
+ hrefs?: Record<string, string>;
64
+ /** The provenance note in the footer. */
65
+ footnote?: string;
66
+ /** Actions in the footer — pass rendered links or buttons. */
67
+ actions?: React.ReactNode;
68
+ linkComponent?: MenuLinkComponent;
69
+ className?: string;
70
+ };
71
+ declare function ConceptCompare({ concept, labels, itemLabels, formatPrice, formatCount, gradient, imageUrl, figures, hrefs, footnote, actions, linkComponent: LinkComponent, className, }: ConceptCompareProps): react_jsx_runtime.JSX.Element;
72
+
73
+ export { ConceptCompare, type ConceptCompareFigure, type ConceptCompareLabels, type ConceptCompareLinkProps, type ConceptCompareProps };
@@ -0,0 +1,162 @@
1
+ import { MenuItem } from '../chunk-TH6W2PWG.js';
2
+ import '../chunk-Q6QH52LJ.js';
3
+ import { MenuField } from '../chunk-YS6BHKZO.js';
4
+ import '../chunk-UKBNQO6Q.js';
5
+ import { cn } from '../chunk-FEK5XGZW.js';
6
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
7
+ import { Star, MessageSquare } from 'lucide-react';
8
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
+
10
+ function DefaultLink(_a) {
11
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
12
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
13
+ }
14
+ function ConceptCompare({
15
+ concept,
16
+ labels,
17
+ itemLabels,
18
+ formatPrice,
19
+ formatCount,
20
+ gradient,
21
+ imageUrl,
22
+ figures,
23
+ hrefs,
24
+ footnote,
25
+ actions,
26
+ linkComponent: LinkComponent = DefaultLink,
27
+ className
28
+ }) {
29
+ var _a;
30
+ const rows = (_a = concept.rows) != null ? _a : [];
31
+ const cheapest = rows.reduce(
32
+ (lowest, row) => lowest === void 0 || row.price < lowest ? row.price : lowest,
33
+ void 0
34
+ );
35
+ return /* @__PURE__ */ jsxs("div", { className: cn("overflow-hidden rounded-[18px] border border-border bg-card", className), children: [
36
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-4 lg:flex-row lg:items-center lg:gap-6 lg:p-5", children: [
37
+ imageUrl ? (
38
+ // eslint-disable-next-line @next/next/no-img-element
39
+ /* @__PURE__ */ jsx(
40
+ "img",
41
+ {
42
+ src: imageUrl,
43
+ alt: "",
44
+ className: "aspect-[16/9] w-full shrink-0 rounded-xl object-cover lg:aspect-auto lg:h-[88px] lg:w-[88px]"
45
+ }
46
+ )
47
+ ) : /* @__PURE__ */ jsx(
48
+ MenuField,
49
+ {
50
+ name: concept.title,
51
+ gradient,
52
+ seedKey: concept.slug,
53
+ className: "aspect-[16/9] w-full shrink-0 rounded-xl lg:aspect-auto lg:h-[88px] lg:w-[88px]"
54
+ }
55
+ ),
56
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
57
+ /* @__PURE__ */ jsx("span", { className: "text-[9.5px] font-bold uppercase tracking-[0.12em] text-muted-foreground", children: labels.kind }),
58
+ /* @__PURE__ */ jsx("h3", { className: "mt-1 font-serif text-2xl font-semibold tracking-[-0.02em] lg:text-[30px]", children: concept.title })
59
+ ] }),
60
+ figures && figures.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 gap-6 lg:gap-8", children: figures.map((figure) => /* @__PURE__ */ jsxs("div", { className: "text-left lg:text-right", children: [
61
+ /* @__PURE__ */ jsx("div", { className: "text-xl font-extrabold leading-none tracking-[-0.03em] tabular-nums lg:text-2xl", children: figure.value }),
62
+ /* @__PURE__ */ jsx("div", { className: "mt-1 text-[10.5px] text-muted-foreground", children: figure.label })
63
+ ] }, figure.label)) })
64
+ ] }),
65
+ /* @__PURE__ */ jsx("ul", { className: "border-t border-border lg:hidden", children: rows.map((row) => /* @__PURE__ */ jsx("li", { className: "border-b border-border last:border-b-0", children: /* @__PURE__ */ jsx(
66
+ MenuItem,
67
+ {
68
+ item: {
69
+ id: row.place_slug,
70
+ slug: row.place_slug,
71
+ // The kitchen's OWN wording leads the row — that IS the point
72
+ // of the module.
73
+ name: row.alias,
74
+ kind: concept.kind,
75
+ mentions: row.mentions,
76
+ sizes_and_prices: [{ size: 0, price: row.price }]
77
+ },
78
+ labels: itemLabels,
79
+ formatPrice,
80
+ formatCount,
81
+ venueName: row.place_name,
82
+ href: hrefs == null ? void 0 : hrefs[row.place_slug],
83
+ linkComponent: LinkComponent,
84
+ className: cn(row.price === cheapest && "bg-accent-subtle/40")
85
+ }
86
+ ) }, row.place_slug)) }),
87
+ /* @__PURE__ */ jsx("ol", { className: "hidden border-t border-border lg:block", children: rows.map((row, index) => {
88
+ const isCheapest = row.price === cheapest;
89
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ /* @__PURE__ */ jsx("span", { className: "w-5 shrink-0 text-center text-xs font-bold tabular-nums text-muted-foreground", children: index + 1 }),
91
+ /* @__PURE__ */ jsxs("span", { className: "relative h-10 w-10 shrink-0 overflow-hidden rounded-lg bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)]", children: [
92
+ row.place_thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
93
+ /* @__PURE__ */ jsx(
94
+ "img",
95
+ {
96
+ src: row.place_thumbnail_url,
97
+ alt: "",
98
+ loading: "lazy",
99
+ className: "h-full w-full object-cover"
100
+ }
101
+ ),
102
+ row.open_now && /* @__PURE__ */ jsx(
103
+ "i",
104
+ {
105
+ "aria-hidden": true,
106
+ className: "absolute right-0.5 top-0.5 h-[6px] w-[6px] rounded-full bg-[var(--free,currentColor)] ring-2 ring-white/80"
107
+ }
108
+ )
109
+ ] }),
110
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 w-[200px] shrink-0", children: [
111
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-bold tracking-[-0.01em]", children: row.place_name }),
112
+ /* @__PURE__ */ jsxs("span", { className: "mt-px flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
113
+ row.rating != null && /* @__PURE__ */ jsxs(Fragment, { children: [
114
+ /* @__PURE__ */ jsx(Star, { className: "h-2.5 w-2.5", "aria-hidden": true }),
115
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: row.rating })
116
+ ] }),
117
+ row.place_area && /* @__PURE__ */ jsx("span", { className: "truncate", children: row.place_area })
118
+ ] })
119
+ ] }),
120
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
121
+ /* @__PURE__ */ jsx("span", { className: "block text-[9.5px] font-bold uppercase tracking-[0.1em] text-muted-foreground", children: labels.alias }),
122
+ /* @__PURE__ */ jsx("span", { className: "block truncate font-serif text-[13px] italic", children: row.alias })
123
+ ] }),
124
+ row.mentions != null && row.mentions > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-[11px] tabular-nums text-muted-foreground", children: [
125
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-[11px] w-[11px]", "aria-hidden": true }),
126
+ row.mentions
127
+ ] }),
128
+ /* @__PURE__ */ jsxs("span", { className: "w-[92px] shrink-0 text-right", children: [
129
+ /* @__PURE__ */ jsx(
130
+ "b",
131
+ {
132
+ className: cn(
133
+ "block text-sm font-extrabold tabular-nums",
134
+ isCheapest && "text-[var(--free,currentColor)]"
135
+ ),
136
+ children: formatPrice(row.price)
137
+ }
138
+ ),
139
+ /* @__PURE__ */ jsx("span", { className: "block text-[10px] text-muted-foreground", children: isCheapest && labels.cheapest ? labels.cheapest : labels.perPortion })
140
+ ] })
141
+ ] });
142
+ const rowClasses = "flex items-center gap-4 px-5 py-3";
143
+ return /* @__PURE__ */ jsx("li", { className: "border-b border-border last:border-b-0", children: (hrefs == null ? void 0 : hrefs[row.place_slug]) ? /* @__PURE__ */ jsx(
144
+ LinkComponent,
145
+ {
146
+ href: hrefs[row.place_slug],
147
+ className: cn(
148
+ rowClasses,
149
+ "transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
150
+ ),
151
+ children: content
152
+ }
153
+ ) : /* @__PURE__ */ jsx("div", { className: rowClasses, children: content }) }, row.place_slug);
154
+ }) }),
155
+ (footnote || actions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 p-4", children: [
156
+ footnote && /* @__PURE__ */ jsx("span", { className: "text-[11.5px] text-muted-foreground", children: footnote }),
157
+ actions && /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })
158
+ ] })
159
+ ] });
160
+ }
161
+
162
+ export { ConceptCompare };
@@ -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;