Skip to content

Commit fc16de7

Browse files
jarvisclaude
authored andcommitted
feat: add SplitText hero animation and MagneticButton CTAs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9eb9ab6 commit fc16de7

3 files changed

Lines changed: 155 additions & 30 deletions

File tree

app/page.tsx

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { PayNow } from '@/components/paynow'
77
import EmailCapture from '@/components/EmailCapture'
88
import { SocialShare } from '@/components/SocialShare'
99
import { EcosystemFooter } from '@/components/EcosystemFooter'
10+
import { SplitText } from '@/components/SplitText'
11+
import { MagneticButton } from '@/components/MagneticButton'
1012

1113
// ── Example CLAUDE.md for "Try with example" ─────────────────────────
1214

@@ -259,15 +261,17 @@ export default function Home() {
259261
Security Scanner for AI Agent Configs
260262
</motion.div>
261263

262-
<motion.h1
263-
initial={{ opacity: 0, y: 20 }}
264-
animate={{ opacity: 1, y: 0 }}
264+
<motion.div
265+
initial={{ opacity: 0 }}
266+
animate={{ opacity: 1 }}
265267
transition={{ delay: 0.1 }}
266-
className="mx-auto mb-6 max-w-4xl text-3xl font-bold leading-tight tracking-tight text-white sm:text-4xl md:text-5xl"
268+
className="mx-auto mb-6 max-w-4xl"
267269
>
268-
Your AI agent has more access than your junior dev.{' '}
269-
<span className="text-accent">Have you audited it?</span>
270-
</motion.h1>
270+
<h1 className="text-3xl font-bold leading-tight tracking-tight text-white sm:text-4xl md:text-5xl">
271+
<SplitText text="Your AI agent has more access than your junior dev." className="text-3xl sm:text-4xl md:text-5xl font-bold leading-tight tracking-tight justify-center" />
272+
<span className="text-accent">Have you audited it?</span>
273+
</h1>
274+
</motion.div>
271275

272276
<motion.p
273277
initial={{ opacity: 0, y: 20 }}
@@ -330,29 +334,31 @@ export default function Home() {
330334

331335
{/* Action buttons */}
332336
<div className="flex flex-col gap-3 sm:flex-row">
333-
<button
334-
onClick={handleScan}
335-
disabled={!config.trim() || scanning}
336-
className={`flex-1 rounded-xl py-4 text-sm font-bold text-white transition-all ${
337-
scanning
338-
? 'bg-accent/60 cursor-wait'
339-
: config.trim()
340-
? 'bg-accent hover:bg-accent/90 pulse-glow cursor-pointer'
341-
: 'bg-card-border cursor-not-allowed text-muted'
342-
}`}
343-
>
344-
{scanning ? (
345-
<span className="flex items-center justify-center gap-3">
346-
<svg className="h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
347-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
348-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
349-
</svg>
350-
Scanning for vulnerabilities...
351-
</span>
352-
) : (
353-
'Scan Now -- Free'
354-
)}
355-
</button>
337+
<MagneticButton className="flex-1">
338+
<button
339+
onClick={handleScan}
340+
disabled={!config.trim() || scanning}
341+
className={`w-full rounded-xl py-4 text-sm font-bold text-white transition-all ${
342+
scanning
343+
? 'bg-accent/60 cursor-wait'
344+
: config.trim()
345+
? 'bg-accent hover:bg-accent/90 pulse-glow cursor-pointer'
346+
: 'bg-card-border cursor-not-allowed text-muted'
347+
}`}
348+
>
349+
{scanning ? (
350+
<span className="flex items-center justify-center gap-3">
351+
<svg className="h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
352+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
353+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
354+
</svg>
355+
Scanning for vulnerabilities...
356+
</span>
357+
) : (
358+
'Scan Now -- Free'
359+
)}
360+
</button>
361+
</MagneticButton>
356362
<button
357363
onClick={() => {
358364
setConfig(EXAMPLE_CONFIG)

components/MagneticButton.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import React, { useRef, useState } from 'react';
4+
import { motion } from 'framer-motion';
5+
6+
interface MagneticButtonProps {
7+
children: React.ReactNode;
8+
className?: string;
9+
strength?: number;
10+
}
11+
12+
export const MagneticButton: React.FC<MagneticButtonProps> = ({
13+
children,
14+
className = '',
15+
strength = 30,
16+
}) => {
17+
const ref = useRef<HTMLDivElement>(null);
18+
const [position, setPosition] = useState({ x: 0, y: 0 });
19+
20+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
21+
const { clientX, clientY } = e;
22+
const { height, width, left, top } =
23+
ref.current!.getBoundingClientRect();
24+
25+
const middleX = clientX - (left + width / 2);
26+
const middleY = clientY - (top + height / 2);
27+
28+
setPosition({
29+
x: (middleX / width) * strength,
30+
y: (middleY / height) * strength,
31+
});
32+
};
33+
34+
const reset = () => {
35+
setPosition({ x: 0, y: 0 });
36+
};
37+
38+
return (
39+
<motion.div
40+
ref={ref}
41+
onMouseMove={handleMouseMove}
42+
onMouseLeave={reset}
43+
animate={{ x: position.x, y: position.y }}
44+
transition={{ type: 'spring', stiffness: 150, damping: 15, mass: 0.1 }}
45+
className={className}
46+
style={{ display: 'inline-block' }}
47+
>
48+
{children}
49+
</motion.div>
50+
);
51+
};

components/SplitText.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { motion } from 'framer-motion';
5+
6+
interface SplitTextProps {
7+
text: string;
8+
className?: string;
9+
delay?: number;
10+
}
11+
12+
const container = {
13+
hidden: { opacity: 0 },
14+
visible: (i = 1) => ({
15+
opacity: 1,
16+
transition: { staggerChildren: 0.12, delayChildren: i },
17+
}),
18+
};
19+
20+
const child = {
21+
visible: {
22+
opacity: 1,
23+
y: 0,
24+
transition: {
25+
type: 'spring' as const,
26+
damping: 12,
27+
stiffness: 100,
28+
},
29+
},
30+
hidden: {
31+
opacity: 0,
32+
y: 100,
33+
transition: {
34+
type: 'spring' as const,
35+
damping: 12,
36+
stiffness: 100,
37+
},
38+
},
39+
};
40+
41+
export const SplitText: React.FC<SplitTextProps> = ({
42+
text,
43+
className = '',
44+
delay = 0,
45+
}) => {
46+
const words = text.split(' ');
47+
48+
return (
49+
<motion.div
50+
className={`flex flex-wrap ${className}`}
51+
variants={container}
52+
custom={delay}
53+
initial="hidden"
54+
whileInView="visible"
55+
viewport={{ once: true, margin: '-50px' }}
56+
>
57+
{words.map((word, index) => (
58+
<motion.span
59+
variants={child}
60+
style={{ marginRight: '0.25em', display: 'inline-block' }}
61+
key={index}
62+
>
63+
{word}
64+
</motion.span>
65+
))}
66+
</motion.div>
67+
);
68+
};

0 commit comments

Comments
 (0)