pin3D
A Effects 3D block/component from Aceternity UI — clean-room reimplemented for Domphy (see methodology). Call pin3D() with no arguments for a working demo, or edit the code below live.
Implementation notes
Full behavior implemented: click-through <a> card, a persistent radar-ping ring on the base dot (reuses this package's pulsatingButton.ts box-shadow/color-mix(currentColor) keyframe technique verbatim, independent of hover), and a hover-driven beam+title-pill pop-up written via onMouseEnter/onMouseLeave directly setting two State<MotionKeyframe> (no imperative DOM listeners needed). One noted approximation: the spec's 'spring physics... slightly overshoots' is approximated with a cubic-bezier(0.34,1.56,0.64,1) overshoot easing curve rather than a true physical spring simulator, since Domphy's motion() wraps the Web Animations API (CSS easing), not a spring engine — documented in the file header rather than silently treated as identical.
Status: ported · Reference: Aceternity UI original
// Aceternity UI "3D Pin" — clean-room reimplementation from the public
// behavior/visual spec only (no upstream source viewed or copied). A
// dark card that acts as a click-through link, with a "map pin" motif
// anchored at its bottom edge: a small dot with a continuously looping
// radar-ping ring (idle, independent of hover) that, on hover, grows a
// glowing vertical beam topped by a title pill rising up out of the card.
//
// Despite the "3D" name in the reference component's own title, the depth
// here reads through layered glow/gradient and a vertical pop-up motion
// rather than literal `rotateX`/`rotateY` camera work (per the task's own
// researchNote) — this port leans into that: no `perspective`/`rotate*`
// transforms anywhere, just glow, gradient, and `height`/`opacity`/`y`
// keyframes.
//
// The idle radar-ping ring reuses this package's own `pulsatingButton.ts`
// technique verbatim: a `box-shadow` keyframe expanding via `color-mix(in
// srgb, currentColor N%, transparent)`, so the ring's color always tracks
// the dot's own `color` with no extra reactive plumbing. The hover-driven
// beam/pill reveal is plain `motion()` (Web Animations API) driven by two
// `State<MotionKeyframe>`s written directly from `onMouseEnter`/
// `onMouseLeave` — no imperative DOM listeners needed since Domphy elements
// accept native event-handler props directly (the same idiom `tooltip.ts`
// uses for its own show/hide). A `cubic-bezier` back-ease stands in for the
// spec's "spring physics... slightly overshoots before settling" — Domphy's
// `motion()` wraps the Web Animations API (CSS easing curves), not a
// physical spring simulator, so an overshoot easing curve is the closest
// available approximation; noted here rather than silently treated as
// identical.
import type { DomphyElement, Listener, StyleObject } from "@domphy/core";
import { hashString, toState } from "@domphy/core";
import { type MotionKeyframe, heading, motion, paragraph, small } from "@domphy/ui";
import { type ThemeColor, themeColor, themeSpacing } from "@domphy/theme";
export interface Pin3DProps {
/** Card content rendered above the pin motif. Defaults to a short demo panel. */
children?: DomphyElement | DomphyElement[];
/** Text shown in the popup pill on hover. Defaults to `"View project"`. */
title?: string;
/** Click-through destination. Defaults to `"#"`. */
href?: string;
/** Theme color family for the pin dot, ping ring, and beam gradient's start stop. Defaults to `"info"`. */
color?: ThemeColor;
/** Theme color family for the beam gradient's end stop (the "cycles through cool tones" accent). Defaults to `"secondary"`. */
accentColor?: ThemeColor;
/** Base dot diameter, in `themeSpacing` units. Defaults to `2.5`. */
pinSize?: number;
/** How tall the beam grows on hover, in `themeSpacing` units. Defaults to `16`. */
beamHeight?: number;
/** Extra class name merged onto the outer link's native `class` attribute. */
containerClassName?: string;
/** Extra class name merged onto the inner content wrapper's native `class` attribute. */
contentClassName?: string;
/** Passthrough style merged onto the card surface. */
style?: StyleObject;
}
const HIDDEN_BEAM_FRAME: MotionKeyframe = { height: "0em", opacity: 0 };
const HIDDEN_PILL_FRAME: MotionKeyframe = { opacity: 0, y: 8, scale: 0.85 };
const VISIBLE_PILL_FRAME: MotionKeyframe = { opacity: 1, y: 0, scale: 1 };
// A cubic-bezier with a control point past 1.0 overshoots then settles —
// the nearest WAAPI-easing approximation of a spring's bounce.
const SPRING_EASING = "cubic-bezier(0.34, 1.56, 0.64, 1)";
function defaultContent(): DomphyElement[] {
return [
{ h3: "3D Pin", $: [heading()] } as DomphyElement,
{
p: "Hover to raise a glowing pin above this card's pulsing radar base.",
$: [paragraph({ color: "neutral" })],
} as DomphyElement,
];
}
/**
* A dark card acting as a click-through link, with a map-pin motif anchored
* at its bottom edge: a continuously pulsing radar-ping base dot that, on
* hover, grows a glowing beam topped by a title pill rising out of the card.
* Call with no arguments for a working demo card.
*/
function pin3D(props: Pin3DProps = {}): DomphyElement<"a"> {
const content = props.children
? Array.isArray(props.children)
? props.children
: [props.children]
: defaultContent();
const title = props.title ?? "View project";
const href = props.href ?? "#";
const color = props.color ?? "info";
const accentColor = props.accentColor ?? "secondary";
const pinSizeUnits = props.pinSize ?? 2.5;
const beamHeightUnits = Math.max(4, props.beamHeight ?? 16);
const beamFrame = toState<MotionKeyframe>(HIDDEN_BEAM_FRAME);
const pillFrame = toState<MotionKeyframe>(HIDDEN_PILL_FRAME);
const visibleBeamFrame: MotionKeyframe = { height: `${beamHeightUnits / 4}em`, opacity: 1 };
const pingAnimationName = `pin3d-ping-${hashString(JSON.stringify({ pinSizeUnits, color }))}`;
const pingKeyframes = {
"0%": { boxShadow: "0 0 0 0 color-mix(in srgb, currentColor 45%, transparent)" },
"100%": {
boxShadow: `0 0 0 ${themeSpacing(pinSizeUnits * 1.8)} color-mix(in srgb, currentColor 0%, transparent)`,
},
};
const baseDot: DomphyElement<"span"> = {
span: null,
ariaHidden: "true",
style: {
position: "absolute",
insetInlineStart: "50%",
insetBlockEnd: themeSpacing(-pinSizeUnits / 2),
width: themeSpacing(pinSizeUnits),
height: themeSpacing(pinSizeUnits),
borderRadius: "50%",
transform: "translateX(-50%)",
zIndex: 2,
color: (listener: Listener) => themeColor(listener, "shift-9", color),
// `backgroundColor` uses tone "inherit" (not a fixed shift) with the
// color family overridden to `color` — the same "solid accent fill"
// convention this package's `@domphy/ui` `avatar()`/`button()` patches
// use for their own colored surfaces, so the dot still reads as a
// vivid solid color while following the surface-tone contract.
backgroundColor: (listener: Listener) => themeColor(listener, "inherit", color),
animation: `${pingAnimationName} 1.8s cubic-bezier(0, 0, 0.2, 1) infinite`,
[`@keyframes ${pingAnimationName}`]: pingKeyframes,
} as StyleObject,
} as DomphyElement<"span">;
const beamElement: DomphyElement<"span"> = {
span: null,
ariaHidden: "true",
// Decorative gradient beam with no text of its own — exempt from the
// missing-color contract, matching this package's other purely
// decorative glow/accent elements (e.g. `borderBeam.ts`'s orbit rect).
_doctorDisable: "missing-color",
style: {
position: "absolute",
insetInlineStart: "50%",
insetBlockEnd: themeSpacing(pinSizeUnits / 2),
width: themeSpacing(0.375),
borderRadius: themeSpacing(999),
transform: "translateX(-50%)",
transformOrigin: "bottom center",
pointerEvents: "none",
zIndex: 1,
background: (listener: Listener) =>
`linear-gradient(0deg, ${themeColor(listener, "shift-9", color)}, ${themeColor(listener, "shift-9", accentColor)})`,
boxShadow: (listener: Listener) => `0 0 ${themeSpacing(3)} ${themeColor(listener, "shift-9", color)}`,
} as StyleObject,
$: [
motion({
initial: HIDDEN_BEAM_FRAME,
animate: beamFrame,
transition: { duration: 380, easing: SPRING_EASING },
}),
],
} as DomphyElement<"span">;
const pillElement: DomphyElement<"span"> = {
span: [{ small: title, $: [small({ color: "neutral" })] } as DomphyElement],
dataTone: "shift-17",
style: {
position: "absolute",
insetInlineStart: "50%",
insetBlockEnd: themeSpacing(pinSizeUnits / 2 + beamHeightUnits + 1),
transform: "translateX(-50%)",
whiteSpace: "nowrap",
paddingBlock: themeSpacing(1),
paddingInline: themeSpacing(3),
borderRadius: themeSpacing(999),
pointerEvents: "none",
zIndex: 2,
backgroundColor: (listener: Listener) => themeColor(listener),
color: (listener: Listener) => themeColor(listener, "shift-9"),
boxShadow: (listener: Listener) => `0 0 ${themeSpacing(4)} ${themeColor(listener, "shift-9", color)}`,
} as StyleObject,
$: [
motion({
initial: HIDDEN_PILL_FRAME,
animate: pillFrame,
transition: { duration: 380, delay: 60, easing: SPRING_EASING },
}),
],
} as DomphyElement<"span">;
const contentWrapper: DomphyElement<"div"> = {
div: content,
class: props.contentClassName,
style: { position: "relative", zIndex: 1 } as StyleObject,
};
const card: DomphyElement<"div"> = {
div: [contentWrapper, baseDot, beamElement, pillElement],
dataTone: "shift-15",
style: {
position: "relative",
overflow: "visible",
borderRadius: themeSpacing(4),
padding: themeSpacing(6),
paddingBlockEnd: themeSpacing(8),
backgroundColor: (listener: Listener) => themeColor(listener, "inherit"),
color: (listener: Listener) => themeColor(listener, "shift-9"),
outline: (listener: Listener) => `1px solid ${themeColor(listener, "shift-3")}`,
outlineOffset: "-1px",
...(props.style ?? {}),
} as StyleObject,
};
return {
a: [card],
href,
class: props.containerClassName,
style: {
display: "block",
width: "fit-content",
textDecoration: () => "none",
} as StyleObject,
onMouseEnter: () => {
beamFrame.set(visibleBeamFrame);
pillFrame.set(VISIBLE_PILL_FRAME);
},
onMouseLeave: () => {
beamFrame.set(HIDDEN_BEAM_FRAME);
pillFrame.set(HIDDEN_PILL_FRAME);
},
} as DomphyElement<"a">;
}
export { pin3D };