@bearmenu/ui 0.8.12 → 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.
- package/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
- package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
- package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
- package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
- package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
- package/dist/chunk-DJD3EMPK.js +126 -0
- package/dist/chunk-EZG2NELX.js +5 -0
- package/dist/{chunk-VAXOQS7W.js → chunk-F76NCVVR.js} +1 -1
- package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
- package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
- package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
- package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
- package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
- package/dist/chunk-I5V446BX.js +27 -0
- package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
- package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
- package/dist/chunk-LQWGZIYA.js +5 -0
- package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
- package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
- package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
- package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
- package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
- package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
- package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
- package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
- package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
- package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
- package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
- package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
- package/dist/{chunk-24RFI4RQ.js → chunk-SXQZ7DLH.js} +4 -4
- package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
- package/dist/{chunk-DTXNDLSE.js → chunk-WYTJR2YU.js} +16 -1
- package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
- package/dist/chunk-XCB3F6SF.js +16 -0
- package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
- package/dist/{chunk-7A42J73Q.js → chunk-ZLPWEHZO.js} +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +2 -2
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/alert.js +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/avatar.js +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/button-group.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +2 -2
- package/dist/components/calendar.js +2 -2
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +3 -3
- package/dist/components/carousel.js +2 -2
- package/dist/components/category-deck.js +2 -2
- package/dist/components/checkbox.js +2 -2
- package/dist/components/circular-progress.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +5 -5
- package/dist/components/command.d.ts +15 -15
- package/dist/components/command.js +3 -3
- package/dist/components/concept-card.js +2 -2
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.d.ts +2 -1
- package/dist/components/concept-compare.js +5 -5
- package/dist/components/concept-convergence.js +4 -4
- package/dist/components/convergence.js +3 -3
- package/dist/components/coverflow.js +1 -1
- package/dist/components/currency-input.js +2 -2
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/date-input.js +2 -2
- package/dist/components/day-rail-chip.js +2 -2
- package/dist/components/description-list.js +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/doorway-tiles.d.ts +57 -0
- package/dist/components/doorway-tiles.js +86 -0
- package/dist/components/drawer.js +1 -1
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +3 -3
- package/dist/components/event-card.js +4 -4
- package/dist/components/event-compact-row.js +4 -4
- package/dist/components/event-day-header.js +2 -2
- package/dist/components/event-day-separator.js +3 -3
- package/dist/components/event-detail-panel.js +4 -4
- package/dist/components/event-hub-hero.js +2 -2
- package/dist/components/event-index-row.js +4 -4
- package/dist/components/event-list-row-wide.js +5 -5
- package/dist/components/event-list-row.js +5 -5
- package/dist/components/event-poster-fallback.js +1 -1
- package/dist/components/event-types.js +1 -1
- package/dist/components/events-empty-state.js +1 -1
- package/dist/components/explainer-steps.js +1 -1
- package/dist/components/facet-group.js +2 -2
- package/dist/components/featured-rail-item.js +2 -2
- package/dist/components/field.js +2 -2
- package/dist/components/filter-category-row.js +3 -3
- package/dist/components/form.js +2 -2
- package/dist/components/glass-ground.js +2 -2
- package/dist/components/grade-atom.d.ts +3 -0
- package/dist/components/grade-atom.js +4 -0
- package/dist/components/haptic-button.js +2 -2
- package/dist/components/hour-band.js +4 -4
- package/dist/components/hour-group.js +4 -4
- package/dist/components/hub-surface.js +1 -1
- package/dist/components/hue-chip.js +1 -1
- package/dist/components/icon.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input-otp.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/item.d.ts +2 -2
- package/dist/components/item.js +1 -1
- package/dist/components/kbd.js +1 -1
- package/dist/components/kitchen-dossier.d.ts +2 -1
- package/dist/components/kitchen-dossier.js +3 -3
- package/dist/components/label.js +2 -2
- package/dist/components/lazy-markdown.js +1 -1
- package/dist/components/link-button.js +2 -2
- package/dist/components/markdown-renderer.js +1 -1
- package/dist/components/menu-dead-end.d.ts +2 -1
- package/dist/components/menu-dead-end.js +1 -1
- package/dist/components/menu-family-block.d.ts +2 -1
- package/dist/components/menu-family-block.js +14 -8
- package/dist/components/menu-family-card.d.ts +2 -1
- package/dist/components/menu-family-card.js +1 -1
- package/dist/components/menu-field.js +1 -1
- package/dist/components/menu-filter-chips.d.ts +2 -1
- package/dist/components/menu-filter-chips.js +3 -3
- package/dist/components/menu-group-rows.d.ts +2 -1
- package/dist/components/menu-group-rows.js +2 -2
- package/dist/components/menu-hub-hero.js +1 -1
- package/dist/components/menu-index-header.d.ts +2 -1
- package/dist/components/menu-index-header.js +2 -2
- package/dist/components/menu-index-list.d.ts +9 -8
- package/dist/components/menu-index-list.js +4 -4
- package/dist/components/menu-intent-hero.d.ts +3 -2
- package/dist/components/menu-intent-hero.js +1 -1
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +4 -4
- package/dist/components/menu-kitchen-list.d.ts +2 -1
- package/dist/components/menu-kitchen-list.js +4 -4
- package/dist/components/menu-mocks.d.ts +16 -5
- package/dist/components/menu-mocks.js +23 -2
- package/dist/components/menu-types.d.ts +21 -34
- package/dist/components/motion-icon-actions.d.ts +27 -0
- package/dist/components/motion-icon-actions.js +336 -0
- package/dist/components/motion-icon-controls.js +3 -3
- package/dist/components/motion-icon-set.d.ts +13 -1
- package/dist/components/motion-icon-set.js +197 -6
- package/dist/components/motion-icon.d.ts +15 -1
- package/dist/components/motion-icon.js +2 -2
- package/dist/components/multi-select-combobox.js +7 -7
- package/dist/components/multi-select-menu.js +2 -2
- package/dist/components/native-select.js +1 -1
- package/dist/components/open-now-list.d.ts +2 -1
- package/dist/components/open-now-list.js +2 -2
- package/dist/components/pagination.js +2 -2
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -1
- package/dist/components/place-about-tab.js +6 -6
- package/dist/components/place-card.d.ts +94 -12
- package/dist/components/place-card.js +203 -128
- package/dist/components/place-details-mobile.d.ts +2 -1
- package/dist/components/place-details-mobile.js +7 -7
- package/dist/components/place-schedule-week.d.ts +2 -1
- package/dist/components/place-schedule-week.js +3 -3
- package/dist/components/place-types.d.ts +3 -265
- package/dist/components/popover.js +2 -2
- package/dist/components/poster-stack.js +1 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +22 -6
- package/dist/components/poster-wall.js +197 -12
- package/dist/components/progress.js +1 -1
- package/dist/components/qa-block.d.ts +2 -1
- package/dist/components/qa-block.js +1 -1
- package/dist/components/quiet-stretch.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-fade.js +2 -2
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +5 -5
- package/dist/components/section-header.js +2 -2
- package/dist/components/segmented-nav.d.ts +24 -4
- package/dist/components/segmented-nav.js +11 -2
- package/dist/components/select.js +1 -1
- package/dist/components/selection-bar.js +1 -1
- package/dist/components/separator.js +2 -2
- package/dist/components/sheet.js +1 -1
- package/dist/components/signature-chip.js +3 -3
- package/dist/components/skeleton-card.js +3 -3
- package/dist/components/skeleton.js +2 -2
- package/dist/components/slider.js +1 -1
- package/dist/components/sliding-panels.js +2 -2
- package/dist/components/snap-rail.d.ts +0 -5
- package/dist/components/snap-rail.js +2 -2
- package/dist/components/sonner.js +1 -1
- package/dist/components/sort-option-list.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/spinner.js +1 -1
- package/dist/components/stat-banner.js +1 -1
- package/dist/components/stepper.js +1 -1
- package/dist/components/sticky-container.js +1 -1
- package/dist/components/story.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tag-group.js +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/text.js +2 -2
- package/dist/components/textarea.js +1 -1
- package/dist/components/timeline-band.js +4 -4
- package/dist/components/timeline-playhead.js +4 -4
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle.js +2 -2
- package/dist/components/tooltip.js +1 -1
- package/dist/components/top-comparison.d.ts +2 -1
- package/dist/components/top-comparison.js +1 -1
- package/dist/components/top-cross-link.d.ts +2 -1
- package/dist/components/top-cross-link.js +1 -1
- package/dist/components/top-ranking-entry.d.ts +2 -1
- package/dist/components/top-ranking-entry.js +6 -6
- package/dist/components/top-ranking-hero.d.ts +2 -1
- package/dist/components/top-ranking-hero.js +1 -1
- package/dist/components/top-ranking-legend.js +1 -1
- package/dist/components/top-types.d.ts +3 -1
- package/dist/components/use-mobile.js +1 -1
- package/dist/components/venue-events-block.js +5 -5
- package/dist/components/venue-grade-dimensions.d.ts +2 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +2 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.d.ts +2 -1
- package/dist/components/venue-wall.js +1 -1
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +4 -4
- package/dist/hooks/use-drag-swipe.js +1 -1
- package/dist/hooks/use-haptic.js +1 -1
- package/dist/hooks/use-media-query.js +1 -1
- package/dist/hooks/use-mobile.js +1 -1
- package/dist/hooks/use-mounted-theme.js +1 -1
- package/dist/hooks/use-sticky.js +1 -1
- package/dist/lib/card-hover.js +2 -18
- package/dist/lib/grade.d.ts +54 -0
- package/dist/lib/grade.js +2 -0
- package/dist/lib/icon-depth.d.ts +35 -0
- package/dist/lib/icon-depth.js +2 -0
- package/dist/lib/icon-registry.js +1 -1
- package/dist/lib/motion.js +2 -2
- package/dist/lib/overlay-pill.d.ts +30 -0
- package/dist/lib/overlay-pill.js +2 -0
- package/dist/lib/utils.js +1 -1
- package/dist/place-types-BHdeMMiG.d.ts +317 -0
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +268 -82
- package/src/components/doorway-tiles.stories.tsx +83 -0
- package/src/components/doorway-tiles.test.tsx +51 -0
- package/src/components/doorway-tiles.tsx +225 -0
- package/src/components/grade-atom.stories.tsx +81 -0
- package/src/components/grade-atom.tsx +256 -0
- package/src/components/hue-chip.stories.tsx +2 -2
- package/src/components/menu-family-block.stories.tsx +29 -0
- package/src/components/menu-family-block.test.tsx +51 -0
- package/src/components/menu-family-block.tsx +20 -4
- package/src/components/menu-index-list.tsx +7 -7
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-kitchen-list.tsx +4 -1
- package/src/components/menu-mocks.ts +39 -3
- package/src/components/menu-types.ts +17 -33
- package/src/components/motion-icon-actions.test.tsx +115 -0
- package/src/components/motion-icon-actions.tsx +580 -0
- package/src/components/motion-icon-controls.test.tsx +1 -0
- package/src/components/motion-icon-controls.tsx +18 -13
- package/src/components/motion-icon-set.stories.tsx +62 -3
- package/src/components/motion-icon-set.tsx +349 -2
- package/src/components/motion-icon.test.tsx +180 -1
- package/src/components/motion-icon.tsx +97 -8
- package/src/components/place-card.stories.tsx +66 -76
- package/src/components/place-card.test.tsx +188 -100
- package/src/components/place-card.tsx +356 -148
- package/src/components/place-types.ts +17 -4
- package/src/components/poster-wall.stories.tsx +84 -0
- package/src/components/poster-wall.test.tsx +94 -3
- package/src/components/poster-wall.tsx +257 -9
- package/src/components/segmented-nav.stories.tsx +72 -1
- package/src/components/segmented-nav.test.tsx +96 -0
- package/src/components/segmented-nav.tsx +36 -4
- package/src/components/snap-rail.tsx +19 -0
- package/src/globals.css +26 -0
- package/src/lib/grade.test.ts +82 -0
- package/src/lib/grade.ts +76 -0
- package/src/lib/icon-depth.ts +35 -0
- package/src/lib/overlay-pill.ts +29 -0
- package/dist/components/menu-family-tiles.d.ts +0 -19
- package/dist/components/menu-family-tiles.js +0 -57
- package/src/components/menu-family-tiles.stories.tsx +0 -54
- package/src/components/menu-family-tiles.test.tsx +0 -30
- 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.
|
|
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
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
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:
|
|
416
|
-
play: { scaleY: [1, 1.
|
|
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: {
|
|
421
|
-
active: {
|
|
422
|
-
play: {
|
|
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:
|
|
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
|
|