diff --git a/apps/www/__registry__/index.tsx b/apps/www/__registry__/index.tsx index 44e00f9..7137763 100644 --- a/apps/www/__registry__/index.tsx +++ b/apps/www/__registry__/index.tsx @@ -159,6 +159,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "typewriter": { + name: "typewriter", + type: "components:ui", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/ui/typewriter")), + source: "", + files: ["registry/default/ui/typewriter.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "animated-number": { + name: "animated-number", + type: "components:ui", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/ui/animated-number")), + source: "", + files: ["registry/default/ui/animated-number.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "text-animate-demo": { name: "text-animate-demo", type: "components:example", @@ -313,6 +335,28 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "typewriter-demo": { + name: "typewriter-demo", + type: "components:example", + registryDependencies: ["typewriter"], + component: React.lazy(() => import("@/registry/default/example/typewriter-demo")), + source: "", + files: ["registry/default/example/typewriter-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "animated-number-demo": { + name: "animated-number-demo", + type: "components:example", + registryDependencies: ["animated-number"], + component: React.lazy(() => import("@/registry/default/example/animated-number-demo")), + source: "", + files: ["registry/default/example/animated-number-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "authentication-01": { name: "authentication-01", type: "components:block", diff --git a/apps/www/app/(app)/page.tsx b/apps/www/app/(app)/page.tsx index 7aa7a5c..21a1dcb 100644 --- a/apps/www/app/(app)/page.tsx +++ b/apps/www/app/(app)/page.tsx @@ -1,11 +1,11 @@ -import Image from "next/image" +import { Suspense } from "react" import Link from "next/link" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" +import { FeaturesSection } from "@/components/animate/feature-card" import { Announcement } from "@/components/announcement" -import { ExamplesNav } from "@/components/examples-nav" import { Icons } from "@/components/icons" import { PageActions, @@ -55,12 +55,17 @@ export default function IndexPage() { -
+
- + + +
+
+ +
{/*
*/}
diff --git a/apps/www/assets/cult-seo-og.png b/apps/www/assets/cult-seo-og.png new file mode 100644 index 0000000..8ec91e6 Binary files /dev/null and b/apps/www/assets/cult-seo-og.png differ diff --git a/apps/www/assets/cults.png b/apps/www/assets/cults.png new file mode 100644 index 0000000..080f312 Binary files /dev/null and b/apps/www/assets/cults.png differ diff --git a/apps/www/assets/feature-1.png b/apps/www/assets/feature-1.png new file mode 100644 index 0000000..5d33956 Binary files /dev/null and b/apps/www/assets/feature-1.png differ diff --git a/apps/www/assets/feature-2.png b/apps/www/assets/feature-2.png new file mode 100644 index 0000000..12b551c Binary files /dev/null and b/apps/www/assets/feature-2.png differ diff --git a/apps/www/assets/feature-3.png b/apps/www/assets/feature-3.png new file mode 100644 index 0000000..955f895 Binary files /dev/null and b/apps/www/assets/feature-3.png differ diff --git a/apps/www/assets/feature-4.png b/apps/www/assets/feature-4.png new file mode 100644 index 0000000..2490864 Binary files /dev/null and b/apps/www/assets/feature-4.png differ diff --git a/apps/www/assets/feature-5.png b/apps/www/assets/feature-5.png new file mode 100644 index 0000000..d118799 Binary files /dev/null and b/apps/www/assets/feature-5.png differ diff --git a/apps/www/assets/feature-6.png b/apps/www/assets/feature-6.png new file mode 100644 index 0000000..12eba38 Binary files /dev/null and b/apps/www/assets/feature-6.png differ diff --git a/apps/www/assets/texture-card.png b/apps/www/assets/texture-card.png new file mode 100644 index 0000000..004cd0f Binary files /dev/null and b/apps/www/assets/texture-card.png differ diff --git a/apps/www/assets/threed.png b/apps/www/assets/threed.png new file mode 100644 index 0000000..c452731 Binary files /dev/null and b/apps/www/assets/threed.png differ diff --git a/apps/www/components/animate/animated-number.tsx b/apps/www/components/animate/animated-number.tsx new file mode 100644 index 0000000..89c2bcf --- /dev/null +++ b/apps/www/components/animate/animated-number.tsx @@ -0,0 +1,266 @@ +"use client" + +import { useEffect, useState } from "react" +import { MotionValue, motion, useSpring, useTransform } from "framer-motion" +import { Minus, Plus } from "lucide-react" +import { toast } from "sonner" + +import { GradientHeading } from "@/registry/default/ui/gradient-heading" +import TextureCard, { + TextureCardContent, + TextureCardHeader, + TextureCardStyled, +} from "@/registry/default/ui/texture-card" + +import { Button } from "../ui/button" +import { Slider } from "../ui/slider" + +interface AnimatedNumberProps { + value: number + mass?: number + stiffness?: number + damping?: number + precision?: number + format?: (value: number) => string + onAnimationStart?: () => void + onAnimationComplete?: () => void +} + +export function AnimatedNumber({ + value, + mass = 0.8, + stiffness = 75, + damping = 15, + precision = 0, + format = (num) => num.toLocaleString(), + onAnimationStart, + onAnimationComplete, +}: AnimatedNumberProps) { + const spring = useSpring(value, { mass, stiffness, damping }) + const display: MotionValue = useTransform(spring, (current) => + format(parseFloat(current.toFixed(precision))) + ) + + useEffect(() => { + spring.set(value) + if (onAnimationStart) onAnimationStart() + const unsubscribe = spring.onChange(() => { + if (spring.get() === value && onAnimationComplete) onAnimationComplete() + }) + return () => unsubscribe() + }, [spring, value, onAnimationStart, onAnimationComplete]) + + return {display} +} + +export function BasicExample() { + const [value, setValue] = useState(1000) + + return ( +
+ + +
+ ) +} + +export function PrecisionExample() { + const [value, setValue] = useState(1234.5678) + + return ( + + + Precision + + +
+
+ +
+ +
+
+
+ ) +} + +export function FormatExample() { + const [value, setValue] = useState(1000) + + const customFormat = (num: number) => `$${num.toFixed(2)}` + + return ( + + + Format + + +
+
+ +
+ +
+
+
+ ) +} + +export function HooksExample() { + const [value, setValue] = useState(1000) + + const handleAnimationStart = () => { + toast("🏁 Animation started ") + } + + const handleAnimationComplete = () => { + toast("✅ Animation completed ") + } + + return ( + + + Callbacks + + +
+
+ +
+ +
+
+
+ ) +} + +export function CustomSpringExample() { + const [value, setValue] = useState(1000) + const [mass, setMass] = useState(1) + const [stiffness, setStiffness] = useState(100) + const [damping, setDamping] = useState(40) + + const handleValueChange = + (setter: (value: number) => void, minValue: number) => + (values: number[]) => { + const newValue = Math.max(values[0], minValue) + setter(newValue) + } + + return ( + + + Custom Spring Properties + + +
+ +
+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ) +} + +export function AnimatedNumberExamples() { + return ( +
+
+ +
+ + + +
+
+
+ ) +} diff --git a/apps/www/components/animate/feature-card.tsx b/apps/www/components/animate/feature-card.tsx new file mode 100644 index 0000000..1e45c23 --- /dev/null +++ b/apps/www/components/animate/feature-card.tsx @@ -0,0 +1,456 @@ +"use client" + +import { useEffect, useState, type MouseEvent } from "react" +import Image, { type StaticImageData } from "next/image" +import cult from "@/assets/cults.png" +import shiftCard from "@/assets/feature-1.png" +import family from "@/assets/feature-2.png" +import carousel from "@/assets/feature-3.png" +import textureFull from "@/assets/feature-4.png" +import buttons from "@/assets/feature-5.png" +import reincarnate from "@/assets/feature-6.png" +import textureCard from "@/assets/texture-card.png" +import clsx from "clsx" +import { + motion, + useMotionTemplate, + useMotionValue, + type MotionStyle, + type MotionValue, +} from "framer-motion" +import Balancer from "react-wrap-balancer" + +import { cn } from "@/lib/utils" +import { GradientHeading } from "@/registry/default/ui/gradient-heading" + +type WrapperStyle = MotionStyle & { + "--x": MotionValue + "--y": MotionValue +} + +interface CardProps { + title: string + description: string + bgClass?: string +} + +function FeatureCard({ + title, + description, + bgClass, + children, +}: CardProps & { + children: React.ReactNode +}) { + const [mounted, setMounted] = useState(false) + const mouseX = useMotionValue(0) + const mouseY = useMotionValue(0) + const isMobile = useIsMobile() + + function handleMouseMove({ currentTarget, clientX, clientY }: MouseEvent) { + if (isMobile) return + const { left, top } = currentTarget.getBoundingClientRect() + mouseX.set(clientX - left) + mouseY.set(clientY - top) + } + + useEffect(() => { + setMounted(true) + }, []) + + return ( + +
+
+
+

+ {title} +

+

+ {description} +

+
+ {mounted ? children : null} +
+
+
+ ) +} + +const steps = [ + { id: "1", name: "" }, + { id: "2", name: "" }, + // { id: '3', name: '' }, +] + +export function ChallengeCreationCard({ + image, + step1img1Class, + step1img2Class, + step2img1Class, + step2img2Class, + step3imgClass, + + ...props +}: CardProps & { + step1img1Class?: string + step1img2Class?: string + step2img1Class?: string + step2img2Class?: string + step3imgClass?: string + image: { + step1dark1: StaticImageData + step1dark2: StaticImageData + step1light1: StaticImageData + step1light2: StaticImageData + step2dark1: StaticImageData + step2dark2: StaticImageData + step2light1: StaticImageData + step2light2: StaticImageData + step3dark: StaticImageData + step3light: StaticImageData + step4light: StaticImageData + alt: string + } +}) { + const { currentNumber: step, increment } = useNumberCycler() + + return ( + +
+ {image.alt} +
+ + <> + {/* step 1 */} + {image.alt} 0, + })} + src={image.step1light1} + style={{ + position: "absolute", + userSelect: "none", + maxWidth: "unset", + }} + /> + {image.alt} 0, + })} + src={image.step1light2} + style={{ + position: "absolute", + userSelect: "none", + maxWidth: "unset", + }} + /> + + {/* step 2 */} + {image.alt} 1 } + )} + src={image.step2light1} + style={{ + position: "absolute", + userSelect: "none", + maxWidth: "unset", + }} + /> + {image.alt} 1 } + )} + src={image.step2light2} + style={{ + position: "absolute", + userSelect: "none", + maxWidth: "unset", + }} + /> + {/* step 3 */} + {image.alt} 2 }, + { "opacity-90": step === 2 } + )} + src={image.step3light} + style={{ + position: "absolute", + userSelect: "none", + maxWidth: "unset", + }} + /> +
+ {}} steps={steps} /> +
+ + +
increment()} + /> + + ) +} + +function IconCheck({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + + + ) +} + +// @ts-ignore +export function Steps({ steps, current, onChange }) { + return ( + + ) +} + +export function FeaturesSection() { + return ( +
+
+
+
+
+

