diff --git a/apps/web/vibes/soul/docs/gift-card.mdx b/apps/web/vibes/soul/docs/gift-card.mdx
new file mode 100644
index 000000000..8577385a4
--- /dev/null
+++ b/apps/web/vibes/soul/docs/gift-card.mdx
@@ -0,0 +1,4 @@
+---
+title: Gift Card
+preview: gift-card-example
+---
diff --git a/apps/web/vibes/soul/examples.ts b/apps/web/vibes/soul/examples.ts
index a11cc16a6..87485600f 100644
--- a/apps/web/vibes/soul/examples.ts
+++ b/apps/web/vibes/soul/examples.ts
@@ -1156,5 +1156,12 @@ export const examples = [
files: ['examples/primitives/scroll-area/luxury.tsx'],
component: lazy(() => import('./examples/primitives/scroll-area/luxury')),
},
+ {
+ name: 'gift-card-example',
+ dependencies: [],
+ registryDependencies: ['gift-card'],
+ files: ['examples/primitives/gift-card/index.tsx'],
+ component: lazy(() => import('./examples/primitives/gift-card')),
+ },
// PLOP: Append new component here
] satisfies Components;
diff --git a/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx b/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx
new file mode 100644
index 000000000..8b419b475
--- /dev/null
+++ b/apps/web/vibes/soul/examples/primitives/gift-card/index.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import { GiftCard } from '@/vibes/soul/primitives/gift-card';
+
+export default function Preview() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/vibes/soul/navigation.ts b/apps/web/vibes/soul/navigation.ts
index 6cb2aa6e4..83ea9b19c 100644
--- a/apps/web/vibes/soul/navigation.ts
+++ b/apps/web/vibes/soul/navigation.ts
@@ -90,6 +90,7 @@ const primitivePages: [Page, ...Page[]] = [
component: 'dropdown-menu',
},
{ title: 'Favorite', slug: 'favorite', file: 'docs/favorite.mdx', component: 'favorite' },
+ { title: 'Gift Card', slug: 'gift-card', file: 'docs/gift-card.mdx', component: 'gift-card' },
{ title: 'Icon', slug: 'icon', file: 'docs/icon.mdx', component: 'icon' },
{
title: 'Logo',
diff --git a/apps/web/vibes/soul/primitives/gift-card/index.tsx b/apps/web/vibes/soul/primitives/gift-card/index.tsx
new file mode 100644
index 000000000..6c456e985
--- /dev/null
+++ b/apps/web/vibes/soul/primitives/gift-card/index.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import { useMemo, useRef, useState } from 'react';
+
+export interface GiftCardProps {
+ maxTilt?: number;
+ amount: string | null;
+ logo: string;
+ loading?: boolean;
+}
+
+export function GiftCard({
+ maxTilt = 15,
+ amount = '$25.00',
+ logo = 'Planted',
+ loading = false,
+}: GiftCardProps) {
+ const cardRef = useRef(null);
+
+ const [mouse, setMouse] = useState({ x: 0, y: 0, xPct: 50, yPct: 50 });
+ const [tilt, setTilt] = useState({ rotateX: 0, rotateY: 0, z: 0 });
+
+ // Derived background positions for holo layers
+ const bgX = useMemo(() => 40 + 0.2 * mouse.xPct, [mouse.xPct]);
+ const bgY = useMemo(() => 40 + 0.2 * mouse.yPct, [mouse.yPct]);
+
+ function handleMouseMove(e: React.MouseEvent) {
+ const target = e.currentTarget;
+ const rect = target.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const xPct = (x / rect.width) * 100;
+ const yPct = (y / rect.height) * 100;
+ setMouse({ x, y, xPct, yPct });
+ }
+
+ function handlePointerMove(e: React.PointerEvent) {
+ const el = cardRef.current;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const y = e.clientY - rect.top;
+ const xPercent = x / rect.width;
+ const yPercent = y / rect.height;
+ setTilt({
+ rotateX: maxTilt * (0.5 - yPercent),
+ rotateY: maxTilt * (xPercent - 0.5),
+ z: -10,
+ });
+ }
+
+ function handlePointerEnter() {
+ setTilt((t) => ({ ...t, z: -10 }));
+ }
+
+ function handlePointerLeave() {
+ setTilt({ rotateX: 0, rotateY: 0, z: 0 });
+ setMouse({ x: 0, y: 0, xPct: 50, yPct: 50 });
+ }
+
+ return (
+
+
+ {/* Left column */}
+
+
+ {logo}
+
+
+ Gift card
+
+
+
+ {/* Right column */}
+
+ {amount !== null && !loading && (
+
+ {amount}
+
+ )}
+ {loading &&
}
+
+
+ {/* Cursor mask */}
+
+
+ {/* Holographic overlay */}
+
+
+
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 31bf143b2..186b00f4e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -100,7 +100,7 @@ importers:
version: 3.1.0(react@19.0.0)(types-react@19.0.0-rc.1)
'@nextui-org/date-input':
specifier: ^2.1.3
- version: 2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ version: 2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-accordion':
specifier: 1.2.1
version: 1.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(types-react-dom@19.0.0-rc.1)(types-react@19.0.0-rc.1)
@@ -3915,8 +3915,8 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
- framer-motion@12.4.7:
- resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==}
+ framer-motion@12.23.22:
+ resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@@ -4935,11 +4935,11 @@ packages:
mlly@1.7.4:
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
- motion-dom@12.4.10:
- resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==}
+ motion-dom@12.23.21:
+ resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==}
- motion-utils@12.4.10:
- resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==}
+ motion-utils@12.23.6:
+ resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
@@ -7486,13 +7486,13 @@ snapshots:
'@next/swc-win32-x64-msvc@15.1.7':
optional: true
- '@nextui-org/date-input@2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@nextui-org/date-input@2.3.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@internationalized/date': 3.6.0
- '@nextui-org/form': 2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@nextui-org/form': 2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@nextui-org/react-utils': 2.1.3(react@19.0.0)
'@nextui-org/shared-utils': 2.1.2
- '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@nextui-org/theme': 2.4.5(tailwindcss@4.1.5)
'@react-aria/datepicker': 3.12.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@react-aria/i18n': 3.12.4(react@19.0.0)
@@ -7503,11 +7503,11 @@ snapshots:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
- '@nextui-org/form@2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@nextui-org/form@2.1.8(@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@nextui-org/react-utils': 2.1.3(react@19.0.0)
'@nextui-org/shared-utils': 2.1.2
- '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@nextui-org/system': 2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@nextui-org/theme': 2.4.5(tailwindcss@4.1.5)
'@react-aria/utils': 3.26.0(react@19.0.0)
'@react-stately/form': 3.1.0(react@19.0.0)
@@ -7535,7 +7535,7 @@ snapshots:
clsx: 1.2.1
react: 19.0.0
- '@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ '@nextui-org/system@2.4.6(@nextui-org/theme@2.4.5(tailwindcss@4.1.5))(framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@internationalized/date': 3.6.0
'@nextui-org/react-utils': 2.1.3(react@19.0.0)
@@ -7545,7 +7545,7 @@ snapshots:
'@react-aria/utils': 3.26.0(react@19.0.0)
'@react-stately/utils': 3.10.5(react@19.0.0)
'@react-types/datepicker': 3.9.0(react@19.0.0)
- framer-motion: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ framer-motion: 12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
transitivePeerDependencies:
@@ -10471,10 +10471,10 @@ snapshots:
fraction.js@4.3.7: {}
- framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ framer-motion@12.23.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
- motion-dom: 12.4.10
- motion-utils: 12.4.10
+ motion-dom: 12.23.21
+ motion-utils: 12.23.6
tslib: 2.8.1
optionalDependencies:
react: 19.0.0
@@ -11821,11 +11821,11 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
- motion-dom@12.4.10:
+ motion-dom@12.23.21:
dependencies:
- motion-utils: 12.4.10
+ motion-utils: 12.23.6
- motion-utils@12.4.10: {}
+ motion-utils@12.23.6: {}
mri@1.2.0: {}