From d22e4c25963aa4ff9ece2309254e989bb6276beb Mon Sep 17 00:00:00 2001 From: ashim-hq Date: Thu, 23 Apr 2026 17:21:22 +0800 Subject: [PATCH] feat: add FadeIn and TypingCursor utility components --- apps/landing/src/components/fade-in.tsx | 26 ++++++++++ apps/landing/src/components/typing-cursor.tsx | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 apps/landing/src/components/fade-in.tsx create mode 100644 apps/landing/src/components/typing-cursor.tsx diff --git a/apps/landing/src/components/fade-in.tsx b/apps/landing/src/components/fade-in.tsx new file mode 100644 index 00000000..ee635ed9 --- /dev/null +++ b/apps/landing/src/components/fade-in.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; + +export function FadeIn({ + children, + className, + delay = 0, +}: { + children: ReactNode; + className?: string; + delay?: number; +}) { + return ( + + {children} + + ); +} diff --git a/apps/landing/src/components/typing-cursor.tsx b/apps/landing/src/components/typing-cursor.tsx new file mode 100644 index 00000000..1ca4f212 --- /dev/null +++ b/apps/landing/src/components/typing-cursor.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { useCallback, useEffect, useState } from "react"; + +const phrases = [ + "100% local processing.", + "Zero data leaves your network.", + "50+ image tools.", + "14 AI models. Your hardware.", + "Air-gapped ready.", + "Enterprise-grade. Free forever.", + "One Docker container.", + "Open source. Always.", +]; + +export function TypingCursor() { + const [index, setIndex] = useState(0); + + const advance = useCallback(() => { + setIndex((i) => (i + 1) % phrases.length); + }, []); + + useEffect(() => { + const timer = setInterval(advance, 3000); + return () => clearInterval(timer); + }, [advance]); + + return ( + + + + {phrases[index]} + + + + + ); +}