# Djasha System — Full Content Dump > All entries inline. For one-fetch agent consumption. Site: https://system.djasha.me. ## Manifest - animated-text (component) - character-reveal (component) - custom-cursor (component) - emerald-button (component) - expandable (component) - figure (component) - hero-gallery (component) - magnetic-button (component) - parallax-image (component) - project-card (component) - scroll-marquee (component) - scroll-progress (component) - spark-cta (component) - stacking-cards (component) - status-chip (component) - text-mask-reveal (component) - tilt-card (component) - tool-ticker (component) - type-roles (component) - case-study-body (pattern) - editorial-hero (pattern) - emerald-direction (pattern) - filterable-work-grid (pattern) --- # AnimatedText Word-by-word or character-by-character staggered text reveal. ## Prompt Build a text component that reveals words in sequence with a configurable stagger delay. Each word fades in and translates upward slightly. Use motion/react's stagger feature. Default stagger 0.05s, duration 0.6s per word. Support semantic as= prop (h1/h2/p/span). Respect prefers-reduced-motion — disable animation if set. ## Tokens - `motion.ease-out-quart` — `cubic-bezier(0.165, 0.84, 0.44, 1)` - `motion.duration-base` — `300ms` ## A11y Respects prefers-reduced-motion — renders text statically when enabled. Screen readers read the complete text; animation is purely visual. ## Source (`src/components/AnimatedText.tsx`) ```tsx import { motion, useReducedMotion } from 'motion/react'; export interface AnimatedTextProps { text: string; stagger?: number; // default 0.05 — seconds between word animations duration?: number; // default 0.6 — seconds per word className?: string; as?: 'h1' | 'h2' | 'p' | 'span'; } const ease = [0.25, 0.1, 0.25, 1] as const; export function AnimatedText({ text, stagger = 0.05, duration = 0.6, className, as: Tag = 'p', }: AnimatedTextProps) { const reduced = useReducedMotion(); const words = text.split(' '); const MotionTag = motion.create(Tag); return ( {words.map((word, i) => ( {word} ))} ); } ``` ## Usage Use to give headings and body copy a deliberate entrance. Especially effective on hero sections and section intros where you want to control reading pace. - Use `as="h1"` or `as="h2"` to preserve semantic heading hierarchy — the animation wraps the element, not a div. - Keep stagger under 0.08s for sentences longer than 6 words — longer stagger makes the end of a sentence feel like it's loading. - **Not** for dynamic content that updates frequently — the animation fires once via IntersectionObserver. --- # CharacterReveal Character-by-character text reveal with spring-animated masking. ## Prompt Build a text reveal component that animates each character individually from a masked state to visible, with a small vertical translation. Use motion/react. Configurable stagger (default 0.02s) and duration (default 0.4s). Render as inline-block spans so the text wraps naturally. Respect prefers-reduced-motion. ## Tokens - `motion.ease-out-quart` — `cubic-bezier(0.165, 0.84, 0.44, 1)` - `motion.duration-base` — `300ms` ## A11y Respects prefers-reduced-motion — renders text statically when enabled. Screen readers read the complete string; the per-character animation is purely visual and does not affect semantics. ## Source (`src/components/CharacterReveal.tsx`) ```tsx import { motion, useInView, useReducedMotion } from 'motion/react'; import { useRef } from 'react'; export interface CharacterRevealProps { text: string; stagger?: number; // default 0.02 — seconds between characters duration?: number; // default 0.4 — seconds per character className?: string; } // Spitzer-style expo.out — matches --ease-expo-out token const charEase = [0.16, 1, 0.3, 1] as const; /** * Split text into wrappable segments — keeps words together * but allows breaks at spaces and after hyphens. * e.g. "Human-centered design" → ["Human-", "centered ", "design"] */ function splitIntoSegments(text: string): string[] { const segments: string[] = []; let current = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; current += char; if (char === '-' || char === ' ') { segments.push(current); current = ''; } } if (current) segments.push(current); return segments; } export function CharacterReveal({ text, stagger = 0.02, duration = 0.4, className = '', }: CharacterRevealProps) { const ref = useRef(null); const isInView = useInView(ref, { once: true, margin: '-50px' }); const prefersReduced = useReducedMotion(); const segments = splitIntoSegments(text); let charIndex = 0; if (prefersReduced) { return ( {text} ); } return ( {segments.map((segment, si) => { const trimmed = segment.trimEnd(); const hasTrailingSpace = segment !== trimmed; return ( {trimmed.split('').map((char) => { const i = charIndex++; return ( {char} ); })} {hasTrailingSpace && ( {'\u00A0'} )} ); })} ); } ``` ## Usage Use for short, high-impact strings like hero headlines, section titles, or display quotes where you want maximum cinematic entrance. The per-character animation draws the eye across the entire word before it resolves. - Keep strings under ~30 characters — longer text produces a noticeably slow tail. - The default 0.02s stagger is tuned for 5–15 character strings; increase to 0.03s for shorter text, decrease to 0.015s for longer. - **Not** for body copy or multi-sentence content — use AnimatedText (word-level) instead. --- # CustomCursor Custom branded cursor replacement with smooth trailing motion. ## Prompt Build a custom cursor component that replaces the default pointer. A small dot tracks the cursor instantly, and a larger outline ring trails with a slight lerp delay. Hide on touch devices. Respect prefers-reduced-motion — ring snaps rather than lerps. Accept props for color, dot size, and trail size. Clean up all DOM mutations (class, listeners) on unmount. ## Tokens - `color.accent` — `#E8462C` - `motion.duration-base` — `300ms` ## A11y Disabled on touch devices (doesn't mount on pointer:coarse). Falls back to native cursor if prefers-reduced-motion. Does not hide the native cursor on hover-interactive elements like inputs. Cleans up all event listeners and the global CSS class on unmount. ## Source (`src/components/CustomCursor.tsx`) ```tsx import { useEffect, useRef } from 'react'; export interface CustomCursorProps { color?: string; // default '#E8462C' size?: number; // default 12 — dot diameter in px trailSize?: number; // default 40 — trailing ring diameter in px disabled?: boolean; // default false } /** * CustomCursor — replaces the default pointer with a branded dot + trailing ring. * * Mount at the app root so it's active sitewide. In the playground, it applies * globally while the demo is mounted (which is fine for previewing). * * On touch devices (pointer:coarse) the component renders nothing. * When prefers-reduced-motion is set the cursor renders as a plain dot (no trail spring). * * Cleans up all event listeners and CSS mutations on unmount. */ export function CustomCursor({ color = '#E8462C', size = 12, trailSize = 40, disabled = false, }: CustomCursorProps) { const dotRef = useRef(null); const ringRef = useRef(null); useEffect(() => { if (disabled) return; // Touch devices — don't mount cursor if (!window.matchMedia('(pointer: fine)').matches) return; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const dot = dotRef.current!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const ring = ringRef.current!; if (!dot || !ring) return; const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; // Hide native cursor globally document.documentElement.classList.add('ds-custom-cursor'); let mouseX = -999; let mouseY = -999; let ringX = -999; let ringY = -999; let raf = 0; let visible = false; const lerp = (a: number, b: number, t: number) => a + (b - a) * t; const lerpFactor = prefersReduced ? 1 : 0.12; function tick() { if (prefersReduced) { ring.style.transform = `translate(${mouseX}px, ${mouseY}px) translate(-50%, -50%)`; } else { ringX = lerp(ringX, mouseX, lerpFactor); ringY = lerp(ringY, mouseY, lerpFactor); ring.style.transform = `translate(${ringX}px, ${ringY}px) translate(-50%, -50%)`; } raf = requestAnimationFrame(tick); } function onMouseMove(e: MouseEvent) { mouseX = e.clientX; mouseY = e.clientY; dot.style.transform = `translate(${mouseX}px, ${mouseY}px) translate(-50%, -50%)`; if (!visible) { dot.style.opacity = '1'; ring.style.opacity = '1'; if (ringX === -999) { ringX = mouseX; ringY = mouseY; } visible = true; } } function onMouseLeave() { dot.style.opacity = '0'; ring.style.opacity = '0'; visible = false; } function onMouseEnter() { dot.style.opacity = '1'; ring.style.opacity = '1'; visible = true; } function onMouseOver(e: MouseEvent) { const target = e.target as HTMLElement; const interactive = target.closest('a, button, [role="button"], input, textarea, select, label'); const isText = !interactive && target.closest('p, span, h1, h2, h3, h4, h5, h6, li, td, th, blockquote'); if (interactive) { dot.classList.add('is-interactive'); dot.classList.remove('is-text'); ring.classList.add('is-interactive'); ring.classList.remove('is-text'); } else if (isText) { dot.classList.add('is-text'); dot.classList.remove('is-interactive'); ring.classList.add('is-text'); ring.classList.remove('is-interactive'); } else { dot.classList.remove('is-interactive', 'is-text'); ring.classList.remove('is-interactive', 'is-text'); } } raf = requestAnimationFrame(tick); document.addEventListener('mousemove', onMouseMove, { passive: true }); document.addEventListener('mouseleave', onMouseLeave, { passive: true }); document.addEventListener('mouseenter', onMouseEnter, { passive: true }); document.addEventListener('mouseover', onMouseOver, { passive: true }); return () => { cancelAnimationFrame(raf); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseleave', onMouseLeave); document.removeEventListener('mouseenter', onMouseEnter); document.removeEventListener('mouseover', onMouseOver); document.documentElement.classList.remove('ds-custom-cursor'); }; }, [disabled, color, size, trailSize]); if (disabled) return null; return ( <> {/* Dot — instant follow */}