weasel-icons 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/THIRD_PARTY_NOTICES.md +103 -0
  4. package/dist/icons/alert/alert-icon.d.ts +6 -0
  5. package/dist/icons/alert/alert-icon.js +22 -0
  6. package/dist/icons/alert/alert-rig.d.ts +12 -0
  7. package/dist/icons/alert/alert-rig.js +25 -0
  8. package/dist/icons/badge-check/badge-check-icon.d.ts +5 -0
  9. package/dist/icons/badge-check/badge-check-icon.js +33 -0
  10. package/dist/icons/badge-check/badge-check-rig.d.ts +14 -0
  11. package/dist/icons/badge-check/badge-check-rig.js +36 -0
  12. package/dist/icons/bell/bell-icon.d.ts +5 -0
  13. package/dist/icons/bell/bell-icon.js +22 -0
  14. package/dist/icons/bell/bell-rig.d.ts +14 -0
  15. package/dist/icons/bell/bell-rig.js +21 -0
  16. package/dist/icons/bulb/bulb-icon.d.ts +5 -0
  17. package/dist/icons/bulb/bulb-icon.js +25 -0
  18. package/dist/icons/bulb/bulb-rig.d.ts +13 -0
  19. package/dist/icons/bulb/bulb-rig.js +37 -0
  20. package/dist/icons/check/check-icon.d.ts +5 -0
  21. package/dist/icons/check/check-icon.js +26 -0
  22. package/dist/icons/check/check-rig.d.ts +6 -0
  23. package/dist/icons/check/check-rig.js +38 -0
  24. package/dist/icons/clapperboard/clapperboard-icon.d.ts +5 -0
  25. package/dist/icons/clapperboard/clapperboard-icon.js +18 -0
  26. package/dist/icons/clapperboard/clapperboard-rig.d.ts +12 -0
  27. package/dist/icons/clapperboard/clapperboard-rig.js +25 -0
  28. package/dist/icons/close/close-icon.d.ts +5 -0
  29. package/dist/icons/close/close-icon.js +27 -0
  30. package/dist/icons/close/close-rig.d.ts +10 -0
  31. package/dist/icons/close/close-rig.js +31 -0
  32. package/dist/icons/copy/copy-icon.d.ts +5 -0
  33. package/dist/icons/copy/copy-icon.js +31 -0
  34. package/dist/icons/copy/copy-rig.d.ts +13 -0
  35. package/dist/icons/copy/copy-rig.js +44 -0
  36. package/dist/icons/cursor/cursor-icon.d.ts +5 -0
  37. package/dist/icons/cursor/cursor-icon.js +32 -0
  38. package/dist/icons/cursor/cursor-rig.d.ts +21 -0
  39. package/dist/icons/cursor/cursor-rig.js +63 -0
  40. package/dist/icons/delete/delete-icon.d.ts +5 -0
  41. package/dist/icons/delete/delete-icon.js +25 -0
  42. package/dist/icons/delete/delete-rig.d.ts +15 -0
  43. package/dist/icons/delete/delete-rig.js +33 -0
  44. package/dist/icons/dog/dog-icon.d.ts +7 -0
  45. package/dist/icons/dog/dog-icon.js +45 -0
  46. package/dist/icons/dog/dog-rig.d.ts +80 -0
  47. package/dist/icons/dog/dog-rig.js +285 -0
  48. package/dist/icons/download/download-icon.d.ts +5 -0
  49. package/dist/icons/download/download-icon.js +23 -0
  50. package/dist/icons/download/download-rig.d.ts +19 -0
  51. package/dist/icons/download/download-rig.js +71 -0
  52. package/dist/icons/eye/eye-icon.d.ts +5 -0
  53. package/dist/icons/eye/eye-icon.js +27 -0
  54. package/dist/icons/eye/eye-rig.d.ts +9 -0
  55. package/dist/icons/eye/eye-rig.js +37 -0
  56. package/dist/icons/heart/heart-art.d.ts +4 -0
  57. package/dist/icons/heart/heart-art.js +6 -0
  58. package/dist/icons/heart/heart-icon.d.ts +6 -0
  59. package/dist/icons/heart/heart-icon.js +36 -0
  60. package/dist/icons/heart/heart-rig.d.ts +21 -0
  61. package/dist/icons/heart/heart-rig.js +68 -0
  62. package/dist/icons/link/link-icon.d.ts +5 -0
  63. package/dist/icons/link/link-icon.js +30 -0
  64. package/dist/icons/link/link-rig.d.ts +13 -0
  65. package/dist/icons/link/link-rig.js +37 -0
  66. package/dist/icons/monkey/monkey-icon.d.ts +11 -0
  67. package/dist/icons/monkey/monkey-icon.js +44 -0
  68. package/dist/icons/monkey/monkey-rig.d.ts +77 -0
  69. package/dist/icons/monkey/monkey-rig.js +376 -0
  70. package/dist/icons/plug/plug-icon.d.ts +5 -0
  71. package/dist/icons/plug/plug-icon.js +21 -0
  72. package/dist/icons/plug/plug-rig.d.ts +16 -0
  73. package/dist/icons/plug/plug-rig.js +29 -0
  74. package/dist/icons/refresh/refresh-icon.d.ts +5 -0
  75. package/dist/icons/refresh/refresh-icon.js +40 -0
  76. package/dist/icons/refresh/refresh-rig.d.ts +11 -0
  77. package/dist/icons/refresh/refresh-rig.js +30 -0
  78. package/dist/icons/replace/replace-icon.d.ts +5 -0
  79. package/dist/icons/replace/replace-icon.js +25 -0
  80. package/dist/icons/replace/replace-rig.d.ts +14 -0
  81. package/dist/icons/replace/replace-rig.js +24 -0
  82. package/dist/icons/rocket/rocket-art.d.ts +7 -0
  83. package/dist/icons/rocket/rocket-art.js +8 -0
  84. package/dist/icons/rocket/rocket-icon.d.ts +5 -0
  85. package/dist/icons/rocket/rocket-icon.js +39 -0
  86. package/dist/icons/rocket/rocket-rig.d.ts +30 -0
  87. package/dist/icons/rocket/rocket-rig.js +116 -0
  88. package/dist/icons/search/search-icon.d.ts +5 -0
  89. package/dist/icons/search/search-icon.js +21 -0
  90. package/dist/icons/search/search-rig.d.ts +14 -0
  91. package/dist/icons/search/search-rig.js +22 -0
  92. package/dist/icons/shield/shield-icon.d.ts +5 -0
  93. package/dist/icons/shield/shield-icon.js +25 -0
  94. package/dist/icons/shield/shield-rig.d.ts +23 -0
  95. package/dist/icons/shield/shield-rig.js +42 -0
  96. package/dist/icons/ship/ship-art.d.ts +3 -0
  97. package/dist/icons/ship/ship-art.js +5 -0
  98. package/dist/icons/ship/ship-icon.d.ts +6 -0
  99. package/dist/icons/ship/ship-icon.js +42 -0
  100. package/dist/icons/ship/ship-rig.d.ts +65 -0
  101. package/dist/icons/ship/ship-rig.js +206 -0
  102. package/dist/icons/sparkle/sparkle-icon.d.ts +5 -0
  103. package/dist/icons/sparkle/sparkle-icon.js +27 -0
  104. package/dist/icons/sparkle/sparkle-rig.d.ts +8 -0
  105. package/dist/icons/sparkle/sparkle-rig.js +6 -0
  106. package/dist/icons/sparkles/sparkles-icon.d.ts +5 -0
  107. package/dist/icons/sparkles/sparkles-icon.js +27 -0
  108. package/dist/icons/sparkles/sparkles-rig.d.ts +14 -0
  109. package/dist/icons/sparkles/sparkles-rig.js +24 -0
  110. package/dist/icons/star/star-icon.d.ts +5 -0
  111. package/dist/icons/star/star-icon.js +38 -0
  112. package/dist/icons/star/star-rig.d.ts +20 -0
  113. package/dist/icons/star/star-rig.js +61 -0
  114. package/dist/icons/swap/swap-icon.d.ts +5 -0
  115. package/dist/icons/swap/swap-icon.js +26 -0
  116. package/dist/icons/swap/swap-rig.d.ts +10 -0
  117. package/dist/icons/swap/swap-rig.js +24 -0
  118. package/dist/icons/wave/wave-icon.d.ts +5 -0
  119. package/dist/icons/wave/wave-icon.js +18 -0
  120. package/dist/icons/wave/wave-rig.d.ts +8 -0
  121. package/dist/icons/wave/wave-rig.js +39 -0
  122. package/dist/index.d.ts +30 -0
  123. package/dist/index.js +29 -0
  124. package/dist/shared/behavior-curves.d.ts +4 -0
  125. package/dist/shared/behavior-curves.js +17 -0
  126. package/dist/shared/behavior-runtime.d.ts +18 -0
  127. package/dist/shared/behavior-runtime.js +110 -0
  128. package/dist/shared/icon-colors.d.ts +53 -0
  129. package/dist/shared/icon-colors.js +1 -0
  130. package/dist/shared/icon-types.d.ts +14 -0
  131. package/dist/shared/icon-types.js +1 -0
  132. package/dist/shared/pose-spring.d.ts +9 -0
  133. package/dist/shared/pose-spring.js +13 -0
  134. package/dist/shared/sparkle-motion.d.ts +12 -0
  135. package/dist/shared/sparkle-motion.js +38 -0
  136. package/dist/shared/use-icon-motion.d.ts +14 -0
  137. package/dist/shared/use-icon-motion.js +98 -0
  138. package/dist/shared/use-motion-preference.d.ts +1 -0
  139. package/dist/shared/use-motion-preference.js +32 -0
  140. package/package.json +186 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Weasel contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,75 @@
