Skip to content

Commit deb03f0

Browse files
committed
feat(website): add TextEffect component
1 parent 1eaa1c8 commit deb03f0

11 files changed

Lines changed: 216 additions & 154 deletions

File tree

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"@mdx-js/loader": "^3.1.0",
1818
"@mdx-js/react": "^3.1.0",
1919
"@next/mdx": "^15.3.1",
20-
"framer-motion": "12.0.0-alpha.2",
2120
"highlight.js": "^11.11.1",
21+
"motion": "^12.18.1",
2222
"next": "15.2.4",
2323
"react": "19.1.0",
2424
"react-dom": "19.1.0",

app/src/lib/@types/props.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { StaticImageData } from "next/image"
2-
import type { ButtonHTMLAttributes, PropsWithChildren } from "react"
2+
import type { ButtonHTMLAttributes, PropsWithChildren, JSX, CSSProperties } from "react"
33
import type { VariantProps } from "class-variance-authority"
4-
import type { ArgsFunction } from "./types"
4+
import type { ArgsFunction, PerType, PresetType } from "./types"
5+
import type { Transition, Variants } from "motion"
56

67
export interface LayoutProps {
78
children: React.ReactNode
@@ -25,3 +26,32 @@ export interface SeparatorProps extends PropsWithChildren {
2526
index: number
2627
rotate?: boolean
2728
}
29+
30+
export interface AnimationComponentProps {
31+
segment: string
32+
variants: Variants
33+
per: PerType
34+
segmentWrapperClassName?: string
35+
}
36+
37+
export interface TextEffectProps {
38+
children: string
39+
per?: PerType
40+
as?: keyof JSX.IntrinsicElements
41+
variants?: {
42+
container?: Variants
43+
item?: Variants
44+
}
45+
className?: string
46+
preset?: PresetType
47+
delay?: number
48+
speedReveal?: number
49+
speedSegment?: number
50+
trigger?: boolean
51+
onAnimationComplete?: () => void
52+
onAnimationStart?: () => void
53+
segmentWrapperClassName?: string
54+
containerTransition?: Transition
55+
segmentTransition?: Transition
56+
style?: CSSProperties
57+
}

app/src/lib/@types/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
export type ArgsFunction = (...args: any) => void
2+
3+
export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide"
4+
5+
export type PerType = "word" | "char" | "line"

app/src/ui/animation-component.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { memo } from "react"
2+
import { AnimationComponentProps } from "@/lib/@types/props"
3+
import { motion } from "motion/react"
4+
import { merge } from "@halvaradop/ui-core"
5+
6+
export const AnimationComponent = memo(({ segment, variants, per, segmentWrapperClassName }: AnimationComponentProps) => {
7+
const content =
8+
per === "line" ? (
9+
<motion.span variants={variants} className="block">
10+
{segment}
11+
</motion.span>
12+
) : per === "word" ? (
13+
<motion.span aria-hidden="true" variants={variants} className="inline-block whitespace-pre">
14+
{segment}
15+
</motion.span>
16+
) : (
17+
<motion.span className="inline-block whitespace-pre">
18+
{segment.split("").map((char, charIndex) => (
19+
<motion.span
20+
key={`char-${charIndex}`}
21+
aria-hidden="true"
22+
variants={variants}
23+
className="inline-block whitespace-pre"
24+
>
25+
{char}
26+
</motion.span>
27+
))}
28+
</motion.span>
29+
)
30+
31+
if (!segmentWrapperClassName) {
32+
return content
33+
}
34+
35+
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
36+
37+
return <span className={merge(defaultWrapperClassName, segmentWrapperClassName)}>{content}</span>
38+
})
39+
40+
AnimationComponent.displayName = "AnimationComponent"

app/src/ui/header/header-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link"
2-
import { motion } from "framer-motion"
2+
import { motion } from "motion/react"
33
import { itemVariants, menuVariants } from "@/ui/motion/menu.motion"
44

55
export const HeaderMenu = () => {

app/src/ui/header/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Link from "next/link"
33
import Image from "next/image"
44
import { useEffect, useState } from "react"
55
import { HeaderMenu } from "./header-menu"
6-
import { AnimatePresence } from "framer-motion"
6+
import { AnimatePresence } from "motion/react"
77
import { merge } from "@halvaradop/ui-core"
88
import menu from "@/assets/menu.svg"
99
import arrow from "@/assets/arrow.svg"

app/src/ui/hero.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
import { TextEffect } from "./text-effect"
2+
13
export const HeroSection = () => {
24
return (
35
<section className="mx-10 min-h-[calc(100dvh-4rem)] text-primary border-x border-solid border-border base:mx-16">
46
<p className="py-10 px-4 text-lg uppercase border-b border-border sm:px-6 md:px-8 base:py-16 base:px-10">
57
Set Your Utilities in Motion
68
</p>
7-
<h1 className="py-16 px-4 text-3xl/relaxed font-medium uppercase sm:px-6 md:px-8 sm:text-fluid-4xl base:px-10">
9+
<h1 className="py-16 px-4 text-3xl/relaxed font-medium uppercase sm:px-6 md:px-8 sm:text-fluid-4xl base:px-10 hidden">
810
The modern way to extend Tailwind’s capabilities with a curated suite of powerful utilities.
911
</h1>
12+
<TextEffect
13+
className="py-16 px-4 text-3xl/relaxed font-medium uppercase sm:px-6 md:px-8 sm:text-fluid-4xl base:px-10"
14+
preset="fade-in-blur"
15+
speedReveal={1.1}
16+
speedSegment={0.5}
17+
>
18+
The modern way to extend Tailwind’s capabilities with a curated suite of powerful utilities.
19+
</TextEffect>
1020
</section>
1121
)
1222
}

app/src/ui/motion/menu.motion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Variants } from "framer-motion"
1+
import type { Variants } from "motion"
22

33
export const menuVariants: Variants = {
44
hidden: {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Variants } from "motion"
2+
import type { PerType, PresetType } from "@/lib/@types/types"
3+
4+
export const defaultStaggerTimes: Record<PerType, number> = {
5+
char: 0.03,
6+
word: 0.05,
7+
line: 0.1,
8+
}
9+
10+
export const defaultContainerVariants: Variants = {
11+
hidden: { opacity: 0 },
12+
visible: {
13+
opacity: 1,
14+
transition: {
15+
staggerChildren: 0.05,
16+
},
17+
},
18+
exit: {
19+
transition: { staggerChildren: 0.05, staggerDirection: -1 },
20+
},
21+
}
22+
23+
export const defaultItemVariants: Variants = {
24+
hidden: { opacity: 0 },
25+
visible: {
26+
opacity: 1,
27+
},
28+
exit: { opacity: 0 },
29+
}
30+
31+
export const presetVariants: Record<PresetType, { container: Variants; item: Variants }> = {
32+
blur: {
33+
container: defaultContainerVariants,
34+
item: {
35+
hidden: { opacity: 0, filter: "blur(12px)" },
36+
visible: { opacity: 1, filter: "blur(0px)" },
37+
exit: { opacity: 0, filter: "blur(12px)" },
38+
},
39+
},
40+
"fade-in-blur": {
41+
container: defaultContainerVariants,
42+
item: {
43+
hidden: { opacity: 0, y: 20, filter: "blur(12px)" },
44+
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
45+
exit: { opacity: 0, y: 20, filter: "blur(12px)" },
46+
},
47+
},
48+
scale: {
49+
container: defaultContainerVariants,
50+
item: {
51+
hidden: { opacity: 0, scale: 0 },
52+
visible: { opacity: 1, scale: 1 },
53+
exit: { opacity: 0, scale: 0 },
54+
},
55+
},
56+
fade: {
57+
container: defaultContainerVariants,
58+
item: {
59+
hidden: { opacity: 0 },
60+
visible: { opacity: 1 },
61+
exit: { opacity: 0 },
62+
},
63+
},
64+
slide: {
65+
container: defaultContainerVariants,
66+
item: {
67+
hidden: { opacity: 0, y: 20 },
68+
visible: { opacity: 1, y: 0 },
69+
exit: { opacity: 0, y: 20 },
70+
},
71+
},
72+
}

app/src/ui/text-effect.tsx

Lines changed: 9 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,15 @@
11
"use client"
2-
import { merge } from "@halvaradop/ui-core"
32
import { AnimatePresence, motion } from "motion/react"
43
import type { TargetAndTransition, Transition, Variant, Variants } from "motion/react"
5-
import React from "react"
6-
7-
export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide"
8-
9-
export type PerType = "word" | "char" | "line"
10-
11-
export type TextEffectProps = {
12-
children: string
13-
per?: PerType
14-
as?: keyof React.JSX.IntrinsicElements
15-
variants?: {
16-
container?: Variants
17-
item?: Variants
18-
}
19-
className?: string
20-
preset?: PresetType
21-
delay?: number
22-
speedReveal?: number
23-
speedSegment?: number
24-
trigger?: boolean
25-
onAnimationComplete?: () => void
26-
onAnimationStart?: () => void
27-
segmentWrapperClassName?: string
28-
containerTransition?: Transition
29-
segmentTransition?: Transition
30-
style?: React.CSSProperties
31-
}
32-
33-
const defaultStaggerTimes: Record<PerType, number> = {
34-
char: 0.03,
35-
word: 0.05,
36-
line: 0.1,
37-
}
38-
39-
const defaultContainerVariants: Variants = {
40-
hidden: { opacity: 0 },
41-
visible: {
42-
opacity: 1,
43-
transition: {
44-
staggerChildren: 0.05,
45-
},
46-
},
47-
exit: {
48-
transition: { staggerChildren: 0.05, staggerDirection: -1 },
49-
},
50-
}
51-
52-
const defaultItemVariants: Variants = {
53-
hidden: { opacity: 0 },
54-
visible: {
55-
opacity: 1,
56-
},
57-
exit: { opacity: 0 },
58-
}
59-
60-
const presetVariants: Record<PresetType, { container: Variants; item: Variants }> = {
61-
blur: {
62-
container: defaultContainerVariants,
63-
item: {
64-
hidden: { opacity: 0, filter: "blur(12px)" },
65-
visible: { opacity: 1, filter: "blur(0px)" },
66-
exit: { opacity: 0, filter: "blur(12px)" },
67-
},
68-
},
69-
"fade-in-blur": {
70-
container: defaultContainerVariants,
71-
item: {
72-
hidden: { opacity: 0, y: 20, filter: "blur(12px)" },
73-
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
74-
exit: { opacity: 0, y: 20, filter: "blur(12px)" },
75-
},
76-
},
77-
scale: {
78-
container: defaultContainerVariants,
79-
item: {
80-
hidden: { opacity: 0, scale: 0 },
81-
visible: { opacity: 1, scale: 1 },
82-
exit: { opacity: 0, scale: 0 },
83-
},
84-
},
85-
fade: {
86-
container: defaultContainerVariants,
87-
item: {
88-
hidden: { opacity: 0 },
89-
visible: { opacity: 1 },
90-
exit: { opacity: 0 },
91-
},
92-
},
93-
slide: {
94-
container: defaultContainerVariants,
95-
item: {
96-
hidden: { opacity: 0, y: 20 },
97-
visible: { opacity: 1, y: 0 },
98-
exit: { opacity: 0, y: 20 },
99-
},
100-
},
101-
}
102-
103-
const AnimationComponent: React.FC<{
104-
segment: string
105-
variants: Variants
106-
per: "line" | "word" | "char"
107-
segmentWrapperClassName?: string
108-
}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {
109-
const content =
110-
per === "line" ? (
111-
<motion.span variants={variants} className="block">
112-
{segment}
113-
</motion.span>
114-
) : per === "word" ? (
115-
<motion.span aria-hidden="true" variants={variants} className="inline-block whitespace-pre">
116-
{segment}
117-
</motion.span>
118-
) : (
119-
<motion.span className="inline-block whitespace-pre">
120-
{segment.split("").map((char, charIndex) => (
121-
<motion.span
122-
key={`char-${charIndex}`}
123-
aria-hidden="true"
124-
variants={variants}
125-
className="inline-block whitespace-pre"
126-
>
127-
{char}
128-
</motion.span>
129-
))}
130-
</motion.span>
131-
)
132-
133-
if (!segmentWrapperClassName) {
134-
return content
135-
}
136-
137-
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
138-
139-
return <span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>{content}</span>
140-
})
141-
142-
AnimationComponent.displayName = "AnimationComponent"
4+
import { TextEffectProps } from "@/lib/@types/props"
5+
import type { PerType } from "@/lib/@types/types"
6+
import {
7+
defaultContainerVariants,
8+
defaultItemVariants,
9+
defaultStaggerTimes,
10+
presetVariants,
11+
} from "@/ui/motion/text-effect.motion"
12+
import { AnimationComponent } from "@/ui/animation-component"
14313

14414
const splitText = (text: string, per: PerType) => {
14515
if (per === "line") return text.split("\n")

0 commit comments

Comments
 (0)