+ + Tailwind + Framer + React + +

+ +

+ Everything you need to ship +

+
+ +
+ +
+
+ +
+
+
+
+
+
+ ) +} + +function useNumberCycler() { + const [currentNumber, setCurrentNumber] = useState(0) + const [dummy, setDummy] = useState(0) + + const increment = () => { + setCurrentNumber((prevNumber) => { + return (prevNumber + 1) % 4 + }) + + setDummy((prev) => prev + 1) + } + + useEffect(() => { + const intervalId = setInterval(() => { + setCurrentNumber((prevNumber) => { + return (prevNumber + 1) % 4 + }) + }, 3000) + + return () => { + clearInterval(intervalId) + } + }, [dummy]) + + return { + increment, + currentNumber, + } +} + +export function useIsMobile() { + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + const userAgent = navigator.userAgent + const isSmall = window.matchMedia("(max-width: 768px)").matches + const isMobile = Boolean( + /Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile|WPDesktop/i.exec( + userAgent + ) + ) + + const isDev = process.env.NODE_ENV !== "production" + if (isDev) setIsMobile(isSmall || isMobile) + + setIsMobile(isSmall && isMobile) + }, []) + + return isMobile +} diff --git a/apps/www/components/animate/social-cards.tsx b/apps/www/components/animate/social-cards.tsx new file mode 100644 index 0000000..00a24be --- /dev/null +++ b/apps/www/components/animate/social-cards.tsx @@ -0,0 +1,113 @@ +"use client" + +import { ReactNode } from "react" +import Image from "next/image" + +import { TextAnimation } from "./type-animate" + +interface OGAttr { + img: string + title?: string + desc?: string + url: string +} + +export function FacebookOGCard({ img, title, desc, url }: OGAttr) { + return ( +
+
+ {title +
+
+

{url}

+

+ {title} +

+

+ {desc} +

+
+
+ ) +} + +export function TwitterOGCard({ img, title, url }: OGAttr) { + return ( +
+
+ {title +
+
+ {url} +
+
+ ) +} + +export function LinkedInOGCard({ img, title, url }: OGAttr) { + return ( +
+
+ {title +
+
+

+ {title} +

+

{url}

+
+
+ ) +} + +export function SMSOgCard({ title, url, img }: OGAttr) { + return ( +
+
+ Article Thumbnail +
+
+
+

+ {title} +

+

{url}

+
+
+
+ ) +} + +export function IosOgShellCard({ children }: { children: ReactNode }) { + return ( +
+
+
+ iMessage +
+
+ Today 11:29 +
+
+ Check out this new product! +
+ {children} +
+ Delivered +
+
+
+ ) +} diff --git a/apps/www/components/animate/type-animate.tsx b/apps/www/components/animate/type-animate.tsx new file mode 100644 index 0000000..05f5bc7 --- /dev/null +++ b/apps/www/components/animate/type-animate.tsx @@ -0,0 +1,119 @@ +"use client" + +import { useEffect, useState } from "react" +import { animate, motion, useMotionValue, useTransform } from "framer-motion" + +export interface ITextAnimationProps { + delay: number + texts: string[] +} + +export function TextAnimation({ delay, texts }: ITextAnimationProps) { + const [animationComplete, setAnimationComplete] = useState(false) + const baseText = "Create a " + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.slice(0, latest) + ) + + useEffect(() => { + const controls = animate(count, baseText.length, { + type: "tween", + delay, + duration: 1, + ease: "easeInOut", + onComplete: () => setAnimationComplete(true), + }) + return () => { + controls.stop && controls.stop() + } + }, [count, baseText.length, delay]) + + return ( + + {displayText} + {animationComplete && ( + + )} + + + ) +} + +export interface IRepeatedTextAnimationProps { + delay: number + texts: string[] +} + +const defaultTexts = [ + "quiz page with questions and answers", + "blog Article Details Page Layout", + "ecommerce dashboard with a sidebar", + "ui like platform.openai.com....", + "buttttton", + "aop that tracks non-standard split sleep cycles", + "transparent card to showcase achievements of a user", +] +function RepeatedTextAnimation({ + delay, + texts = defaultTexts, +}: IRepeatedTextAnimationProps) { + const textIndex = useMotionValue(0) + + const baseText = useTransform(textIndex, (latest) => texts[latest] || "") + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.get().slice(0, latest) + ) + const updatedThisRound = useMotionValue(true) + + useEffect(() => { + const animation = animate(count, 60, { + type: "tween", + delay, + duration: 1, + ease: "easeIn", + repeat: Infinity, + repeatType: "reverse", + repeatDelay: 1, + onUpdate(latest) { + if (updatedThisRound.get() && latest > 0) { + updatedThisRound.set(false) + } else if (!updatedThisRound.get() && latest === 0) { + textIndex.set((textIndex.get() + 1) % texts.length) + updatedThisRound.set(true) + } + }, + }) + return () => { + animation.stop && animation.stop() + } + }, [count, delay, textIndex, texts, updatedThisRound]) + + return {displayText} +} + +const cursorVariants = { + blinking: { + opacity: [0, 0, 1, 1], + transition: { + duration: 1, + repeat: Infinity, + repeatDelay: 0, + ease: "linear", + times: [0, 0.5, 0.5, 1], + }, + }, +} + +function BlinkingCursor() { + return ( + + ) +} diff --git a/apps/www/components/component-preview.tsx b/apps/www/components/component-preview.tsx index 9f4bfe9..89b726f 100644 --- a/apps/www/components/component-preview.tsx +++ b/apps/www/components/component-preview.tsx @@ -104,7 +104,7 @@ export function ComponentPreview({
- + )) Slider.displayName = SliderPrimitive.Root.displayName diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts index 30dd51a..d11040a 100644 --- a/apps/www/config/docs.ts +++ b/apps/www/config/docs.ts @@ -51,71 +51,100 @@ export const docsConfig: DocsConfig = { title: "Dynamic Island", href: "/docs/components/dynamic-island", items: [], + label: "new", }, { title: "Shift Card", href: "/docs/components/shift-card", items: [], + label: "new", }, { title: "Family Button", href: "/docs/components/family-button", items: [], + label: "new", }, { title: "Direction Aware Tabs", href: "/docs/components/direction-aware-tabs", items: [], + label: "new", }, { title: "Side Panel", href: "/docs/components/side-panel", items: [], + label: "new", }, { title: "Bg Media", href: "/docs/components/bg-media", items: [], + label: "new", }, + { - title: "3D Carousel", - href: "/docs/components/three-d-carousel", + title: "Text Animate", + href: "/docs/components/text-animate", items: [], + label: "new", }, { - title: "Gradient Heading", - href: "/docs/components/gradient-heading", + title: "Typewriter", + href: "/docs/components/typewriter", items: [], + label: "new", }, { - title: "Tweet Grid", - href: "/docs/components/tweet-grid", + title: "Animated Number", + href: "/docs/components/animated-number", items: [], + label: "new", }, { - title: "Bg Animate Button", - href: "/docs/components/bg-animate-button", + title: "3D Carousel", + href: "/docs/components/three-d-carousel", items: [], + label: "new", }, + + { + title: "Tweet Grid", + href: "/docs/components/tweet-grid", + items: [], + label: "new", + }, + { title: "Texture Button", href: "/docs/components/texture-button", items: [], + label: "new", }, { title: "Texture Card", href: "/docs/components/texture-card", items: [], + label: "new", }, { title: "Minimal Card", href: "/docs/components/minimal-card", items: [], + label: "new", }, { - title: "Text Animate", - href: "/docs/components/text-animate", + title: "Gradient Heading", + href: "/docs/components/gradient-heading", + items: [], + label: "new", + }, + { + title: "Bg Animate Button", + href: "/docs/components/bg-animate-button", items: [], + label: "new", }, ], }, diff --git a/apps/www/content/docs/components/accordion.mdx b/apps/www/content/docs/components/accordion.mdx index 251249d..0356b4a 100644 --- a/apps/www/content/docs/components/accordion.mdx +++ b/apps/www/content/docs/components/accordion.mdx @@ -15,15 +15,12 @@ links: ## Installation - + - CLI Manual - - Run the following command: @@ -62,8 +59,6 @@ module.exports = { - - @@ -110,8 +105,6 @@ module.exports = { - - ## Usage diff --git a/apps/www/content/docs/components/animated-number.mdx b/apps/www/content/docs/components/animated-number.mdx new file mode 100644 index 0000000..bf95cbc --- /dev/null +++ b/apps/www/content/docs/components/animated-number.mdx @@ -0,0 +1,62 @@ +--- +title: AnimatedNumber +description: A simple animated number animation +component: true +links: +--- + + + +## Installation + + + + + Manual + + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + + + +## Usage + +```tsx +import { AnimatedNumber } from "@/components/ui/animated-number" +``` + +```tsx +function BasicExample() { + const [value, setValue] = useState(1000) + + return ( +
+ + +
+ ) +} +``` diff --git a/apps/www/content/docs/components/bg-animate-button.mdx b/apps/www/content/docs/components/bg-animate-button.mdx index c10cf71..2d9b6b9 100644 --- a/apps/www/content/docs/components/bg-animate-button.mdx +++ b/apps/www/content/docs/components/bg-animate-button.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add bg-animate-button -``` - - - - - diff --git a/apps/www/content/docs/components/bg-media.mdx b/apps/www/content/docs/components/bg-media.mdx index 49bf41c..6c2177f 100644 --- a/apps/www/content/docs/components/bg-media.mdx +++ b/apps/www/content/docs/components/bg-media.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add bg-media -``` - - - - - diff --git a/apps/www/content/docs/components/direction-aware-tabs.mdx b/apps/www/content/docs/components/direction-aware-tabs.mdx index 32fc913..a02f64c 100644 --- a/apps/www/content/docs/components/direction-aware-tabs.mdx +++ b/apps/www/content/docs/components/direction-aware-tabs.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add direction-aware-tabs -``` - - - - - diff --git a/apps/www/content/docs/components/dynamic-island.mdx b/apps/www/content/docs/components/dynamic-island.mdx index 458ccbf..aca2cbe 100644 --- a/apps/www/content/docs/components/dynamic-island.mdx +++ b/apps/www/content/docs/components/dynamic-island.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add dynamic-island -``` - - - - - diff --git a/apps/www/content/docs/components/family-button.mdx b/apps/www/content/docs/components/family-button.mdx index d4a81f9..aac8791 100644 --- a/apps/www/content/docs/components/family-button.mdx +++ b/apps/www/content/docs/components/family-button.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add family-button -``` - - - - - diff --git a/apps/www/content/docs/components/gradient-heading.mdx b/apps/www/content/docs/components/gradient-heading.mdx index e712d15..ab8d018 100644 --- a/apps/www/content/docs/components/gradient-heading.mdx +++ b/apps/www/content/docs/components/gradient-heading.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add gradient-heading -``` - - - - - diff --git a/apps/www/content/docs/components/minimal-card.mdx b/apps/www/content/docs/components/minimal-card.mdx index e76c86c..2eb453f 100644 --- a/apps/www/content/docs/components/minimal-card.mdx +++ b/apps/www/content/docs/components/minimal-card.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add minimal-card -``` - - - - - diff --git a/apps/www/content/docs/components/shift-card.mdx b/apps/www/content/docs/components/shift-card.mdx index 6aa7baa..8df021c 100644 --- a/apps/www/content/docs/components/shift-card.mdx +++ b/apps/www/content/docs/components/shift-card.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add shift-card -``` - - - - - diff --git a/apps/www/content/docs/components/side-panel.mdx b/apps/www/content/docs/components/side-panel.mdx index a7b325f..cfa765e 100644 --- a/apps/www/content/docs/components/side-panel.mdx +++ b/apps/www/content/docs/components/side-panel.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add side-panel -``` - - - - - diff --git a/apps/www/content/docs/components/text-animate.mdx b/apps/www/content/docs/components/text-animate.mdx index 9c935f8..71d69a9 100644 --- a/apps/www/content/docs/components/text-animate.mdx +++ b/apps/www/content/docs/components/text-animate.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add text-animate -``` - - - - - diff --git a/apps/www/content/docs/components/texture-button.mdx b/apps/www/content/docs/components/texture-button.mdx index da13d4e..9fb1107 100644 --- a/apps/www/content/docs/components/texture-button.mdx +++ b/apps/www/content/docs/components/texture-button.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add texture-button -``` - - - - - diff --git a/apps/www/content/docs/components/texture-card.mdx b/apps/www/content/docs/components/texture-card.mdx index 5b31813..8551e54 100644 --- a/apps/www/content/docs/components/texture-card.mdx +++ b/apps/www/content/docs/components/texture-card.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add texture-card -``` - - - - - diff --git a/apps/www/content/docs/components/three-d-carousel.mdx b/apps/www/content/docs/components/three-d-carousel.mdx index 7893f3c..fd2ed2e 100644 --- a/apps/www/content/docs/components/three-d-carousel.mdx +++ b/apps/www/content/docs/components/three-d-carousel.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add three-d-carousel -``` - - - - - diff --git a/apps/www/content/docs/components/tweet-grid.mdx b/apps/www/content/docs/components/tweet-grid.mdx index 28545fd..126fa34 100644 --- a/apps/www/content/docs/components/tweet-grid.mdx +++ b/apps/www/content/docs/components/tweet-grid.mdx @@ -13,27 +13,12 @@ links: ## Installation - + - CLI Manual - - - - -Run the following command: - -```bash -npx cult-ui@latest add tweet-grid -``` - - - - - diff --git a/apps/www/content/docs/components/typewriter.mdx b/apps/www/content/docs/components/typewriter.mdx new file mode 100644 index 0000000..61b1beb --- /dev/null +++ b/apps/www/content/docs/components/typewriter.mdx @@ -0,0 +1,63 @@ +--- +title: Typewriter +description: A repeating typewriter effect +component: true +links: +--- + + + +## Installation + + + + + Manual + + + + + + +Copy and paste the following code into your project. + + + +Update the import paths to match your project setup. + + + + + + + +## Usage + +```tsx +import { TypewriterDemo } from "@/components/ui/typewriter" +``` + +```tsx +const texts = [ + "Testing 124", + "Look at newcult.co", + "and check gnow.io", + "Sick af", +] + +export default function TypewriterDemo() { + return ( + +
+