1
- # Temporary Holding Version
1
+ # Weasel Icons
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Icons that overreact beautifully. Stateful React SVG icons with smooth transitions and optional continuous behavior.
4
+
5
+ Version `0.2.0` contains 28 icons. See the repository design docs for supported states and verification evidence.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install weasel-icons
11
+ # or
12
+ pnpm add weasel-icons
13
+ ```
14
+
15
+ Supports React / React DOM 18.3 or 19. Motion is installed as a dependency. No stylesheet, provider, Tailwind, or Next.js dependency is required. The package is ESM with TypeScript declarations and per-icon exports.
16
+
17
+ ## Use
18
+
19
+ ```tsx
20
+ import { DogIcon } from 'weasel-icons';
21
+ // Alternatively: import { DogIcon } from 'weasel-icons/dog';
22
+
23
+ export function HappyDog() {
24
+ return <DogIcon state="happy" alive size={32} color="#75002f" accentColor="#ff5ca3" />;
25
+ }
26
+ ```
27
+
28
+ Import only the icons you use. Compatible production bundlers can tree-shake the others.
29
+
30
+ ## Change a state
31
+
32
+ ```tsx
33
+ 'use client';
34
+
35
+ import { useState } from 'react';
36
+ import { HeartIcon } from 'weasel-icons';
37
+
38
+ export function LikeButton() {
39
+ const [liked, setLiked] = useState(false);
40
+ return (
41
+ <button aria-label="Like" aria-pressed={liked} onClick={() => setLiked(!liked)}>
42
+ <HeartIcon state={liked ? 'liked' : 'unliked'} size={24} />
43
+ </button>
44
+ );
45
+ }
46
+ ```
47
+
48
+ The existing SVG smoothly transitions when `state` changes, including during an unfinished transition. Use your own button or wrapper for hover, click, keyboard, and sequence behavior. Next.js App Router client boundaries are preserved in the package.
49
+
50
+ ## Icons and props
51
+
52
+ Exports in the current local build: `DogIcon`, `MonkeyIcon`, `HeartIcon`, `ShipIcon`, `RocketIcon`, `SearchIcon`, `BellIcon`, `DownloadIcon`, `ShieldIcon`, `CursorIcon`, `ClapperboardIcon`, `PlugIcon`, `BulbIcon`, `AlertIcon`, `BadgeCheckIcon`, `ReplaceIcon`, `DeleteIcon`, `SwapIcon`, `SparklesIcon`, `SparkleIcon`, `StarIcon`, `WaveIcon`, `RefreshIcon`, `CopyIcon`, `EyeIcon`, `LinkIcon`, `CheckIcon`, and `CloseIcon`. Each icon exports its own state type and list, for example `DogState` and `dogStates`, or `BadgeCheckState` and `badgeCheckStates`. TypeScript autocompletion shows the valid states for each icon.
53
+
54
+ ### Upgrading from 0.1.0
55
+
56
+ Version `0.2.0` also includes `SparklesIcon` and the single-contour `SparkleIcon` (both `idle`, `shimmering`, `blink`, `dimmed`), `StarIcon` (`idle`, `shooting`, `starred`, `loading`) and `WaveIcon` (`idle`, `flowing`, `energetic`, `flat`), with direct exports at `/sparkles`, `/sparkle`, `/star` and `/wave`. Sparkles uses `blink` and Star uses `shooting`. `RefreshIcon`, `CopyIcon` and `LinkIcon` have four states; `EyeIcon`, `CheckIcon` and `CloseIcon` intentionally have three.
57
+
58
+ `0.2.0` adds Shield, Cursor, Clapperboard, Plug, Bulb, Alert, Badge Check, Replace, Delete and Swap, with the latest state feedback fixes. Leaf and its `weasel-icons/leaf` export have been removed; remove or replace any Leaf imports before upgrading. Existing retained icon imports continue to work. The new motions have geometry and lifecycle regression coverage, but are not newly browser-profiled performance guarantees.
59
+
60
+ Common optional props: `state`, `size`, `color`, `accentColor`, `strokeWidth`, `speed`, `alive`, `className`.
61
+
62
+ - `alive={false}` (default) transitions to the state, then settles.
63
+ - `strokeWidth` defaults to `1.65` SVG units; pass a different width to override it.
64
+ - `alive` adds varied continuous in-state behavior. System reduced-motion preferences take precedence.
65
+ - `color` and `accentColor` default to `currentColor`.
66
+ - Dog, monkey, heart, ship and alert accept small icon-specific `colors` objects, with exported types such as `DogColors`. Alert's critical punctuation defaults to red; use `colors={{ critical: 'your-colour' }}` to override it without recolouring the triangle.
67
+ - Dog and ship have neutral interiors using `--weasel-surface` (white by default). Set this CSS variable on their parent for other backgrounds. Monkey supports `colors.face`.
68
+
69
+ SVGs are decorative and hidden from screen readers. Label the containing control or adjacent status text. Do not communicate state using only color or animation.
70
+
71
+ This package installs into `node_modules`; it does not copy source into your components directory and does not expose an `npx` command. Upgrading the dependency brings new icons and fixes; it does not update already deployed websites until you rebuild and deploy them.
72
+
73
+ ## License
74
+
75
+ Original code is MIT licensed. Adapted source geometry retains its upstream notices in `THIRD_PARTY_NOTICES.md`, included with this package. The website, mascot, fonts, tests, and authoring tools are not part of this package.
@@ -0,0 +1,103 @@
1
+ # Third-party notices
2
+
3
+ The animated components in this project use original Weasel motion design on top of the following source geometry.
4
+
5
+ ## Lucide
6
+
7
+ Rocket, Dog, Heart, Ship, Bell, Search, Download, Shield, Cursor, Clapperboard, and Plug base SVG geometry is adapted from [Lucide](https://github.com/lucide-icons/lucide), licensed under the ISC License, with the additional Feather notice below for Search and Download. The current front-facing ship replaces the earlier Fluent side-profile implementation. Source SVGs for Bell, Search, Download and their inspected state references are retained in `docs/references/lucide-*.svg`. Shield uses `shield`; Cursor uses `mouse-pointer-2`; Clapperboard adapts `clapperboard` into a left-hinged rig; Plug articulates the two halves of `unplug`. Their source links and design decisions are recorded in `docs/TRACE_ICONS.md`.
8
+
9
+ Bulb, Alert, Badge Check, Replace, Delete and Swap additionally adapt Lucide's `lightbulb`, `triangle-alert`, `badge-check`, `replace`, `delete` and `arrow-left-right` assets. Their motion, rays, filament and status details are original to this project. No `lucide-react` runtime dependency is installed or redistributed.
10
+
11
+ Refresh, Copy, Eye, Link, Check and Close additionally adapt `refresh-cw`, `copy`, `eye` / `eye-off` landmarks, `link` (previously `link-2`), `check` and `x`. Eye uses a compatible cubic eyelid rather than swapping complete SVGs. Reference geometry and state-specific choreography are recorded in `docs/TRACE_UTILITY_ICONS.md`. Check, Link and Close retain the additional Feather MIT notice below.
12
+
13
+ The retained Lucide geometry carries the following [Lucide ISC notice](https://lucide.dev/license):
14
+ Star, Sparkles and Sparkle additionally adapt the current `star` and central `sparkles` contours. Sparkle is the single-contour composition; Sparkles' satellite diamonds and the single-line Wave geometry are original to Weasel. Wave follows the user's visual reference, not Lucide's stacked wave glyph.
15
+
16
+
17
+ ```text
18
+ ISC License
19
+
20
+ Copyright (c) 2026 Lucide Icons and Contributors
21
+
22
+ Permission to use, copy, modify, and/or distribute this software for any purpose
23
+ with or without fee is hereby granted, provided that the above copyright notice
24
+ and this permission notice appear in all copies.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
27
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
28
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
29
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
30
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
31
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
32
+ PERFORMANCE OF THIS SOFTWARE.
33
+ ```
34
+
35
+ ## Feather-derived Lucide icons
36
+
37
+ Search, Download, Alert (listed upstream under its former `alert-triangle` name), Check, Link (`link-2`) and Close (`x`) are listed by Lucide as derived from Feather. Their additional notice is retained from the [upstream license](https://github.com/lucide-icons/lucide/blob/main/LICENSE):
38
+
39
+ ```text
40
+ The MIT License (MIT)
41
+
42
+ Copyright (c) 2013-present Cole Bemis
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining a copy
45
+ of this software and associated documentation files (the "Software"), to deal
46
+ in the Software without restriction, including without limitation the rights
47
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
48
+ copies of the Software, and to permit persons to whom the Software is
49
+ furnished to do so, subject to the following conditions:
50
+
51
+ The above copyright notice and this permission notice shall be included in all
52
+ copies or substantial portions of the Software.
53
+
54
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
55
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
56
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
57
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
58
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
59
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
60
+ SOFTWARE.
61
+ ```
62
+
63
+ ## Microsoft Fluent Emoji
64
+
65
+ Monkey Face base SVG geometry is adapted from [Microsoft Fluent Emoji](https://github.com/microsoft/fluentui-emoji), licensed under the MIT License. Historical ship and dog references (not used by the current outline components), and the complete Microsoft copyright/permission notice are retained in `docs/references/`.
66
+
67
+ ```text
68
+ MIT License
69
+
70
+ Copyright (c) Microsoft Corporation.
71
+
72
+ Permission is hereby granted, free of charge, to any person obtaining a copy
73
+ of this software and associated documentation files (the "Software"), to deal
74
+ in the Software without restriction, including without limitation the rights
75
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
76
+ copies of the Software, and to permit persons to whom the Software is
77
+ furnished to do so, subject to the following conditions:
78
+
79
+ The above copyright notice and this permission notice shall be included in all
80
+ copies or substantial portions of the Software.
81
+
82
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
83
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
84
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
85
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
86
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
87
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
88
+ SOFTWARE
89
+ ```
90
+
91
+ The animation choreography, state behavior and state effects are independently authored for this project. This does not grant rights to third-party artwork displayed by the website.
92
+
93
+ ## Website-only illustration references and brand draft
94
+
95
+ The red-panda sheet under `public/art` and its reference-derived raster cutout match [Madi / madivoid's Red Panda Spread Art Print](https://www.etsy.com/listing/1624097837/red-panda-spread-art-print-animal-art), visually checked on October 24, 2026. Reuse permission has not been obtained; the physical-print listing is not a commercial website or branding licence. The website currently uses six poses from this sheet. Older dog/cat sheets remain local historical files but are not rendered; the animated SVG box-cat component has been removed. The eyes/nose/apple logo in `public/weasel-mark.svg` has newly drawn paths, but has not received copyright or trademark clearance. The requested red-panda head crop has not replaced it. See `docs/reference-artwork.md` for provenance, source-research limits and required pre-launch checks. Keep these website design assets local; they are not part of the npm icon package, and its MIT licence does not license the supplied illustrations.
96
+
97
+ ## Motion study reference
98
+
99
+ The animation system was informed by the part-level motion principles demonstrated by [lucide-animated](https://github.com/pqoqubbw/icons), licensed under the MIT License. Weasel does not redistribute its React components; the state model and choreography in this project are independently authored.
100
+
101
+ ## Website fonts
102
+
103
+ The playground self-hosts unmodified Latin WOFF2 assets for [Anton](https://github.com/google/fonts/tree/main/ofl/anton) and [DM Sans](https://github.com/google/fonts/tree/main/ofl/dmsans), distributed by Google Fonts under the SIL Open Font License 1.1. Their complete copyright and license notices are included alongside the assets at `public/fonts/anton/OFL.txt` and `public/fonts/dm-sans/OFL.txt`. These website assets are not included in the icon installation payloads.
@@ -0,0 +1,6 @@
1
+ import type { StatefulIconProps } from "../../shared/icon-types.js";
2
+ import { type AlertState } from "./alert-rig.js";
3
+ import type { AlertColors } from "../../shared/icon-colors.js";
4
+ export { alertStates, type AlertState } from "./alert-rig.js";
5
+ export type AlertIconProps = StatefulIconProps<AlertState, AlertColors>;
6
+ export declare const AlertIcon: import("react").NamedExoticComponent<AlertIconProps>;
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { memo, useRef } from 'react';
4
+ import { motion, useTransform } from 'motion/react';
5
+ import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
6
+ import { useMotionPreference } from "../../shared/use-motion-preference.js";
7
+ import { ALERT_SHELL, sampleAlert } from "./alert-rig.js";
8
+ export { alertStates } from "./alert-rig.js";
9
+ const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
10
+ export const AlertIcon = memo(function AlertIcon({ state = 'idle', size = 24, color = 'currentColor', colors, strokeWidth = 1.65, speed = 1, alive = false, className, }) {
11
+ const reduced = useMotionPreference(), element = useRef(null);
12
+ const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
13
+ const idle = usePoseWeight(state === 'idle', pace, reduced), warning = usePoseWeight(state === 'warning', pace, reduced), critical = usePoseWeight(state === 'critical', pace, reduced), resolved = usePoseWeight(state === 'resolved', pace, reduced);
14
+ const performs = alive && !reduced && state !== 'idle', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
15
+ const warningEntry = useActionProgress(state === 'warning', pace, reduced, 1.4);
16
+ const criticalEntry = useActionProgress(state === 'critical', pace, reduced, 1.4);
17
+ const rig = useTransform(() => sampleAlert([idle.get(), warning.get(), critical.get(), resolved.get()], time.get(), life.get(), warningEntry.get(), criticalEntry.get()));
18
+ const criticalColor = colors?.critical ?? '#ef4444';
19
+ const blendedColor = useTransform(critical, [0, 1], [color, criticalColor]);
20
+ const body = useTransform(rig, (r) => r.body), shell = useTransform(rig, (r) => r.shell), mark = useTransform(rig, (r) => r.mark), markOpacity = useTransform(rig, (r) => r.markOpacity), dot = useTransform(rig, (r) => r.dot * r.markOpacity), dotY = useTransform(rig, (r) => r.dotY), markColor = useTransform(() => critical.get() === 0 ? color : critical.get() === 1 ? criticalColor : blendedColor.get());
21
+ return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "alert-body", transform: body, style: ORIGIN, children: [_jsx(motion.path, { "data-part": "shell", "data-layer": "alert-shell", d: ALERT_SHELL, transform: shell, style: ORIGIN, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "alert-mark", d: mark, opacity: markOpacity, stroke: markColor, strokeWidth: Math.min(strokeWidth, 1.7) }), _jsx(motion.circle, { "data-layer": "alert-dot", cx: "12", cy: dotY, r: Math.min(strokeWidth * 0.55, 1.1), opacity: dot, fill: markColor })] }) }));
22
+ });
@@ -0,0 +1,12 @@
1
+ export declare const alertStates: readonly ["idle", "warning", "critical", "resolved"];
2
+ export type AlertState = (typeof alertStates)[number];
3
+ export declare const ALERT_SHELL = "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3";
4
+ export declare function sampleAlert(weights: readonly number[], time?: number, alive?: number, entry?: number, criticalEntry?: number): {
5
+ body: string;
6
+ shell: string;
7
+ mark: string;
8
+ markOpacity: number;
9
+ dot: number;
10
+ dotY: number;
11
+ resolved: number;
12
+ };
@@ -0,0 +1,25 @@
1
+ import { behaviorBeat } from "../../shared/behavior-curves.js";
2
+ export const alertStates = ['idle', 'warning', 'critical', 'resolved'];
3
+ export const ALERT_SHELL = 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3';
4
+ export function sampleAlert(weights, time = 0, alive = 0, entry = 1, criticalEntry = entry) {
5
+ const clamp = (n) => Math.max(0, Math.min(1, n));
6
+ const [, warning, critical, resolved] = weights.map(clamp), life = clamp(alive);
7
+ const actionTime = clamp(entry) * 1.4;
8
+ const envelope = entry >= 1 ? 0 : behaviorBeat(actionTime, 0, 0.15, 0.8, 0.45);
9
+ const blink = Math.max((envelope * (1 - Math.cos(actionTime * Math.PI * 4))) / 2, (life * (1 - Math.cos((time * Math.PI * 2) / 1.4))) / 2);
10
+ const criticalTime = clamp(criticalEntry) * 1.4;
11
+ const criticalEnvelope = criticalEntry >= 1 ? 0 : behaviorBeat(criticalTime, 0, 0.15, 0.8, 0.45);
12
+ const vibration = critical *
13
+ ((1 - life) * criticalEnvelope * Math.sin(criticalTime * Math.PI * 10) +
14
+ life * Math.sin(time * Math.PI * 10));
15
+ return {
16
+ body: 'translate(12px, 12px) scale(0.8) translate(-12px, -12px)',
17
+ shell: `translate(12px, 13px) rotate(${vibration * 3.2}deg) translate(-12px, -13px)`,
18
+ // Corresponding three landmarks fold the upright stem into one interior check.
19
+ mark: `M${12 - 2.6 * resolved} ${9 + 5 * resolved}L${12 - 0.7 * resolved} ${11 + 4.9 * resolved}L${12 + 2.1 * resolved} ${13 - 0.1 * resolved}`,
20
+ markOpacity: 1 - warning * blink * 0.85,
21
+ dot: 1 - resolved,
22
+ dotY: 17,
23
+ resolved,
24
+ };
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { StatefulIconProps } from "../../shared/icon-types.js";
2
+ import { type BadgeCheckState } from "./badge-check-rig.js";
3
+ export { badgeCheckStates, type BadgeCheckState } from "./badge-check-rig.js";
4
+ export type BadgeCheckIconProps = StatefulIconProps<BadgeCheckState>;
5
+ export declare const BadgeCheckIcon: import("react").NamedExoticComponent<BadgeCheckIconProps>;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { memo, useCallback, useRef } from 'react';
4
+ import { motion, useTransform } from 'motion/react';
5
+ import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
6
+ import { useMotionPreference } from "../../shared/use-motion-preference.js";
7
+ import { BADGE_SHELL, sampleBadgeCheck } from "./badge-check-rig.js";
8
+ export { badgeCheckStates } from "./badge-check-rig.js";
9
+ const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
10
+ export const BadgeCheckIcon = memo(function BadgeCheckIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
11
+ const reduced = useMotionPreference(), element = useRef(null);
12
+ const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
13
+ const idle = usePoseWeight(state === 'idle', pace, reduced), checking = usePoseWeight(state === 'checking', pace, reduced), verified = usePoseWeight(state === 'verified', pace, reduced), rejected = usePoseWeight(state === 'rejected', pace, reduced);
14
+ const performs = alive && !reduced && state !== 'idle', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
15
+ // Mount an already verified value as a still; replay only on a later entry.
16
+ const first = useRef(state !== 'verified');
17
+ const canRestart = useCallback(() => {
18
+ const invisible = first.current || verified.get() < 0.001;
19
+ first.current = false;
20
+ return invisible;
21
+ }, [verified]);
22
+ const entry = useActionProgress(state === 'verified', pace, reduced, 0.9, canRestart);
23
+ const firstRejection = useRef(state !== 'rejected');
24
+ const canRestartRejection = useCallback(() => {
25
+ const invisible = firstRejection.current || rejected.get() < 0.001;
26
+ firstRejection.current = false;
27
+ return invisible;
28
+ }, [rejected]);
29
+ const rejectionEntry = useActionProgress(state === 'rejected', pace, reduced, 0.8, canRestartRejection);
30
+ const rig = useTransform(() => sampleBadgeCheck([idle.get(), checking.get(), verified.get(), rejected.get()], time.get(), life.get(), entry.get(), rejectionEntry.get()));
31
+ const body = useTransform(rig, (r) => r.body), mark = useTransform(rig, (r) => r.mark), markOpacity = useTransform(rig, (r) => r.markOpacity), markOffset = useTransform(rig, (r) => r.markOffset), reverse = useTransform(rig, (r) => r.reverse), cross = useTransform(rig, (r) => r.cross), progress = useTransform(rig, (r) => r.progress), progressOffset = useTransform(rig, (r) => r.progressOffset), shine = useTransform(rig, (r) => r.shine);
32
+ return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "badge-body", transform: body, style: ORIGIN, children: [_jsx("path", { "data-part": "shell", d: BADGE_SHELL, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "badge-progress", d: "M12 7.5a4.5 4.5 0 1 1 0 9a4.5 4.5 0 1 1 0-9", opacity: progress, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.8, 1.5), strokeDasharray: "7 21.274334", strokeDashoffset: progressOffset }), _jsx(motion.path, { "data-layer": "badge-mark", d: mark, opacity: markOpacity, stroke: color, strokeWidth: strokeWidth, strokeDasharray: "12 12", strokeDashoffset: markOffset }), _jsx(motion.path, { "data-layer": "badge-reverse", d: cross, opacity: reverse, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "badge-shine", d: "M7.2 6.3a2 2 0 0 1 1.7-.3", opacity: shine, stroke: accentColor, strokeWidth: strokeWidth * 0.65 })] }) }));
33
+ });
@@ -0,0 +1,14 @@
1
+ export declare const badgeCheckStates: readonly ["idle", "checking", "verified", "rejected"];
2
+ export type BadgeCheckState = (typeof badgeCheckStates)[number];
3
+ export declare const BADGE_SHELL = "M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z";
4
+ export declare function sampleBadgeCheck(weights: readonly number[], time?: number, alive?: number, entry?: number, rejectionEntry?: number): {
5
+ body: string;
6
+ mark: string;
7
+ markOpacity: number;
8
+ markOffset: number;
9
+ reverse: number;
10
+ cross: string;
11
+ progress: number;
12
+ progressOffset: number;
13
+ shine: number;
14
+ };
@@ -0,0 +1,36 @@
1
+ import { behaviorBeat } from "../../shared/behavior-curves.js";
2
+ export const badgeCheckStates = ['idle', 'checking', 'verified', 'rejected'];
3
+ export const BADGE_SHELL = 'M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z';
4
+ export function sampleBadgeCheck(weights, time = 0, alive = 0, entry = 1, rejectionEntry = 1) {
5
+ const clamp = (n) => Math.max(0, Math.min(1, n));
6
+ const smooth = (n) => {
7
+ const t = clamp(n);
8
+ return t * t * t * (t * (t * 6 - 15) + 10);
9
+ };
10
+ const [, checking, verified, rejected] = weights.map(clamp), life = clamp(alive);
11
+ const t = time % 7.2;
12
+ const stamp = Math.max(behaviorBeat(entry * 0.9, 0.3, 0.18, 0.06, 0.36), life * behaviorBeat(t, 0.5, 0.18, 0.08, 0.55));
13
+ const shine = behaviorBeat(t, 3.9, 0.45, 0.1, 0.7);
14
+ const refuse = life * rejected * behaviorBeat(t, 0.35, 0.35, 0.12, 0.7);
15
+ const draw = smooth(entry / 0.68);
16
+ const crossDraw = smooth((rejectionEntry - 0.1) / 0.72);
17
+ const cross = [
18
+ [8.5, 8.5],
19
+ [15.5, 8.5],
20
+ [15.5, 15.5],
21
+ [8.5, 15.5],
22
+ ]
23
+ .map(([x, y]) => `M${x} ${y}L${x + (12 - x) * crossDraw} ${y + (12 - y) * crossDraw}`)
24
+ .join('');
25
+ return {
26
+ body: `translate(12px, 12px) rotate(${-4 * refuse}deg) scale(${0.84 + verified * stamp * 0.027}) translate(-12px, -12px)`,
27
+ mark: `M${8 + 0.5 * rejected} ${12 - 3.5 * rejected}L${10.5 + 1.5 * rejected} ${14.5 - 2.5 * rejected}L${16 - 0.5 * rejected} ${9 + 6.5 * rejected}`,
28
+ markOpacity: (1 - checking) * (1 - rejected) * (1 - verified + verified * smooth(draw / 0.025)),
29
+ markOffset: verified * (1 - draw) * 12,
30
+ reverse: rejected * smooth(crossDraw / 0.04),
31
+ cross,
32
+ progress: checking,
33
+ progressOffset: -Math.PI * 9 * (time / 1.5) * life,
34
+ shine: verified * life * shine,
35
+ };
36
+ }
@@ -0,0 +1,5 @@
1
+ import type { StatefulIconProps } from "../../shared/icon-types.js";
2
+ import { type BellState } from "./bell-rig.js";
3
+ export { bellStates, type BellState } from "./bell-rig.js";
4
+ export type BellIconProps = StatefulIconProps<BellState>;
5
+ export declare const BellIcon: import("react").NamedExoticComponent<BellIconProps>;
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { memo, useId, useRef } from 'react';
4
+ import { motion, useTransform } from 'motion/react';
5
+ import { useMotionPreference } from "../../shared/use-motion-preference.js";
6
+ import { usePoseWeight, useBehaviorClock } from "../../shared/use-icon-motion.js";
7
+ import { sampleBell, BELL_SHELL, BELL_CLAPPER } from "./bell-rig.js";
8
+ export { bellStates } from "./bell-rig.js";
9
+ const ORIGIN = { originX: 0, originY: 0, transformBox: 'view-box' };
10
+ export const BellIcon = memo(function BellIcon({ state = 'idle', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
11
+ const reduced = useMotionPreference(), element = useRef(null), id = useId();
12
+ const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
13
+ const idle = usePoseWeight(state === 'idle', pace, reduced), ringing = usePoseWeight(state === 'ringing', pace, reduced);
14
+ const unread = usePoseWeight(state === 'unread', pace, reduced), muted = usePoseWeight(state === 'muted', pace, reduced);
15
+ const performs = alive && !reduced && (state === 'ringing' || state === 'unread'), life = usePoseWeight(performs, pace, reduced);
16
+ const time = useBehaviorClock(element, performs, pace, life);
17
+ const rig = useTransform(() => sampleBell([idle.get(), ringing.get(), unread.get(), muted.get()], time.get(), life.get()));
18
+ const body = useTransform(rig, (r) => r.body), clapper = useTransform(rig, (r) => r.clapper), cut = useTransform(rig, (r) => r.cut);
19
+ const ringOpacity = useTransform(rig, (r) => r.ringing), badge = useTransform(rig, (r) => r.badge), mute = useTransform(rig, (r) => r.muted);
20
+ const badgeRadius = useTransform(rig, (r) => r.badgeScale * 1.65);
21
+ return (_jsxs("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: [_jsx("defs", { children: _jsx("clipPath", { id: id, children: _jsx(motion.path, { "data-part": "mute-cut", d: cut }) }) }), _jsx(motion.path, { "data-layer": "bell-sound", d: "M2.5 6.5C2.7 5.2 3.2 4.2 4 3.2M20 3.2c.8 1 1.3 2 1.5 3.3", stroke: accentColor, strokeWidth: strokeWidth * 0.65, opacity: ringOpacity }), _jsxs(motion.g, { "data-layer": "bell-body", transform: body, style: ORIGIN, children: [_jsxs("g", { clipPath: `url(#${id})`, children: [_jsx("path", { "data-part": "shell", d: BELL_SHELL, stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-part": "clapper", d: BELL_CLAPPER, stroke: color, strokeWidth: strokeWidth, transform: clapper, style: ORIGIN })] }), _jsx(motion.path, { "data-layer": "bell-mute", d: "M3 3l18 18", stroke: color, strokeWidth: strokeWidth, opacity: mute })] }), _jsx(motion.circle, { "data-layer": "bell-badge", cx: "19", cy: "4", r: badgeRadius, fill: accentColor, opacity: badge })] }));
22
+ });
@@ -0,0 +1,14 @@
1
+ export declare const bellStates: readonly ["idle", "ringing", "unread", "muted"];
2
+ export type BellState = (typeof bellStates)[number];
3
+ export type BellWeights = readonly [number, number, number, number];
4
+ export declare const BELL_SHELL = "M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326";
5
+ export declare const BELL_CLAPPER = "M10.268 21a2 2 0 0 0 3.464 0";
6
+ export declare function sampleBell([, ringing, unread, muted]: BellWeights, time?: number, alive?: number): {
7
+ body: string;
8
+ clapper: string;
9
+ cut: string;
10
+ ringing: number;
11
+ badge: number;
12
+ muted: number;
13
+ badgeScale: number;
14
+ };
@@ -0,0 +1,21 @@
1
+ export const bellStates = ['idle', 'ringing', 'unread', 'muted'];
2
+ export const BELL_SHELL = 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326';
3
+ export const BELL_CLAPPER = 'M10.268 21a2 2 0 0 0 3.464 0';
4
+ const n = (v) => Number(v.toFixed(5));
5
+ export function sampleBell([, ringing, unread, muted], time = 0, alive = 0) {
6
+ const life = Math.max(0, Math.min(1, alive));
7
+ const ring = Math.max(0, behaviorBeat(time % 3.4, 0.12, 0.28, 0.9, 0.75));
8
+ const notify = Math.max(0, behaviorBeat(time % 4.8, 0.3, 0.4, 0.1, 0.8));
9
+ const angle = ringing * (7 * (1 - life) + life * 10 * Math.sin(time * 10.6) * ring), gap = muted * 1.5;
10
+ const clapperAngle = ringing * life * -13 * Math.sin(time * 10.6 - 0.55) * ring;
11
+ return {
12
+ body: `translate(12px, 12px) scale(.86) translate(-12px, -12px) translate(12px, 4px) rotate(${n(angle)}deg) translate(-12px, -4px)`,
13
+ clapper: `translate(12px, 17px) rotate(${n(clapperAngle)}deg) translate(-12px, -17px)`,
14
+ cut: `M${n(gap)} 0H24V${n(24 - gap)}ZM0 ${n(gap)}V24H${n(24 - gap)}Z`,
15
+ ringing: ringing * (0.4 + life * 0.35 * ring),
16
+ badge: unread,
17
+ muted,
18
+ badgeScale: 1 + life * unread * 0.14 * notify,
19
+ };
20
+ }
21
+ import { behaviorBeat } from "../../shared/behavior-curves.js";
@@ -0,0 +1,5 @@
1
+ import type { StatefulIconProps } from "../../shared/icon-types.js";
2
+ import { type BulbState } from "./bulb-rig.js";
3
+ export { bulbStates, type BulbState } from "./bulb-rig.js";
4
+ export type BulbIconProps = StatefulIconProps<BulbState>;
5
+ export declare const BulbIcon: import("react").NamedExoticComponent<BulbIconProps>;
@@ -0,0 +1,25 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { memo, useCallback, useRef } from 'react';
4
+ import { motion, useTransform } from 'motion/react';
5
+ import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
6
+ import { useMotionPreference } from "../../shared/use-motion-preference.js";
7
+ import { BULB_SHELL, sampleBulb } from "./bulb-rig.js";
8
+ export { bulbStates } from "./bulb-rig.js";
9
+ const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
10
+ export const BulbIcon = memo(function BulbIcon({ state = 'off', size = 24, color = 'currentColor', accentColor = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
11
+ const reduced = useMotionPreference(), element = useRef(null);
12
+ const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
13
+ const off = usePoseWeight(state === 'off', pace, reduced), thinking = usePoseWeight(state === 'thinking', pace, reduced), on = usePoseWeight(state === 'on', pace, reduced), fault = usePoseWeight(state === 'fault', pace, reduced);
14
+ const performs = alive && !reduced && state !== 'off', life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
15
+ const first = useRef(state !== 'on');
16
+ const canRestart = useCallback(() => {
17
+ const invisible = first.current || on.get() < 0.001;
18
+ first.current = false;
19
+ return invisible;
20
+ }, [on]);
21
+ const entry = useActionProgress(state === 'on', pace, reduced, 1, canRestart);
22
+ const rig = useTransform(() => sampleBulb([off.get(), thinking.get(), on.get(), fault.get()], time.get(), life.get(), entry.get()));
23
+ const body = useTransform(rig, (r) => r.body), filament = useTransform(rig, (r) => r.filament), filamentOpacity = useTransform(rig, (r) => r.filamentOpacity), light = useTransform(rig, (r) => r.light), rays = useTransform(rig, (r) => r.rays), failure = useTransform(rig, (r) => r.fault), shine = useTransform(rig, (r) => r.shine);
24
+ return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block', overflow: 'visible' }, children: _jsxs(motion.g, { "data-layer": "bulb-body", transform: body, style: ORIGIN, children: [_jsx(motion.path, { "data-layer": "bulb-rays", d: rays, opacity: light, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx("path", { "data-part": "shell", d: BULB_SHELL, stroke: color, strokeWidth: strokeWidth }), _jsx("path", { "data-part": "base", d: "M9 18h6M10 22h4", stroke: color, strokeWidth: strokeWidth }), _jsx(motion.path, { "data-layer": "bulb-filament", d: filament, opacity: filamentOpacity, stroke: accentColor, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx(motion.path, { "data-layer": "bulb-fault", d: "m10 8 4 4m0-4-4 4", opacity: failure, stroke: color, strokeWidth: Math.min(strokeWidth * 0.75, 1.4) }), _jsx(motion.path, { "data-layer": "bulb-shine", d: "M8.2 7.2a4 4 0 0 1 1.5-2.4", opacity: shine, stroke: accentColor, strokeWidth: strokeWidth * 0.6 })] }) }));
25
+ });
@@ -0,0 +1,13 @@
1
+ export declare const bulbStates: readonly ["off", "thinking", "on", "fault"];
2
+ export type BulbState = (typeof bulbStates)[number];
3
+ /** Lucide lightbulb contour. Rays and filament are original supporting details. */
4
+ export declare const BULB_SHELL = "M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5";
5
+ export declare function sampleBulb(weights: readonly number[], time?: number, alive?: number, entry?: number): {
6
+ body: string;
7
+ filament: string;
8
+ light: number;
9
+ filamentOpacity: number;
10
+ rays: string;
11
+ fault: number;
12
+ shine: number;
13
+ };
@@ -0,0 +1,37 @@
1
+ import { behaviorBeat } from "../../shared/behavior-curves.js";
2
+ export const bulbStates = ['off', 'thinking', 'on', 'fault'];
3
+ /** Lucide lightbulb contour. Rays and filament are original supporting details. */
4
+ export const BULB_SHELL = 'M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5';
5
+ export function sampleBulb(weights, time = 0, alive = 0, entry = 1) {
6
+ const clamp = (n) => Math.max(0, Math.min(1, n));
7
+ const [, thinking, on, fault] = weights.map(clamp), life = clamp(alive);
8
+ const t = time % 8;
9
+ // A tentative filament warms twice, then considers; never falsely signals ON.
10
+ const warm = behaviorBeat(t, 0.2, 0.6, 0.1, 0.55) +
11
+ 0.65 * behaviorBeat(t, 2.05, 0.3, 0.15, 0.6) +
12
+ 0.8 * behaviorBeat(t, 4.9, 0.7, 0.25, 0.7);
13
+ const glint = behaviorBeat(t, 0.8, 0.35, 0.1, 0.9) + 0.65 * behaviorBeat(t, 5, 0.35, 0.1, 0.8);
14
+ const bloom = behaviorBeat(clamp(entry), 0.08, 0.23, 0.12, 0.5);
15
+ const emission = clamp(Math.max(bloom, life * glint));
16
+ // Five rays grow outwards together; the invisible gap separates each repeat.
17
+ const reach = 0.2 + emission * 0.8;
18
+ const rays = [Math.PI, Math.PI * 1.25, Math.PI * 1.5, Math.PI * 1.75, 0]
19
+ .map((angle) => {
20
+ const radius = 9.4 + emission * 0.45;
21
+ const x = 12 + Math.cos(angle) * radius;
22
+ const y = 8 + Math.sin(angle) * radius;
23
+ return `M${x} ${y}L${x + Math.cos(angle) * reach} ${y + Math.sin(angle) * reach}`;
24
+ })
25
+ .join('');
26
+ const retry = behaviorBeat(t, 0.35, 0.25, 0.1, 0.5) - 0.65 * behaviorBeat(t, 3.8, 0.3, 0.1, 0.6);
27
+ const scale = 0.78 + on * 0.015 + thinking * life * warm * 0.014;
28
+ return {
29
+ body: `translate(12px, 18px) rotate(${fault * life * retry * -5}deg) scale(${scale}) translate(-12px, -18px)`,
30
+ filament: 'M10 10.5L12 12.5L14 10.5M12 12.5V15',
31
+ light: on * emission,
32
+ filamentOpacity: thinking * (0.3 + life * warm * 0.65) + on,
33
+ rays,
34
+ fault,
35
+ shine: on * emission,
36
+ };
37
+ }
@@ -0,0 +1,5 @@
1
+ import type { StatefulIconProps } from "../../shared/icon-types.js";
2
+ import { type CheckState } from "./check-rig.js";
3
+ export { checkStates, type CheckState } from "./check-rig.js";
4
+ export type CheckIconProps = StatefulIconProps<CheckState>;
5
+ export declare const CheckIcon: import("react").NamedExoticComponent<CheckIconProps>;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { memo, useRef, useCallback } from 'react';
4
+ import { motion, useTransform } from 'motion/react';
5
+ import { usePoseWeight, useBehaviorClock, useActionProgress } from "../../shared/use-icon-motion.js";
6
+ import { useMotionPreference } from "../../shared/use-motion-preference.js";
7
+ import { sampleCheck } from "./check-rig.js";
8
+ export { checkStates } from "./check-rig.js";
9
+ const ORIGIN = { originX: '0px', originY: '0px', transformBox: 'view-box' };
10
+ export const CheckIcon = memo(function CheckIcon({ state = 'checked', size = 24, color = 'currentColor', strokeWidth = 1.65, speed = 1, alive = false, className, }) {
11
+ const reduced = useMotionPreference(), element = useRef(null);
12
+ const pace = Number.isFinite(speed) ? Math.min(4, Math.max(0.1, speed)) : 1;
13
+ const w0 = usePoseWeight(state === 'checked', pace, reduced);
14
+ const w1 = usePoseWeight(state === 'checking', pace, reduced);
15
+ const w2 = usePoseWeight(state === 'unchecked', pace, reduced);
16
+ const performs = alive && !reduced && state === 'checking';
17
+ const life = usePoseWeight(performs, pace, reduced), time = useBehaviorClock(element, performs, pace, life);
18
+ const can0 = useCallback(() => w1.get() < 0.001, [w1]);
19
+ const e0 = useActionProgress(state === 'checking', pace, reduced, 1.1, can0);
20
+ const can1 = useCallback(() => w0.get() < 0.001, [w0]);
21
+ const e1 = useActionProgress(state === 'checked', pace, reduced, 0.85, can1);
22
+ const rig = useTransform(() => sampleCheck([w0.get(), w1.get(), w2.get()], time.get(), life.get(), e0.get(), e1.get()));
23
+ const mark = useTransform(rig, (r) => r.mark);
24
+ const confirm = useTransform(rig, (r) => r.confirm);
25
+ return (_jsx("svg", { ref: element, className: className, width: size, height: size, viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", fill: "none", stroke: color, strokeWidth: strokeWidth, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", pointerEvents: "none", style: { display: 'block' }, children: _jsx("g", { "data-layer": "check-body", transform: "translate(12 12) scale(.84) translate(-12 -12)", children: _jsx(motion.path, { "data-layer": "check-mark", "data-part": "mark", d: mark, transform: confirm, style: ORIGIN }) }) }));
26
+ });
@@ -0,0 +1,6 @@
1
+ export declare const checkStates: readonly ["checked", "checking", "unchecked"];
2
+ export type CheckState = (typeof checkStates)[number];
3
+ export declare function sampleCheck(weights: readonly number[], time?: number, alive?: number, entry?: number, successEntry?: number): {
4
+ mark: string;
5
+ confirm: string;
6
+ };
@@ -0,0 +1,38 @@
1
+ import { behaviorBeat } from "../../shared/behavior-curves.js";
2
+ const clamp = (n) => Math.max(0, Math.min(1, Number.isFinite(n) ? n : 0));
3
+ const normalized = (values) => {
4
+ const w = values.map(clamp), sum = w.reduce((a, b) => a + b, 0) || 1;
5
+ return w.map((v) => v / sum);
6
+ };
7
+ export const checkStates = ['checked', 'checking', 'unchecked'];
8
+ export function sampleCheck(weights, time = 0, alive = 0, entry = 1, successEntry = 1) {
9
+ const [checked, checking] = normalized(weights), life = clamp(alive);
10
+ const entryWave = behaviorBeat(clamp(entry) * 1.1, 0.08, 0.22, 0.05, 0.65);
11
+ const wave = checking * ((1 - life) * entryWave * 1.2 + life * Math.sin(time * 4.2) * 1.4);
12
+ const x0 = 8 - checked * 4 - checking * 2, x1 = 12 - checked * 3, x2 = 16 + checked * 4 + checking * 2;
13
+ const y0 = 12 + checking * life * Math.sin(time * 4.2) * 0.35, crest = checking * 2.6 + wave, knee = 12 + checked * 5;
14
+ const pop = checked * behaviorBeat(clamp(successEntry) * 0.85, 0.12, 0.15, 0.03, 0.48);
15
+ return {
16
+ mark: 'M' +
17
+ x0 +
18
+ ' ' +
19
+ y0 +
20
+ 'Q' +
21
+ (x0 + x1) / 2 +
22
+ ' ' +
23
+ (12 + checked * 2.5 - crest) +
24
+ ' ' +
25
+ x1 +
26
+ ' ' +
27
+ knee +
28
+ 'Q' +
29
+ (x1 + x2) / 2 +
30
+ ' ' +
31
+ (12 - checked * 0.5 + crest) +
32
+ ' ' +
33
+ x2 +
34
+ ' ' +
35
+ (12 - checked * 6),
36
+ confirm: 'translate(9px, 17px) scale(' + (1 + pop * 0.07) + ') translate(-9px, -17px)',
37
+ };
38
+ }