@bearmenu/ui 0.8.13 → 0.8.14

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 (285) hide show
  1. package/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
  2. package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
  3. package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
  4. package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
  5. package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
  6. package/dist/chunk-DJD3EMPK.js +126 -0
  7. package/dist/chunk-EZG2NELX.js +5 -0
  8. package/dist/{chunk-VAXOQS7W.js → chunk-F76NCVVR.js} +1 -1
  9. package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
  10. package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
  11. package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
  12. package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
  13. package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
  14. package/dist/chunk-I5V446BX.js +27 -0
  15. package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
  16. package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
  17. package/dist/chunk-LQWGZIYA.js +5 -0
  18. package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
  19. package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
  20. package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
  21. package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
  22. package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
  23. package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
  24. package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
  25. package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
  26. package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
  27. package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
  28. package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
  29. package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
  30. package/dist/{chunk-3NMG3KNA.js → chunk-SXQZ7DLH.js} +3 -3
  31. package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
  32. package/dist/{chunk-FKNBZF5T.js → chunk-WYTJR2YU.js} +1 -1
  33. package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
  34. package/dist/chunk-XCB3F6SF.js +16 -0
  35. package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
  36. package/dist/{chunk-P7XTW2CG.js → chunk-ZLPWEHZO.js} +1 -1
  37. package/dist/components/accordion.js +1 -1
  38. package/dist/components/alert-dialog.js +2 -2
  39. package/dist/components/alert.js +2 -2
  40. package/dist/components/aspect-ratio.js +2 -2
  41. package/dist/components/avatar.js +1 -1
  42. package/dist/components/badge.js +2 -2
  43. package/dist/components/breadcrumb.js +1 -1
  44. package/dist/components/button-group.js +1 -1
  45. package/dist/components/button.js +2 -2
  46. package/dist/components/calendar.js +2 -2
  47. package/dist/components/card.js +3 -3
  48. package/dist/components/carousel.js +2 -2
  49. package/dist/components/category-deck.js +2 -2
  50. package/dist/components/checkbox.js +2 -2
  51. package/dist/components/circular-progress.js +1 -1
  52. package/dist/components/collapsible.js +1 -1
  53. package/dist/components/combobox.js +5 -5
  54. package/dist/components/command.js +3 -3
  55. package/dist/components/concept-card.js +2 -2
  56. package/dist/components/concept-compare-rail.js +3 -3
  57. package/dist/components/concept-compare.d.ts +2 -1
  58. package/dist/components/concept-compare.js +5 -5
  59. package/dist/components/concept-convergence.js +4 -4
  60. package/dist/components/convergence.js +3 -3
  61. package/dist/components/coverflow.js +1 -1
  62. package/dist/components/currency-input.js +2 -2
  63. package/dist/components/date-input.js +2 -2
  64. package/dist/components/day-rail-chip.js +2 -2
  65. package/dist/components/description-list.js +1 -1
  66. package/dist/components/dialog.js +2 -2
  67. package/dist/components/doorway-tiles.d.ts +57 -0
  68. package/dist/components/doorway-tiles.js +86 -0
  69. package/dist/components/drawer.js +1 -1
  70. package/dist/components/drift-rail.js +1 -1
  71. package/dist/components/dropdown-menu.js +1 -1
  72. package/dist/components/empty-state.js +3 -3
  73. package/dist/components/event-card.js +4 -4
  74. package/dist/components/event-compact-row.js +4 -4
  75. package/dist/components/event-day-header.js +2 -2
  76. package/dist/components/event-day-separator.js +3 -3
  77. package/dist/components/event-detail-panel.js +4 -4
  78. package/dist/components/event-hub-hero.js +2 -2
  79. package/dist/components/event-index-row.js +4 -4
  80. package/dist/components/event-list-row-wide.js +5 -5
  81. package/dist/components/event-list-row.js +5 -5
  82. package/dist/components/event-poster-fallback.js +1 -1
  83. package/dist/components/event-types.js +1 -1
  84. package/dist/components/events-empty-state.js +1 -1
  85. package/dist/components/explainer-steps.js +1 -1
  86. package/dist/components/facet-group.js +2 -2
  87. package/dist/components/featured-rail-item.js +2 -2
  88. package/dist/components/field.js +2 -2
  89. package/dist/components/filter-category-row.js +3 -3
  90. package/dist/components/form.js +2 -2
  91. package/dist/components/glass-ground.js +2 -2
  92. package/dist/components/grade-atom.d.ts +3 -0
  93. package/dist/components/grade-atom.js +4 -0
  94. package/dist/components/haptic-button.js +2 -2
  95. package/dist/components/hour-band.js +4 -4
  96. package/dist/components/hour-group.js +4 -4
  97. package/dist/components/hub-surface.js +1 -1
  98. package/dist/components/hue-chip.js +1 -1
  99. package/dist/components/icon.js +1 -1
  100. package/dist/components/input-group.js +1 -1
  101. package/dist/components/input-otp.js +1 -1
  102. package/dist/components/input.js +2 -2
  103. package/dist/components/item.js +1 -1
  104. package/dist/components/kbd.js +1 -1
  105. package/dist/components/kitchen-dossier.d.ts +2 -1
  106. package/dist/components/kitchen-dossier.js +3 -3
  107. package/dist/components/label.js +2 -2
  108. package/dist/components/lazy-markdown.js +1 -1
  109. package/dist/components/link-button.js +2 -2
  110. package/dist/components/markdown-renderer.js +1 -1
  111. package/dist/components/menu-dead-end.d.ts +2 -1
  112. package/dist/components/menu-dead-end.js +1 -1
  113. package/dist/components/menu-family-block.d.ts +2 -1
  114. package/dist/components/menu-family-block.js +14 -8
  115. package/dist/components/menu-family-card.d.ts +2 -1
  116. package/dist/components/menu-family-card.js +1 -1
  117. package/dist/components/menu-field.js +1 -1
  118. package/dist/components/menu-filter-chips.d.ts +2 -1
  119. package/dist/components/menu-filter-chips.js +3 -3
  120. package/dist/components/menu-group-rows.d.ts +2 -1
  121. package/dist/components/menu-group-rows.js +2 -2
  122. package/dist/components/menu-hub-hero.js +1 -1
  123. package/dist/components/menu-index-header.d.ts +2 -1
  124. package/dist/components/menu-index-header.js +2 -2
  125. package/dist/components/menu-index-list.d.ts +2 -1
  126. package/dist/components/menu-index-list.js +4 -4
  127. package/dist/components/menu-intent-hero.d.ts +3 -2
  128. package/dist/components/menu-intent-hero.js +1 -1
  129. package/dist/components/menu-item.d.ts +2 -1
  130. package/dist/components/menu-item.js +4 -4
  131. package/dist/components/menu-kitchen-list.d.ts +2 -1
  132. package/dist/components/menu-kitchen-list.js +4 -4
  133. package/dist/components/menu-mocks.d.ts +16 -5
  134. package/dist/components/menu-mocks.js +23 -2
  135. package/dist/components/menu-types.d.ts +21 -34
  136. package/dist/components/motion-icon-actions.d.ts +27 -0
  137. package/dist/components/motion-icon-actions.js +336 -0
  138. package/dist/components/motion-icon-controls.js +3 -3
  139. package/dist/components/motion-icon-set.d.ts +13 -1
  140. package/dist/components/motion-icon-set.js +197 -6
  141. package/dist/components/motion-icon.d.ts +15 -1
  142. package/dist/components/motion-icon.js +2 -2
  143. package/dist/components/multi-select-combobox.js +7 -7
  144. package/dist/components/multi-select-menu.js +2 -2
  145. package/dist/components/native-select.js +1 -1
  146. package/dist/components/open-now-list.d.ts +2 -1
  147. package/dist/components/open-now-list.js +2 -2
  148. package/dist/components/pagination.js +2 -2
  149. package/dist/components/phone-frame.js +1 -1
  150. package/dist/components/photo-ground.js +1 -1
  151. package/dist/components/place-about-tab.d.ts +2 -1
  152. package/dist/components/place-about-tab.js +6 -6
  153. package/dist/components/place-card.d.ts +94 -12
  154. package/dist/components/place-card.js +203 -128
  155. package/dist/components/place-details-mobile.d.ts +2 -1
  156. package/dist/components/place-details-mobile.js +7 -7
  157. package/dist/components/place-schedule-week.d.ts +2 -1
  158. package/dist/components/place-schedule-week.js +3 -3
  159. package/dist/components/place-types.d.ts +3 -265
  160. package/dist/components/popover.js +2 -2
  161. package/dist/components/poster-stack.js +1 -1
  162. package/dist/components/poster-tape.js +1 -1
  163. package/dist/components/poster-wall.d.ts +22 -6
  164. package/dist/components/poster-wall.js +197 -12
  165. package/dist/components/progress.js +1 -1
  166. package/dist/components/qa-block.d.ts +2 -1
  167. package/dist/components/qa-block.js +1 -1
  168. package/dist/components/quiet-stretch.js +1 -1
  169. package/dist/components/radio-group.js +1 -1
  170. package/dist/components/rating.js +1 -1
  171. package/dist/components/scroll-area.js +1 -1
  172. package/dist/components/scroll-fade.js +2 -2
  173. package/dist/components/search-bar.js +1 -1
  174. package/dist/components/searchable-select.js +5 -5
  175. package/dist/components/section-header.js +2 -2
  176. package/dist/components/segmented-nav.d.ts +24 -4
  177. package/dist/components/segmented-nav.js +11 -2
  178. package/dist/components/select.js +1 -1
  179. package/dist/components/selection-bar.js +1 -1
  180. package/dist/components/separator.js +2 -2
  181. package/dist/components/sheet.js +1 -1
  182. package/dist/components/signature-chip.js +3 -3
  183. package/dist/components/skeleton-card.js +3 -3
  184. package/dist/components/skeleton.js +2 -2
  185. package/dist/components/slider.js +1 -1
  186. package/dist/components/sliding-panels.js +2 -2
  187. package/dist/components/snap-rail.js +2 -2
  188. package/dist/components/sonner.js +1 -1
  189. package/dist/components/sort-option-list.js +1 -1
  190. package/dist/components/spinner.js +1 -1
  191. package/dist/components/stat-banner.js +1 -1
  192. package/dist/components/stepper.js +1 -1
  193. package/dist/components/sticky-container.js +1 -1
  194. package/dist/components/story.js +1 -1
  195. package/dist/components/switch.js +1 -1
  196. package/dist/components/table.js +1 -1
  197. package/dist/components/tabs.js +2 -2
  198. package/dist/components/tag-group.js +2 -2
  199. package/dist/components/tag.js +2 -2
  200. package/dist/components/text.js +2 -2
  201. package/dist/components/textarea.js +1 -1
  202. package/dist/components/timeline-band.js +4 -4
  203. package/dist/components/timeline-playhead.js +4 -4
  204. package/dist/components/toggle-group.js +2 -2
  205. package/dist/components/toggle.js +2 -2
  206. package/dist/components/tooltip.js +1 -1
  207. package/dist/components/top-comparison.d.ts +2 -1
  208. package/dist/components/top-comparison.js +1 -1
  209. package/dist/components/top-cross-link.d.ts +2 -1
  210. package/dist/components/top-cross-link.js +1 -1
  211. package/dist/components/top-ranking-entry.d.ts +2 -1
  212. package/dist/components/top-ranking-entry.js +6 -6
  213. package/dist/components/top-ranking-hero.d.ts +2 -1
  214. package/dist/components/top-ranking-hero.js +1 -1
  215. package/dist/components/top-ranking-legend.js +1 -1
  216. package/dist/components/top-types.d.ts +3 -1
  217. package/dist/components/use-mobile.js +1 -1
  218. package/dist/components/venue-events-block.js +5 -5
  219. package/dist/components/venue-grade-dimensions.d.ts +2 -1
  220. package/dist/components/venue-grade-dimensions.js +1 -1
  221. package/dist/components/venue-quotes.d.ts +2 -1
  222. package/dist/components/venue-quotes.js +1 -1
  223. package/dist/components/venue-wall.d.ts +2 -1
  224. package/dist/components/venue-wall.js +1 -1
  225. package/dist/components/weekday-theme-banner.js +1 -1
  226. package/dist/components/weekend-panel.js +4 -4
  227. package/dist/hooks/use-drag-swipe.js +1 -1
  228. package/dist/hooks/use-haptic.js +1 -1
  229. package/dist/hooks/use-media-query.js +1 -1
  230. package/dist/hooks/use-mobile.js +1 -1
  231. package/dist/hooks/use-mounted-theme.js +1 -1
  232. package/dist/hooks/use-sticky.js +1 -1
  233. package/dist/lib/card-hover.js +2 -18
  234. package/dist/lib/grade.d.ts +54 -0
  235. package/dist/lib/grade.js +2 -0
  236. package/dist/lib/icon-depth.d.ts +35 -0
  237. package/dist/lib/icon-depth.js +2 -0
  238. package/dist/lib/icon-registry.js +1 -1
  239. package/dist/lib/motion.js +2 -2
  240. package/dist/lib/overlay-pill.d.ts +30 -0
  241. package/dist/lib/overlay-pill.js +2 -0
  242. package/dist/lib/utils.js +1 -1
  243. package/dist/place-types-BHdeMMiG.d.ts +317 -0
  244. package/package.json +1 -1
  245. package/src/components/colour-dictionary.stories.tsx +268 -82
  246. package/src/components/doorway-tiles.stories.tsx +83 -0
  247. package/src/components/doorway-tiles.test.tsx +51 -0
  248. package/src/components/doorway-tiles.tsx +225 -0
  249. package/src/components/grade-atom.stories.tsx +81 -0
  250. package/src/components/grade-atom.tsx +256 -0
  251. package/src/components/hue-chip.stories.tsx +2 -2
  252. package/src/components/menu-family-block.stories.tsx +29 -0
  253. package/src/components/menu-family-block.test.tsx +51 -0
  254. package/src/components/menu-family-block.tsx +20 -4
  255. package/src/components/menu-intent-hero.tsx +1 -1
  256. package/src/components/menu-mocks.ts +39 -3
  257. package/src/components/menu-types.ts +17 -33
  258. package/src/components/motion-icon-actions.test.tsx +115 -0
  259. package/src/components/motion-icon-actions.tsx +580 -0
  260. package/src/components/motion-icon-controls.test.tsx +1 -0
  261. package/src/components/motion-icon-controls.tsx +18 -13
  262. package/src/components/motion-icon-set.stories.tsx +62 -3
  263. package/src/components/motion-icon-set.tsx +349 -2
  264. package/src/components/motion-icon.test.tsx +180 -1
  265. package/src/components/motion-icon.tsx +97 -8
  266. package/src/components/place-card.stories.tsx +66 -76
  267. package/src/components/place-card.test.tsx +188 -100
  268. package/src/components/place-card.tsx +356 -148
  269. package/src/components/place-types.ts +17 -4
  270. package/src/components/poster-wall.stories.tsx +84 -0
  271. package/src/components/poster-wall.test.tsx +94 -3
  272. package/src/components/poster-wall.tsx +257 -9
  273. package/src/components/segmented-nav.stories.tsx +72 -1
  274. package/src/components/segmented-nav.test.tsx +96 -0
  275. package/src/components/segmented-nav.tsx +36 -4
  276. package/src/globals.css +26 -0
  277. package/src/lib/grade.test.ts +82 -0
  278. package/src/lib/grade.ts +76 -0
  279. package/src/lib/icon-depth.ts +35 -0
  280. package/src/lib/overlay-pill.ts +29 -0
  281. package/dist/components/menu-family-tiles.d.ts +0 -19
  282. package/dist/components/menu-family-tiles.js +0 -57
  283. package/src/components/menu-family-tiles.stories.tsx +0 -54
  284. package/src/components/menu-family-tiles.test.tsx +0 -30
  285. package/src/components/menu-family-tiles.tsx +0 -132