+ +

+
+
+ ) +} +``` diff --git a/apps/www/public/registry/index.json b/apps/www/public/registry/index.json index 609625b..67bd490 100644 --- a/apps/www/public/registry/index.json +++ b/apps/www/public/registry/index.json @@ -139,5 +139,25 @@ "ui/gradient-heading.tsx" ], "type": "components:ui" + }, + { + "name": "typewriter", + "dependencies": [ + "framer-motion" + ], + "files": [ + "ui/typewriter.tsx" + ], + "type": "components:ui" + }, + { + "name": "animated-number", + "dependencies": [ + "framer-motion" + ], + "files": [ + "ui/animated-number.tsx" + ], + "type": "components:ui" } ] \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/animated-number.json b/apps/www/public/registry/styles/default/animated-number.json new file mode 100644 index 0000000..2678dc8 --- /dev/null +++ b/apps/www/public/registry/styles/default/animated-number.json @@ -0,0 +1,13 @@ +{ + "name": "animated-number", + "dependencies": [ + "framer-motion" + ], + "files": [ + { + "name": "animated-number.tsx", + "content": "\"use client\"\n\nimport { useEffect } from \"react\"\nimport { MotionValue, motion, useSpring, useTransform } from \"framer-motion\"\n\ninterface AnimatedNumberProps {\n value: number\n mass?: number\n stiffness?: number\n damping?: number\n precision?: number\n format?: (value: number) => string\n onAnimationStart?: () => void\n onAnimationComplete?: () => void\n}\n\nexport function AnimatedNumber({\n value,\n mass = 0.8,\n stiffness = 75,\n damping = 15,\n precision = 0,\n format = (num) => num.toLocaleString(),\n onAnimationStart,\n onAnimationComplete,\n}: AnimatedNumberProps) {\n const spring = useSpring(value, { mass, stiffness, damping })\n const display: MotionValue = useTransform(spring, (current) =>\n format(parseFloat(current.toFixed(precision)))\n )\n\n useEffect(() => {\n spring.set(value)\n if (onAnimationStart) onAnimationStart()\n const unsubscribe = spring.onChange(() => {\n if (spring.get() === value && onAnimationComplete) onAnimationComplete()\n })\n return () => unsubscribe()\n }, [spring, value, onAnimationStart, onAnimationComplete])\n\n return {display}\n}\n" + } + ], + "type": "components:ui" +} \ No newline at end of file diff --git a/apps/www/public/registry/styles/default/texture-card.json b/apps/www/public/registry/styles/default/texture-card.json index a797727..5b22e91 100644 --- a/apps/www/public/registry/styles/default/texture-card.json +++ b/apps/www/public/registry/styles/default/texture-card.json @@ -6,7 +6,7 @@ "files": [ { "name": "texture-card.tsx", - "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TextureCardStyled = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes & { children?: React.ReactNode }\n>(({ className, children, ...props }, ref) => (\n \n {/* Nested structure for aesthetic borders */}\n
\n
\n
\n {/* Inner content wrapper */}\n
\n {children}\n
\n
\n
\n
\n
\n))\n\n// Allows for global css overrides and theme support - similar to shad cn\nconst TextureCard = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes & { children?: React.ReactNode }\n>(({ className, children, ...props }, ref) => {\n return (\n \n
\n
\n
\n
\n {children}\n
\n
\n
\n
\n
\n )\n})\n\nTextureCard.displayName = \"TextureCard\"\n\nconst TextureCardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardHeader.displayName = \"TextureCardHeader\"\n\nconst TextureCardTitle = React.forwardRef<\n HTMLHeadingElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardTitle.displayName = \"TextureCardTitle\"\n\nconst TextureCardDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardDescription.displayName = \"TextureCardDescription\"\n\nconst TextureCardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n
\n))\nTextureCardContent.displayName = \"TextureCardContent\"\n\nconst TextureCardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardFooter.displayName = \"TextureCardFooter\"\n\nconst TextureSeparator = () => {\n return (\n
\n )\n}\n\nexport {\n TextureCard,\n TextureCardHeader,\n TextureCardStyled,\n TextureCardFooter,\n TextureCardTitle,\n TextureSeparator,\n TextureCardDescription,\n TextureCardContent,\n}\n\nexport default TextureCard\n" + "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst TextureCardStyled = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes & { children?: React.ReactNode }\n>(({ className, children, ...props }, ref) => (\n \n {/* Nested structure for aesthetic borders */}\n
\n
\n
\n {/* Inner content wrapper */}\n
\n {children}\n
\n
\n
\n
\n
\n))\n\n// Allows for global css overrides and theme support - similar to shad cn\nconst TextureCard = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes & { children?: React.ReactNode }\n>(({ className, children, ...props }, ref) => {\n return (\n \n
\n
\n
\n
\n {children}\n
\n
\n
\n
\n
\n )\n})\n\nTextureCard.displayName = \"TextureCard\"\n\nconst TextureCardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardHeader.displayName = \"TextureCardHeader\"\n\nconst TextureCardTitle = React.forwardRef<\n HTMLHeadingElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardTitle.displayName = \"TextureCardTitle\"\n\nconst TextureCardDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardDescription.displayName = \"TextureCardDescription\"\n\nconst TextureCardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n
\n))\nTextureCardContent.displayName = \"TextureCardContent\"\n\nconst TextureCardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n))\nTextureCardFooter.displayName = \"TextureCardFooter\"\n\nconst TextureSeparator = () => {\n return (\n
\n )\n}\n\nexport {\n TextureCard,\n TextureCardHeader,\n TextureCardStyled,\n TextureCardFooter,\n TextureCardTitle,\n TextureSeparator,\n TextureCardDescription,\n TextureCardContent,\n}\n\nexport default TextureCard\n" } ], "type": "components:ui" diff --git a/apps/www/public/registry/styles/default/typewriter.json b/apps/www/public/registry/styles/default/typewriter.json new file mode 100644 index 0000000..93b532f --- /dev/null +++ b/apps/www/public/registry/styles/default/typewriter.json @@ -0,0 +1,13 @@ +{ + "name": "typewriter", + "dependencies": [ + "framer-motion" + ], + "files": [ + { + "name": "typewriter.tsx", + "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { animate, motion, useMotionValue, useTransform } from \"framer-motion\"\n\nexport interface ITypewriterProps {\n delay: number\n texts: string[]\n baseText?: string\n}\n\nexport function Typewriter({ delay, texts, baseText = \"\" }: ITypewriterProps) {\n const [animationComplete, setAnimationComplete] = useState(false)\n const count = useMotionValue(0)\n const rounded = useTransform(count, (latest) => Math.round(latest))\n const displayText = useTransform(rounded, (latest) =>\n baseText.slice(0, latest)\n )\n\n useEffect(() => {\n const controls = animate(count, baseText.length, {\n type: \"tween\",\n delay,\n duration: 1,\n ease: \"easeInOut\",\n onComplete: () => setAnimationComplete(true),\n })\n return () => {\n controls.stop && controls.stop()\n }\n }, [count, baseText.length, delay])\n\n return (\n \n {displayText}\n {animationComplete && (\n \n )}\n \n \n )\n}\n\nexport interface IRepeatedTextAnimationProps {\n delay: number\n texts: string[]\n}\n\nconst defaultTexts = [\n \"quiz page with questions and answers\",\n \"blog Article Details Page Layout\",\n \"ecommerce dashboard with a sidebar\",\n \"ui like platform.openai.com....\",\n \"buttttton\",\n \"aop that tracks non-standard split sleep cycles\",\n \"transparent card to showcase achievements of a user\",\n]\nfunction RepeatedTextAnimation({\n delay,\n texts = defaultTexts,\n}: IRepeatedTextAnimationProps) {\n const textIndex = useMotionValue(0)\n\n const baseText = useTransform(textIndex, (latest) => texts[latest] || \"\")\n const count = useMotionValue(0)\n const rounded = useTransform(count, (latest) => Math.round(latest))\n const displayText = useTransform(rounded, (latest) =>\n baseText.get().slice(0, latest)\n )\n const updatedThisRound = useMotionValue(true)\n\n useEffect(() => {\n const animation = animate(count, 60, {\n type: \"tween\",\n delay,\n duration: 1,\n ease: \"easeIn\",\n repeat: Infinity,\n repeatType: \"reverse\",\n repeatDelay: 1,\n onUpdate(latest) {\n if (updatedThisRound.get() && latest > 0) {\n updatedThisRound.set(false)\n } else if (!updatedThisRound.get() && latest === 0) {\n textIndex.set((textIndex.get() + 1) % texts.length)\n updatedThisRound.set(true)\n }\n },\n })\n return () => {\n animation.stop && animation.stop()\n }\n }, [count, delay, textIndex, texts, updatedThisRound])\n\n return {displayText}\n}\n\nconst cursorVariants = {\n blinking: {\n opacity: [0, 0, 1, 1],\n transition: {\n duration: 1,\n repeat: Infinity,\n repeatDelay: 0,\n ease: \"linear\",\n times: [0, 0.5, 0.5, 1],\n },\n },\n}\n\nfunction BlinkingCursor() {\n return (\n \n )\n}\n" + } + ], + "type": "components:ui" +} \ No newline at end of file diff --git a/apps/www/registry/default/example/animated-number-demo.tsx b/apps/www/registry/default/example/animated-number-demo.tsx new file mode 100644 index 0000000..120c271 --- /dev/null +++ b/apps/www/registry/default/example/animated-number-demo.tsx @@ -0,0 +1,218 @@ +"use client" + +import { useState } from "react" +import { Minus, Plus } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { Slider } from "@/components/ui/slider" +import { AnimatedNumber } from "@/registry/default/ui/animated-number" +import { GradientHeading } from "@/registry/default/ui/gradient-heading" +import { + TextureCardContent, + TextureCardHeader, + TextureCardStyled, +} from "@/registry/default/ui/texture-card" + +function PrecisionExample() { + const [value, setValue] = useState(14.5678) + + return ( + + + Precision + + +
+
+ +
+ +
+
+
+ ) +} + +function FormatExample() { + const [value, setValue] = useState(10) + + const customFormat = (num: number) => `$${num.toFixed(2)}` + + return ( + + + Format + + +
+
+ +
+ +
+
+
+ ) +} + +function HooksExample() { + const [value, setValue] = useState(10) + + const handleAnimationStart = () => { + toast("🏁 Animation started ") + } + + const handleAnimationComplete = () => { + toast("✅ Animation completed ") + } + + return ( + + + Callbacks + + +
+
+ +
+ +
+
+
+ ) +} + +function CustomSpringExample() { + const [value, setValue] = useState(1000) + const [mass, setMass] = useState(1) + const [stiffness, setStiffness] = useState(100) + const [damping, setDamping] = useState(40) + + const handleValueChange = + (setter: (value: number) => void, minValue: number) => + (values: number[]) => { + const newValue = Math.max(values[0], minValue) + setter(newValue) + } + + return ( + + + Custom Spring Properties + + +
+ +
+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ ) +} + +export default function AnimatedNumberExamples() { + return ( +
+
+ +
+ + + +
+
+
+ ) +} diff --git a/apps/www/registry/default/example/typewriter-demo.tsx b/apps/www/registry/default/example/typewriter-demo.tsx new file mode 100644 index 0000000..2f496fa --- /dev/null +++ b/apps/www/registry/default/example/typewriter-demo.tsx @@ -0,0 +1,49 @@ +"use client" + +import { ReactNode } from "react" + +import { Typewriter } from "../ui/typewriter" + +const texts = [ + "Testing 124", + "Look at newcult.co", + "and check gnow.io", + "Sick af", +] + +export default function TypewriterDemo() { + return ( + +
+

+ +

+
+
+ ) +} + +function IosOgShellCard({ children }: { children: ReactNode }) { + return ( +
+
+
+ iMessage +
+
+ Today 11:29 +
+
+ Hey! +
+
+ Whats up bretheren?! +
+ {children} +
+ Delivered +
+
+
+ ) +} diff --git a/apps/www/registry/default/ui/animated-number.tsx b/apps/www/registry/default/ui/animated-number.tsx new file mode 100644 index 0000000..92c8353 --- /dev/null +++ b/apps/www/registry/default/ui/animated-number.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useEffect } from "react" +import { MotionValue, motion, useSpring, useTransform } from "framer-motion" + +interface AnimatedNumberProps { + value: number + mass?: number + stiffness?: number + damping?: number + precision?: number + format?: (value: number) => string + onAnimationStart?: () => void + onAnimationComplete?: () => void +} + +export function AnimatedNumber({ + value, + mass = 0.8, + stiffness = 75, + damping = 15, + precision = 0, + format = (num) => num.toLocaleString(), + onAnimationStart, + onAnimationComplete, +}: AnimatedNumberProps) { + const spring = useSpring(value, { mass, stiffness, damping }) + const display: MotionValue = useTransform(spring, (current) => + format(parseFloat(current.toFixed(precision))) + ) + + useEffect(() => { + spring.set(value) + if (onAnimationStart) onAnimationStart() + const unsubscribe = spring.onChange(() => { + if (spring.get() === value && onAnimationComplete) onAnimationComplete() + }) + return () => unsubscribe() + }, [spring, value, onAnimationStart, onAnimationComplete]) + + return {display} +} diff --git a/apps/www/registry/default/ui/texture-card.tsx b/apps/www/registry/default/ui/texture-card.tsx index d9fbb48..40b1852 100644 --- a/apps/www/registry/default/ui/texture-card.tsx +++ b/apps/www/registry/default/ui/texture-card.tsx @@ -20,7 +20,7 @@ const TextureCardStyled = React.forwardRef<
{/* Inner content wrapper */} -
+
{children}
diff --git a/apps/www/registry/default/ui/type-animate.tsx b/apps/www/registry/default/ui/type-animate.tsx new file mode 100644 index 0000000..1bcddd7 --- /dev/null +++ b/apps/www/registry/default/ui/type-animate.tsx @@ -0,0 +1,174 @@ +"use client" + +import { useEffect, useState } from "react" +import { animate, motion, useMotionValue, useTransform } from "framer-motion" + +import { cn } from "@/lib/utils" + +const containerVariants = { + hidden: { opacity: 0, y: 30 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.3, + ease: "easeOut", + delayChildren: 0.3, + staggerChildren: 0.1, + }, + }, +} + +const itemVariants = { + hidden: { opacity: 0, y: 15 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.3, + ease: "easeOut", + }, + }, +} + +export function AnimatedContainer() { + return ( + + + + + + + + ) +} + +export interface ITextAnimationProps { + delay: number +} + +function TextAnimation({ delay }: ITextAnimationProps) { + const [animationComplete, setAnimationComplete] = useState(false) + const baseText = "Create a " + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.slice(0, latest) + ) + + useEffect(() => { + const controls = animate(count, baseText.length, { + type: "tween", + delay, + duration: 1, + ease: "easeInOut", + onComplete: () => setAnimationComplete(true), + }) + return controls.stop + }, [count, baseText.length, delay]) + + return ( + + {displayText} + {animationComplete && } + + + ) +} + +export interface IRepeatedTextAnimationProps { + delay: number +} + +function RepeatedTextAnimation({ delay }: IRepeatedTextAnimationProps) { + const textIndex = useMotionValue(0) + const texts = [ + "quiz page with questions and answers", + "blog Article Details Page Layout", + "ecommerce dashboard with a sidebar navigation and a table of recent orders.", + "ui like platform.openai.com....", + "buttttton", + "fully detailed landing page for an application that tracks non-standard split sleep cycles", + "transparent card to showcase achievements of a user", + "list of product categories with image, name and description.", + "landing page hero section with a heading, leading text and an opt-in form.", + "contact form with first name, last name, email, and message fields.", + ] + + const baseText = useTransform(textIndex, (latest) => texts[latest] || "") + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.get().slice(0, latest) + ) + const updatedThisRound = useMotionValue(true) + + useEffect(() => { + const animation = animate(count, 60, { + type: "tween", + delay, + duration: 1, + ease: "easeIn", + repeat: Infinity, + repeatType: "reverse", + repeatDelay: 1, + onUpdate(latest) { + if (updatedThisRound.get() && latest > 0) { + updatedThisRound.set(false) + } else if (!updatedThisRound.get() && latest === 0) { + textIndex.set((textIndex.get() + 1) % texts.length) + updatedThisRound.set(true) + } + }, + }) + return () => animation.stop() + }, [count, delay, textIndex, texts, updatedThisRound]) + + return {displayText} +} + +const cursorVariants = { + blinking: { + opacity: [0, 0, 1, 1], + transition: { + duration: 1, + repeat: Infinity, + repeatDelay: 0, + ease: "linear", + times: [0, 0.5, 0.5, 1], + }, + }, +} + +function BlinkingCursor() { + return ( + + ) +} diff --git a/apps/www/registry/default/ui/typewriter.tsx b/apps/www/registry/default/ui/typewriter.tsx new file mode 100644 index 0000000..8bfa29f --- /dev/null +++ b/apps/www/registry/default/ui/typewriter.tsx @@ -0,0 +1,119 @@ +"use client" + +import { useEffect, useState } from "react" +import { animate, motion, useMotionValue, useTransform } from "framer-motion" + +export interface ITypewriterProps { + delay: number + texts: string[] + baseText?: string +} + +export function Typewriter({ delay, texts, baseText = "" }: ITypewriterProps) { + const [animationComplete, setAnimationComplete] = useState(false) + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.slice(0, latest) + ) + + useEffect(() => { + const controls = animate(count, baseText.length, { + type: "tween", + delay, + duration: 1, + ease: "easeInOut", + onComplete: () => setAnimationComplete(true), + }) + return () => { + controls.stop && controls.stop() + } + }, [count, baseText.length, delay]) + + return ( + + {displayText} + {animationComplete && ( + + )} + + + ) +} + +export interface IRepeatedTextAnimationProps { + delay: number + texts: string[] +} + +const defaultTexts = [ + "quiz page with questions and answers", + "blog Article Details Page Layout", + "ecommerce dashboard with a sidebar", + "ui like platform.openai.com....", + "buttttton", + "aop that tracks non-standard split sleep cycles", + "transparent card to showcase achievements of a user", +] +function RepeatedTextAnimation({ + delay, + texts = defaultTexts, +}: IRepeatedTextAnimationProps) { + const textIndex = useMotionValue(0) + + const baseText = useTransform(textIndex, (latest) => texts[latest] || "") + const count = useMotionValue(0) + const rounded = useTransform(count, (latest) => Math.round(latest)) + const displayText = useTransform(rounded, (latest) => + baseText.get().slice(0, latest) + ) + const updatedThisRound = useMotionValue(true) + + useEffect(() => { + const animation = animate(count, 60, { + type: "tween", + delay, + duration: 1, + ease: "easeIn", + repeat: Infinity, + repeatType: "reverse", + repeatDelay: 1, + onUpdate(latest) { + if (updatedThisRound.get() && latest > 0) { + updatedThisRound.set(false) + } else if (!updatedThisRound.get() && latest === 0) { + textIndex.set((textIndex.get() + 1) % texts.length) + updatedThisRound.set(true) + } + }, + }) + return () => { + animation.stop && animation.stop() + } + }, [count, delay, textIndex, texts, updatedThisRound]) + + return {displayText} +} + +const cursorVariants = { + blinking: { + opacity: [0, 0, 1, 1], + transition: { + duration: 1, + repeat: Infinity, + repeatDelay: 0, + ease: "linear", + times: [0, 0.5, 0.5, 1], + }, + }, +} + +function BlinkingCursor() { + return ( + + ) +} diff --git a/apps/www/registry/examples.ts b/apps/www/registry/examples.ts index caf014b..b3d81c6 100644 --- a/apps/www/registry/examples.ts +++ b/apps/www/registry/examples.ts @@ -85,4 +85,16 @@ export const examples: Registry = [ registryDependencies: ["gradient-heading"], files: ["example/gradient-heading-demo.tsx"], }, + { + name: "typewriter-demo", + type: "components:example", + registryDependencies: ["typewriter"], + files: ["example/typewriter-demo.tsx"], + }, + { + name: "animated-number-demo", + type: "components:example", + registryDependencies: ["animated-number"], + files: ["example/animated-number-demo.tsx"], + }, ] diff --git a/apps/www/registry/ui.ts b/apps/www/registry/ui.ts index 22f2ead..81b0c3b 100644 --- a/apps/www/registry/ui.ts +++ b/apps/www/registry/ui.ts @@ -85,4 +85,16 @@ export const ui: Registry = [ dependencies: ["@radix-ui/react-slot"], files: ["ui/gradient-heading.tsx"], }, + { + name: "typewriter", + type: "components:ui", + dependencies: ["framer-motion"], + files: ["ui/typewriter.tsx"], + }, + { + name: "animated-number", + type: "components:ui", + dependencies: ["framer-motion"], + files: ["ui/animated-number.tsx"], + }, ] diff --git a/apps/www/styles/globals.css b/apps/www/styles/globals.css index 80c17f5..1033e20 100644 --- a/apps/www/styles/globals.css +++ b/apps/www/styles/globals.css @@ -98,3 +98,19 @@ @apply px-4; } } + +.animated-cards::before { + @apply pointer-events-none absolute select-none rounded-3xl opacity-0 transition-opacity duration-300 hover:opacity-100; + background: radial-gradient( + 1000px circle at var(--x) var(--y), + #70daff 0, + #fee4bd 10%, + #fbadd6 25%, + #70daff 35%, + rgba(255, 255, 255, 0) 50%, + transparent 80% + ); + z-index: -1; + content: ""; + inset: -1px; +} diff --git a/turbo.json b/turbo.json index 75f1559..870020c 100644 --- a/turbo.json +++ b/turbo.json @@ -4,7 +4,7 @@ "pipeline": { "build": { "dependsOn": ["^build"], - "env": ["NEXT_PUBLIC_APP_URL", "npm_config_user_agent"], + "env": ["NEXT_PUBLIC_APP_URL"], "outputs": ["dist/**", ".next/**"] }, "preview": {