Media & effects
Hanging Amulet
Pointer-driven hanging amulet with layered spring physics
detent-hanging-amulet
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 4
- Wave
- phase-4-pointer
- Group
- Media & effects
5.2 KB gzip for the class module · inlines the shared spring
Variants
Move the pointer across an amulet: the cord, the metallic cap and the disc each swing by a different amount from the
same normalized pointer offset (cord ±9°, cap ±7°, disc ±5°) on top of the
swing wrapper's max-rotation. The springs are the pinned
stiffness-x/stiffness-y/damping values, so leaving an amulet eases it back to
its rest pose and the loop stops. Each amulet carries one role="img" name; the painted SVG is
decorative.
With prefers-reduced-motion: reduce no pointer listener is wired at all and every amulet rests in its
static pose. Without a PointerEvent constructor the component degrades to the same rest pose.
Recorded states
reduced motionrecorded state; no settercompletedrecorded state; no setter
Usage
Import
import '/components/hanging-amulet/define.js';
// or register it yourself
import { DetentHangingAmulet } from '/components/hanging-amulet/hanging-amulet.js';
Attributes
captiondampingmax-rotationstiffness-xstiffness-y
Properties
No properties are recorded for this component.
Slots
No slots are recorded for this component.
Events
No events are recorded for this component.
Example
Runnable example: site/examples/hanging-amulet.ts. The snippet below is generated from the catalogued attributes.
<detent-hanging-amulet
caption
damping="9"
max-rotation="13"
stiffness-x="55"
stiffness-y="45"
></detent-hanging-amulet>
Source
The pinned upstream source for this row:
src/components/ui/EvilEye.tsxsha256 63d4592bc72a…
Native files
The native module and its artifacts:
src/components/hanging-amulet/hanging-amulet.tssrc/components/hanging-amulet/styles.csssrc/components/hanging-amulet/define.tsdocs/components/hanging-amulet.mdsite/examples/hanging-amulet.tstests/browser/hanging-amulet.spec.tsevidence/components/hanging-amulet.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
caption | caption | string | undefined | Kebab-case attribute; camelCase property. |
damping | damping | number | 9 | Kebab-case attribute; camelCase property. |
max-rotation | maxRotation | number | 13 | Kebab-case attribute; camelCase property. |
stiffness-x | stiffnessX | number | 55 | Kebab-case attribute; camelCase property. |
stiffness-y | stiffnessY | number | 45 | Kebab-case attribute; camelCase property. |
Properties
No property-only values are recorded for this component.
Events
No custom events are recorded for this component.
Slots
No slots are recorded for this component.
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The upstream duplicate accessible name (aria-label plus an sr-only span) collapses to the single host aria-label.
- The hanging line uses --detent-hanging-amulet-ceiling (#6b7280) instead of neutral-300/500 (1.4:1 on white), an accessibility correction.
- Gradient ids are host-local (detent-hanging-amulet-<n>-...) so two instances cannot collide.
What it does
A hanging amulet painted as one SVG whose cord, cap and disc swing with layered spring physics as the pointer moves across the element.
Upstream behaviour
- Pointer position drives a normalized MotionValue pipeline (no React renders per frame)
- Layered secondary motion — braided cord, metallic cap, and amulet disc each swing independently
- Painted SVG with radial gradients for the navy disc, sky-blue ring, and the signature pupil glint
- Configurable spring stiffness, damping, and max rotation for tuning the physical feel
- Geometry-aware normalization that adapts to the actual container size
- Respects prefers-reduced-motion with a clean static rest pose
- Graceful degradation when pointer events are unavailable
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The upstream duplicate accessible name (aria-label plus an sr-only span) collapses to the single host aria-label.
- The hanging line uses --detent-hanging-amulet-ceiling (#6b7280) instead of neutral-300/500 (1.4:1 on white), an accessibility correction.
- Gradient ids are host-local (detent-hanging-amulet-<n>-...) so two instances cannot collide.