@@ -0,0 +1,580 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The action vocabulary, character-animated — the glyphs `lib/icon-registry`
5
+ * names for generic actions (back, see-all, close, clear, call, directions…)
6
+ * and the handful of app controls beside them (the Bruno composer's send/stop,
7
+ * the map's locate / expand, sign in / out, settings).
8
+ *
9
+ * Same doctrine as `motion-icon-set.tsx`: geometry vendored from lucide on the
10
+ * 24-unit grid; each glyph performs a behaviour its object performs. Almost
11
+ * everything here is a MOMENTARY action, so almost everything RETURNS (active
12
+ * = rest); the exceptions say so. Most of these render at 16px (`Button`'s
13
+ * `[&_svg]:size-4`), where one unit is 0.67px, so primary travel stays at 8+
14
+ * units and every apex lands before ~250ms — the layer a close button
15
+ * dismisses usually leaves under the finger.
16
+ */
17
+
18
+ import { motion, type Transition, type Variants } from "framer-motion";
19
+
20
+ import {
21
+ MotionIcon,
22
+ beats,
23
+ pivot,
24
+ poses,
25
+ returnTransition,
26
+ useSvgId,
27
+ type MotionIconProps,
28
+ } from "./motion-icon";
29
+
30
+ export type MotionDirection = "right" | "left";
31
+
32
+ /** A part that RETURNS: `rest` and `active` are both the gesture's first keyframe. */
33
+ function returning(play: Record<string, number[]>, transition: Transition): Variants {
34
+ const held: Record<string, number> = {};
35
+ for (const key of Object.keys(play)) held[key] = play[key][0];
36
+ return poses(held, held, play, transition);
37
+ }
38
+
39
+ const flip = (values: number[], direction: MotionDirection) =>
40
+ direction === "right" ? values : values.map((v) => -v);
41
+
42
+ // ─── Arrow — the head fires ahead, the shaft chases (detach and return) ───────
43
+
44
+ const ARROW = beats([0, 0.14, 0.42, 0.58, 0.82, 1], 0.6);
45
+
46
+ /**
47
+ * Nocks back, the head fires off the leading edge of the box (apex at x 28),
48
+ * the shaft stretches from its tail to catch up — the arrow is 1.6× its rest
49
+ * length at that beat — and both snap home. `arrow-left` is lucide's exact
50
+ * mirror, so one glyph with a sign flip; a back button plays under a route
51
+ * transition and does not deserve a second character.
52
+ */
53
+ function arrowParts(direction: MotionDirection) {
54
+ const tailX = direction === "right" ? 5 : 19;
55
+ return {
56
+ head: returning({ x: flip([0, -2.5, 9, 8, -1, 0], direction) }, ARROW),
57
+ shaft: returning({ scaleX: [1, 0.82, 1, 1.45, 0.93, 1] }, ARROW),
58
+ tail: pivot(tailX, 12),
59
+ headD: direction === "right" ? "m12 5 7 7-7 7" : "m12 19-7-7 7-7",
60
+ shaftD: direction === "right" ? "M5 12h14" : "M19 12H5",
61
+ };
62
+ }
63
+
64
+ const ARROW_RIGHT = arrowParts("right");
65
+ const ARROW_LEFT = arrowParts("left");
66
+
67
+ export function MotionArrow({
68
+ direction = "right",
69
+ ...props
70
+ }: MotionIconProps & { direction?: MotionDirection }) {
71
+ const parts = direction === "right" ? ARROW_RIGHT : ARROW_LEFT;
72
+ return (
73
+ <MotionIcon {...props}>
74
+ <motion.path variants={parts.shaft} style={parts.tail} d={parts.shaftD} />
75
+ <motion.path variants={parts.head} d={parts.headD} />
76
+ </MotionIcon>
77
+ );
78
+ }
79
+
80
+ // ─── Caret — coils, darts with its arms folded, springs open (articulate) ─────
81
+
82
+ const CARET = beats([0, 0.14, 0.4, 0.56, 0.8, 1], 0.56);
83
+
84
+ /**
85
+ * The directional `>` (row drill-in, carousel paging) — NOT `MotionChevron`,
86
+ * which is the 180° disclosure flip that holds. A `>` that flipped to `<` on
87
+ * a row would say "collapse". The single lucide path is cut at the apex into
88
+ * two arms so they can fold: at the dart the chevron is 8 wide × 5.8 tall
89
+ * instead of 6 × 12.
90
+ */
91
+ function caretParts(direction: MotionDirection) {
92
+ const apexX = direction === "right" ? 15 : 9;
93
+ const armX = direction === "right" ? 9 : 15;
94
+ const upperSwing = flip([0, 8, -25, -20, 6, 0], direction);
95
+ return {
96
+ body: returning({ x: flip([0, -2, 7, 6.5, -1, 0], direction) }, CARET),
97
+ upper: returning({ rotate: upperSwing }, CARET),
98
+ lower: returning({ rotate: upperSwing.map((v) => -v) }, CARET),
99
+ apex: pivot(apexX, 12),
100
+ upperD: `M${apexX} 12 ${armX} 6`,
101
+ lowerD: `M${apexX} 12 ${armX} 18`,
102
+ };
103
+ }
104
+
105
+ const CARET_RIGHT = caretParts("right");
106
+ const CARET_LEFT = caretParts("left");
107
+
108
+ export function MotionCaret({
109
+ direction = "right",
110
+ ...props
111
+ }: MotionIconProps & { direction?: MotionDirection }) {
112
+ const parts = direction === "right" ? CARET_RIGHT : CARET_LEFT;
113
+ return (
114
+ <MotionIcon {...props}>
115
+ <motion.g variants={parts.body}>
116
+ <motion.path variants={parts.upper} style={parts.apex} d={parts.upperD} />
117
+ <motion.path variants={parts.lower} style={parts.apex} d={parts.lowerD} />
118
+ </motion.g>
119
+ </MotionIcon>
120
+ );
121
+ }
122
+
123
+ // ─── X — the blades shut like scissors and reopen (articulate) ───────────────
124
+
125
+ const X_TIMES = beats([0, 0.15, 0.42, 0.56, 0.8, 1], 0.56);
126
+
127
+ /**
128
+ * Close a layer. Flares a touch, slams shut into a single vertical bar with a
129
+ * 3° cross-over, springs back open. The clack lands at 235ms — before a
130
+ * sheet's exit has taken the button away. Both blades pivot on the crossing.
131
+ */
132
+ const bladeA = returning({ rotate: [0, 7, -48, -45, 5, 0] }, X_TIMES);
133
+ const bladeB = returning({ rotate: [0, -7, 48, 45, -5, 0] }, X_TIMES);
134
+
135
+ export function MotionX(props: MotionIconProps) {
136
+ const crossing = pivot(12, 12);
137
+ return (
138
+ <MotionIcon {...props}>
139
+ <motion.path variants={bladeA} style={crossing} d="M18 6 6 18" />
140
+ <motion.path variants={bladeB} style={crossing} d="m6 6 12 12" />
141
+ </MotionIcon>
142
+ );
143
+ }
144
+
145
+ // ─── Circle-X — a clear key: pressed, it ejects its contents (deform + eject) ─
146
+
147
+ const CLEAR = beats([0, 0.18, 0.4, 0.54, 0.72, 1], 0.6);
148
+
149
+ /**
150
+ * Clear a value in place — the registry separates it from close, and so does
151
+ * the gesture. The ring flattens on its floor under the press, rebounds, and
152
+ * on the rebound the little X is thrown out of the top (2 units clear of the
153
+ * box); a fresh X drops back in and the ring takes the impact.
154
+ */
155
+ const clearRing = returning(
156
+ { scaleY: [1, 0.66, 1.06, 1, 0.9, 1], scaleX: [1, 1.1, 1, 1, 1, 1] },
157
+ CLEAR
158
+ );
159
+ const clearX = returning(
160
+ {
161
+ y: [0, 0, -11, -11, 1, 0],
162
+ scaleY: [1, 0.66, 1, 1, 1, 1],
163
+ opacity: [1, 1, 1, 0, 1, 1],
164
+ },
165
+ CLEAR
166
+ );
167
+
168
+ export function MotionCircleX(props: MotionIconProps) {
169
+ return (
170
+ <MotionIcon {...props}>
171
+ <motion.circle variants={clearRing} style={pivot(12, 22)} cx="12" cy="12" r="10" />
172
+ <motion.g variants={clearX} style={pivot(12, 12)}>
173
+ <path d="m15 9-6 6" />
174
+ <path d="m9 9 6 6" />
175
+ </motion.g>
176
+ </MotionIcon>
177
+ );
178
+ }
179
+
180
+ // ─── External link — the arrow leaves, the next one loads (detach, no return) ─
181
+
182
+ const EXTERNAL = beats([0, 0.16, 0.4, 0.5, 0.62, 1], 0.6);
183
+
184
+ /**
185
+ * The arrow dips into the box, launches up-right clear of the frame (apex at
186
+ * (30, −6)), the box recoils under it; the arrow vanishes off the corner and
187
+ * a fresh one slides up into the notch from inside the box. Rest = active:
188
+ * the destination opens elsewhere.
189
+ */
190
+ const leavingArrow = returning(
191
+ {
192
+ x: [0, -2, 9, 9, -5, 0],
193
+ y: [0, 2, -9, -9, 5, 0],
194
+ opacity: [1, 1, 1, 0, 0, 1],
195
+ },
196
+ EXTERNAL
197
+ );
198
+ const recoilingBox = returning({ scaleY: [1, 1, 0.9, 1.03, 1, 1] }, EXTERNAL);
199
+
200
+ export function MotionExternalLink(props: MotionIconProps) {
201
+ return (
202
+ <MotionIcon {...props}>
203
+ <motion.path
204
+ variants={recoilingBox}
205
+ style={pivot(10.5, 21)}
206
+ d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
207
+ />
208
+ <motion.g variants={leavingArrow}>
209
+ <path d="M15 3h6v6" />
210
+ <path d="M10 14 21 3" />
211
+ </motion.g>
212
+ </MotionIcon>
213
+ );
214
+ }
215
+
216
+ // ─── Send — the arrow fires out, the stop block drops in (fall and land) ──────
217
+
218
+ const SEND_TIMES = [0, 0.14, 0.36, 0.5, 0.72, 1];
219
+ const SEND = beats(SEND_TIMES, 0.62);
220
+
221
+ /**
222
+ * ONE glyph for the composer's send / stop slot, `active` = generating. Two
223
+ * swapped glyphs are strictly worse: the send arrow unmounts the instant the
224
+ * stream starts (~50–100ms after pointer down, cutting the gesture at its
225
+ * anticipation), and stop → send has no transition at all. The button is a
226
+ * SLOT: the arrow is fired out of it, the stop block drops into it, filling
227
+ * on the descent and squashing on impact — the pin's rule.
228
+ *
229
+ * HOLDS. Leaving (stop tapped, or the stream ends) is the return spring: the
230
+ * block sinks and fades, the arrow drops back in from above — gravity agrees
231
+ * in both directions. Callers bump `replayToken` on the SEND pointer down
232
+ * only; the stop tap is the `active → false` flip.
233
+ *
234
+ * The block is a 14-unit rect, not lucide's 18: at `size-4` that matches the
235
+ * mass of the `h-3.5` filled square the composer draws today.
236
+ */
237
+ const sendArrow: Variants = {
238
+ rest: { y: 0, opacity: 1, transition: returnTransition },
239
+ active: { y: -18, opacity: 0, transition: returnTransition },
240
+ play: {
241
+ y: [0, 2.5, -18, -18, -18, -18],
242
+ opacity: [1, 1, 1, 0, 0, 0],
243
+ transition: SEND,
244
+ },
245
+ };
246
+ const sendShaft: Variants = {
247
+ rest: { scaleY: 1, transition: returnTransition },
248
+ active: { scaleY: 1, transition: returnTransition },
249
+ play: { scaleY: [1, 0.85, 1.25, 1, 1, 1], transition: SEND },
250
+ };
251
+ const stopBlock: Variants = {
252
+ rest: { y: 6, opacity: 0, fillOpacity: 0, scaleY: 1, transition: returnTransition },
253
+ active: { y: 0, opacity: 1, fillOpacity: 1, scaleY: 1, transition: returnTransition },
254
+ play: {
255
+ y: [-12, -12, -12, -12, 1, 0],
256
+ opacity: [0, 0, 0, 1, 1, 1],
257
+ fillOpacity: [0, 0, 0, 0, 1, 1],
258
+ scaleY: [1, 1, 1, 1, 0.82, 1],
259
+ transition: SEND,
260
+ },
261
+ };
262
+
263
+ export function MotionSend(props: MotionIconProps) {
264
+ const floor = pivot(12, 19);
265
+ return (
266
+ <MotionIcon {...props}>
267
+ <motion.g variants={sendArrow}>
268
+ <path d="m5 12 7-7 7 7" />
269
+ <motion.path variants={sendShaft} style={floor} d="M12 19V5" />
270
+ </motion.g>
271
+ <motion.rect
272
+ variants={stopBlock}
273
+ style={floor}
274
+ fill="currentColor"
275
+ fillOpacity={0}
276
+ opacity={0}
277
+ x="5"
278
+ y="5"
279
+ width="14"
280
+ height="14"
281
+ rx="2"
282
+ />
283
+ </MotionIcon>
284
+ );
285
+ }
286
+
287
+ // ─── Navigation — the course arrow hunts for its bearing (cycle content) ──────
288
+
289
+ /**
290
+ * One closed polygon with no separable part, so the only move is rigid — and
291
+ * the hunt is the one rigid move that is a behaviour rather than a tween: the
292
+ * needle swings the wrong way, whips back through north, overshoots, settles.
293
+ * Pivot at (12, 12) keeps the tail notch (11, 13) nearly still, which is what
294
+ * makes it a needle rather than a spinner.
295
+ */
296
+ const needle = returning(
297
+ { rotate: [0, 12, -38, 14, -5, 0] },
298
+ beats([0, 0.16, 0.44, 0.64, 0.84, 1], 0.62)
299
+ );
300
+
301
+ export function MotionNavigation(props: MotionIconProps) {
302
+ return (
303
+ <MotionIcon {...props}>
304
+ <motion.polygon variants={needle} style={pivot(12, 12)} points="3 11 22 2 13 21 11 13 3 11" />
305
+ </MotionIcon>
306
+ );
307
+ }
308
+
309
+ // ─── Locate — the reticle hunts off-centre and locks onto the dot ─────────────
310
+
311
+ const LOCATE = beats([0, 0.16, 0.44, 0.6, 0.8, 1], 0.62);
312
+
313
+ /**
314
+ * On a real map the crosshair is fixed and the map moves — but at 16px the
315
+ * 14-unit ring displaced 7 units is far more legible than a 6-unit dot
316
+ * displaced the same, and the pictures are identical. So the reticle skids
317
+ * off-centre (concentric → eccentric is the silhouette change), swings back
318
+ * and clamps: the ring shrinks, the ticks flick inward, the dot flashes as
319
+ * the lock lands.
320
+ */
321
+ const reticle = returning(
322
+ { x: [0, 2, -7, 0.8, -0.3, 0], y: [0, -1.5, 5, -0.6, 0.2, 0] },
323
+ LOCATE
324
+ );
325
+ const ring = returning({ scale: [1, 1, 1, 0.9, 1.02, 1] }, LOCATE);
326
+ const tick = (axis: "x" | "y", inward: number) =>
327
+ axis === "x"
328
+ ? returning({ x: [0, 0, 0, inward, 0, 0] }, LOCATE)
329
+ : returning({ y: [0, 0, 0, inward, 0, 0] }, LOCATE);
330
+ const tickLeft = tick("x", 2.5);
331
+ const tickRight = tick("x", -2.5);
332
+ const tickTop = tick("y", 2.5);
333
+ const tickBottom = tick("y", -2.5);
334
+ const lockDot = returning(
335
+ { scale: [1, 1, 1, 1.35, 1, 1], fillOpacity: [0, 0, 0, 1, 1, 0] },
336
+ LOCATE
337
+ );
338
+
339
+ export function MotionLocate(props: MotionIconProps) {
340
+ const centre = pivot(12, 12);
341
+ return (
342
+ <MotionIcon {...props}>
343
+ <motion.g variants={reticle}>
344
+ <motion.line variants={tickLeft} x1="2" x2="5" y1="12" y2="12" />
345
+ <motion.line variants={tickRight} x1="19" x2="22" y1="12" y2="12" />
346
+ <motion.line variants={tickTop} x1="12" x2="12" y1="2" y2="5" />
347
+ <motion.line variants={tickBottom} x1="12" x2="12" y1="19" y2="22" />
348
+ <motion.circle variants={ring} style={centre} cx="12" cy="12" r="7" />
349
+ </motion.g>
350
+ <motion.circle
351
+ variants={lockDot}
352
+ style={centre}
353
+ fill="currentColor"
354
+ fillOpacity={0}
355
+ cx="12"
356
+ cy="12"
357
+ r="3"
358
+ />
359
+ </MotionIcon>
360
+ );
361
+ }
362
+
363
+ // ─── Maximize — the corners shove apart along their diagonal (detach) ─────────
364
+
365
+ const MAXIMIZE = beats([0, 0.16, 0.44, 0.6, 0.82, 1], 0.6);
366
+
367
+ /**
368
+ * The corners pull toward the centre, then shove outward past the box (apex
369
+ * at (28, −4) and (−4, 28)) with the diagonals stretching to keep hold of
370
+ * them, and snap back. The diagonal scale is (9.9 + d·√2) / 9.9 so its end
371
+ * always meets the corner's apex — corners flying with static diagonals open
372
+ * a gap mid-arm and read as breakage.
373
+ */
374
+ const CORNER_OFFSET = [0, -2, 7, 6, -0.8, 0];
375
+ const DIAGONAL_STRETCH = CORNER_OFFSET.map((d) => (9.9 + d * Math.SQRT2) / 9.9);
376
+
377
+ const cornerTopRight = returning(
378
+ { x: CORNER_OFFSET, y: CORNER_OFFSET.map((d) => -d) },
379
+ MAXIMIZE
380
+ );
381
+ const cornerBottomLeft = returning(
382
+ { x: CORNER_OFFSET.map((d) => -d), y: CORNER_OFFSET },
383
+ MAXIMIZE
384
+ );
385
+ const diagonal = returning({ scale: DIAGONAL_STRETCH }, MAXIMIZE);
386
+
387
+ export function MotionMaximize(props: MotionIconProps) {
388
+ return (
389
+ <MotionIcon {...props}>
390
+ <motion.path variants={cornerTopRight} d="M15 3h6v6" />
391
+ <motion.path variants={diagonal} style={pivot(14, 10)} d="m21 3-7 7" />
392
+ <motion.path variants={diagonal} style={pivot(10, 14)} d="m3 21 7-7" />
393
+ <motion.path variants={cornerBottomLeft} d="M9 21H3v-6" />
394
+ </MotionIcon>
395
+ );
396
+ }
397
+
398
+ // ─── Phone — the handset is lifted to the ear and dropped back (detach) ───────
399
+
400
+ const PHONE = beats([0, 0.14, 0.42, 0.56, 0.8, 1], 0.64);
401
+
402
+ /**
403
+ * Placing a call, not receiving one — so it is picked up, not rung. The
404
+ * handset swings from the diagonal to upright around the mouthpiece block
405
+ * (earpiece ends 3.5 units above the box, inside the button's padding), hangs
406
+ * at the ear a beat, and drops back onto the hook with a clank. Two origins
407
+ * need two elements: the rotation on the path, the squash on the group.
408
+ */
409
+ const handset = returning({ rotate: [0, -6, 46, 40, -5, 0] }, PHONE);
410
+ const cradle = returning({ scaleY: [1, 0.97, 1, 1, 0.92, 1] }, PHONE);
411
+
412
+ export function MotionPhone(props: MotionIconProps) {
413
+ return (
414
+ <MotionIcon {...props}>
415
+ <motion.g variants={cradle} style={pivot(18.5, 22)}>
416
+ <motion.path
417
+ variants={handset}
418
+ style={pivot(18.5, 18.5)}
419
+ d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"
420
+ />
421
+ </motion.g>
422
+ </MotionIcon>
423
+ );
424
+ }
425
+
426
+ // ─── Mail — the flap flips open on its fold and slaps shut (articulate) ───────
427
+
428
+ const MAIL_TIMES = [0, 0.14, 0.42, 0.56, 0.84, 1];
429
+
430
+ /**
431
+ * The flap flips up over the top edge into lucide `mail-open`'s peaked
432
+ * silhouette, hangs a beat, slams back down and the body flinches. `scaleY`
433
+ * through zero rather than `rotateX`: WebKit does not do 3D transforms on SVG
434
+ * children, and the edge-on frame is the same. The fold is at y 5.5, between
435
+ * the drawn top edge (4) and the V's ends (7), so the open flap attaches at
436
+ * the crease instead of floating 3 units above it.
437
+ */
438
+ const flap = returning({ scaleY: [1, 1.12, -1.15, -1, 1.08, 1] }, beats(MAIL_TIMES, 0.64));
439
+ const envelope = returning({ scaleY: [1, 1, 0.92, 1] }, beats([0, 0.84, 0.92, 1], 0.64));
440
+
441
+ export function MotionMail(props: MotionIconProps) {
442
+ return (
443
+ <MotionIcon {...props}>
444
+ <motion.rect variants={envelope} style={pivot(12, 20)} x="2" y="4" width="20" height="16" rx="2" />
445
+ <motion.path
446
+ variants={flap}
447
+ style={pivot(12, 5.5)}
448
+ d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"
449
+ />
450
+ </MotionIcon>
451
+ );
452
+ }
453
+
454
+ // ─── Flag — raised, planted, the cloth whips on the impact (fall and land) ────
455
+
456
+ const FLAG = beats([0, 0.18, 0.4, 0.56, 0.76, 1], 0.62);
457
+
458
+ /**
459
+ * Report a listing: the flag is raised 8 units (cloth clear of the top),
460
+ * thrust into the ground, the pole compresses on impact and the cloth —
461
+ * streaming upward from the momentum — whips forward and flops to rest, 100ms
462
+ * behind the impact. Lucide draws pole and cloth as one path; cut at (4, 4),
463
+ * where the cloth is fixed to the pole. The cloth ends at (4, 15.5), on the
464
+ * pole, so the seam sits under the pole stroke at rest.
465
+ */
466
+ const wholeFlag = returning({ y: [0, -8, 1.2, 0, 0, 0] }, FLAG);
467
+ const pole = returning({ scaleY: [1, 1, 0.88, 1.02, 1, 1] }, FLAG);
468
+ const cloth = returning({ rotate: [0, 5, -22, 12, -4, 0] }, FLAG);
469
+
470
+ export function MotionFlag(props: MotionIconProps) {
471
+ return (
472
+ <MotionIcon {...props}>
473
+ <motion.g variants={wholeFlag}>
474
+ <motion.path variants={pole} style={pivot(4, 22)} d="M4 22V4" />
475
+ <motion.path
476
+ variants={cloth}
477
+ style={pivot(4, 4)}
478
+ d="M4 4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"
479
+ />
480
+ </motion.g>
481
+ </MotionIcon>
482
+ );
483
+ }
484
+
485
+ // ─── Log in / out — the arrow goes through the door (cycle content) ───────────
486
+
487
+ const DOOR_REEL = beats([0, 0.16, 0.6, 0.78, 1], 0.6);
488
+
489
+ /**
490
+ * A 5-unit slide into the bracket is 3.4px at 16px — invisible — so this is
491
+ * the calendar's reel: the arrow backs up, runs through the doorway and is
492
+ * swallowed by it while an identical arrow arrives to take its place, and the
493
+ * frame shudders as the first one goes through. The reel ends 16 units on
494
+ * with the picture identical; `useIconGesture` snaps it back. The clip shows
495
+ * the head 2 units INSIDE the doorway before it vanishes — it goes through,
496
+ * it does not hit a wall — and stops the arrow at the box edge so it never
497
+ * draws over the label beside it.
498
+ */
499
+ const reel = returning({ x: [0, -2, 17.2, 15.6, 16] }, DOOR_REEL);
500
+
501
+ function DoorArrow({ shaftD, headD, dx }: { shaftD: string; headD: string; dx: number }) {
502
+ return (
503
+ <g transform={dx ? `translate(${dx} 0)` : undefined}>
504
+ <path d={shaftD} />
505
+ <path d={headD} />
506
+ </g>
507
+ );
508
+ }
509
+
510
+ const doorKickIn = returning({ x: [0, 0, 1.5, -0.4, 0] }, beats([0, 0.5, 0.64, 0.82, 1], 0.6));
511
+
512
+ export function MotionLogIn(props: MotionIconProps) {
513
+ const clipId = useSvgId("door-in");
514
+ return (
515
+ <MotionIcon {...props}>
516
+ <defs>
517
+ <clipPath id={clipId}>
518
+ <rect x="0" y="3" width="17" height="18" />
519
+ </clipPath>
520
+ </defs>
521
+ <g clipPath={`url(#${clipId})`}>
522
+ <motion.g variants={reel}>
523
+ <DoorArrow shaftD="M15 12H3" headD="m10 17 5-5-5-5" dx={0} />
524
+ <DoorArrow shaftD="M15 12H3" headD="m10 17 5-5-5-5" dx={-16} />
525
+ </motion.g>
526
+ </g>
527
+ <motion.path variants={doorKickIn} d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
528
+ </MotionIcon>
529
+ );
530
+ }
531
+
532
+ /** The mirror: the arrow runs off the right edge and a fresh one comes OUT of the doorway behind it. */
533
+ const doorKickOut = returning({ x: [0, 0, -1.5, 0.4, 0] }, beats([0, 0.3, 0.44, 0.62, 1], 0.6));
534
+
535
+ export function MotionLogOut(props: MotionIconProps) {
536
+ const clipId = useSvgId("door-out");
537
+ return (
538
+ <MotionIcon {...props}>
539
+ <defs>
540
+ <clipPath id={clipId}>
541
+ <rect x="8" y="3" width="16" height="18" />
542
+ </clipPath>
543
+ </defs>
544
+ <g clipPath={`url(#${clipId})`}>
545
+ <motion.g variants={reel}>
546
+ <DoorArrow shaftD="M21 12H9" headD="m16 17 5-5-5-5" dx={0} />
547
+ <DoorArrow shaftD="M21 12H9" headD="m16 17 5-5-5-5" dx={-16} />
548
+ </motion.g>
549
+ </g>
550
+ <motion.path variants={doorKickOut} d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
551
+ </MotionIcon>
552
+ );
553
+ }
554
+
555
+ // ─── Settings — the gear turns two teeth and seats (cycle content) ───────────
556
+
557
+ /**
558
+ * Winds back, snaps a quarter turn with an overshoot, and the hub pops as the
559
+ * teeth seat. Two teeth (90° on an 8-tooth gear), not one: 45° is 7.5 units
560
+ * of rim travel, a click that disappears at 16px. Not 360° — that is
561
+ * `MotionRefresh`, and a settings gear doing a full turn reads as loading.
562
+ * The weakest silhouette change in the batch, by its own admission: a near-
563
+ * round outline turning; the bumps shifting along the rim are what read.
564
+ */
565
+ const gear = returning({ rotate: [0, -10, 98, 87, 90] }, beats([0, 0.16, 0.62, 0.8, 1], 0.6));
566
+ const hub = returning({ scale: [1, 0.8, 0.8, 1.2, 1] }, beats([0, 0.16, 0.62, 0.78, 1], 0.6));
567
+
568
+ export function MotionSettings(props: MotionIconProps) {
569
+ const centre = pivot(12, 12);
570
+ return (
571
+ <MotionIcon {...props}>
572
+ <motion.path
573
+ variants={gear}
574
+ style={centre}
575
+ d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"
576
+ />
577
+ <motion.circle variants={hub} style={centre} cx="12" cy="12" r="3" />
578
+ </MotionIcon>
579
+ );
580
+ }
@@ -22,6 +22,7 @@ vi.mock("framer-motion", () => {
22
22
  React.createElement(tag, rest);
23
23
  return {
24
24
  useAnimationControls: () => ({ set: () => {}, start: () => Promise.resolve() }),
25
+ useReducedMotion: () => false,
25
26
  motion: {
26
27
  svg: passthrough("svg"),
27
28
  g: passthrough("g"),
@@ -402,33 +402,38 @@ export function MotionPlayPause(props: MotionIconProps) {
402
402
 
403
403
  // ─── Eye — password reveal. The lid closes over it (articulate) ──────────────
404
404
 
405
- const EYE_TIMES = [0, 0.3, 0.74, 1];
405
+ const EYE_TIMES = [0, 0.18, 0.4, 0.5, 0.76, 1];
406
+ const EYE = beats(EYE_TIMES, 0.52);
406
407
 
407
408
  /**
408
- * `active` is hidden, matching `EyeOff`. The eye does not swap glyph: the
409
- * pupil shrinks as the lid narrows, and the slash draws across with
410
- * `pathLength`, which is the one property that can grow a stroke without
411
- * morphing its `d`.
409
+ * `active` is hidden, matching `EyeOff`: the full eye with the slash across
410
+ * it. The held pose used to be the lid crushed to `scaleY 0.18` a 2px
411
+ * sliver at 28px — which put the whole character in the state layer and read
412
+ * as a broken glyph. The gesture is a BLINK: the lid shuts fast, holds shut
413
+ * one beat, and reopens with overshoot ONTO the slash, which draws through
414
+ * the shut hold with `pathLength` — the one property that can grow a stroke
415
+ * without morphing its `d`. The pupil flattens with the lid (occluded, not
416
+ * receding), so `scaleY` only.
412
417
  */
413
418
  const lid: Variants = {
414
419
  rest: { scaleY: 1, transition: returnTransition },
415
- active: { scaleY: 0.18, transition: returnTransition },
416
- play: { scaleY: [1, 1.06, 0.14, 0.18], transition: beats(EYE_TIMES, 0.46) },
420
+ active: { scaleY: 1, transition: returnTransition },
421
+ play: { scaleY: [1, 1.08, 0.2, 0.2, 1.08, 1], transition: EYE },
417
422
  };
418
423
 
419
424
  const pupil: Variants = {
420
- rest: { scale: 1, opacity: 1, transition: returnTransition },
421
- active: { scale: 0.2, opacity: 0, transition: returnTransition },
422
- play: { scale: [1, 1, 0.2, 0.2], opacity: [1, 1, 0, 0], transition: beats(EYE_TIMES, 0.46) },
425
+ rest: { scaleY: 1, transition: returnTransition },
426
+ active: { scaleY: 1, transition: returnTransition },
427
+ play: { scaleY: [1, 1, 0.2, 0.2, 1, 1], transition: EYE },
423
428
  };
424
429
 
425
430
  const slash: Variants = {
426
431
  rest: { pathLength: 0, opacity: 0, transition: { duration: 0.16 } },
427
432
  active: { pathLength: 1, opacity: 1, transition: { duration: 0.16 } },
428
433
  play: {
429
- pathLength: [0, 0, 1, 1],
430
- opacity: [0, 1, 1, 1],
431
- transition: beats(EYE_TIMES, 0.46),
434
+ pathLength: [0, 0, 0, 0, 1, 1],
435
+ opacity: [0, 0, 1, 1, 1, 1],
436
+ transition: EYE,
432
437
  },
433
438
  };
434
439