{
  "name": "textlab",
  "homepage": "https://textlab.dev",
  "count": 70,
  "categories": [
    {
      "id": "reveal",
      "title": "Reveal",
      "blurb": "Bring text in"
    },
    {
      "id": "numbers",
      "title": "Numbers",
      "blurb": "Values that move"
    },
    {
      "id": "typing",
      "title": "Typing",
      "blurb": "Text that writes itself"
    },
    {
      "id": "light",
      "title": "Light",
      "blurb": "Color, glow and sheen"
    },
    {
      "id": "distort",
      "title": "Distort",
      "blurb": "Filters that bend type"
    },
    {
      "id": "scroll",
      "title": "Scroll",
      "blurb": "Driven by the page"
    },
    {
      "id": "draw",
      "title": "Draw",
      "blurb": "Pen strokes and marks"
    },
    {
      "id": "cursor",
      "title": "Cursor",
      "blurb": "React to the pointer"
    },
    {
      "id": "physics",
      "title": "Physics",
      "blurb": "Springs and gravity"
    },
    {
      "id": "path",
      "title": "Path",
      "blurb": "Type on a curve"
    },
    {
      "id": "pixel",
      "title": "Pixel",
      "blurb": "Canvas and shaders"
    },
    {
      "id": "kinetic",
      "title": "Kinetic",
      "blurb": "Choreographed words"
    }
  ],
  "items": [
    {
      "name": "blur-reveal",
      "title": "Blur reveal",
      "description": "Words sharpen into focus.",
      "category": "reveal",
      "exportName": "BlurReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "word",
        "char",
        "line"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"word\" | \"char\" | \"line\"",
          "default": "\"word\"",
          "description": "What sharpens on its own: each word, each letter or each measured line."
        },
        {
          "name": "blur",
          "type": "number",
          "default": "12",
          "description": "Starting blur in px."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.06 (word), 0.02 (char), 0.1 (line)"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<BlurReveal as=\"h2\" text=\"Make every word count\" />",
      "whenToUse": [
        "Hero and section titles that should feel calm and deliberate.",
        "Short lines of copy, up to two sentences."
      ],
      "whenNotToUse": [
        "Body paragraphs. Use scroll-fill for long copy that follows the reader.",
        "Labels that change often. Use text-morph."
      ],
      "accessibility": [
        "The full text is in a visually hidden span; the animated parts are aria-hidden.",
        "Keeps the heading tag you pass with as."
      ],
      "motion": [
        "Triggers when 45% of the element is visible.",
        "Opacity leads, blur clears next, the rise settles last."
      ],
      "reducedMotion": "Shows the text immediately with no blur or movement.",
      "performance": [
        "Animates opacity, filter and transform only.",
        "Blur is the costliest part; keep char variant under ~80 letters."
      ],
      "related": [
        "word-reveal",
        "in-view-title",
        "focus-pull"
      ],
      "keywords": [
        "blur",
        "focus",
        "fade",
        "in view",
        "heading"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "word",
            "char",
            "line"
          ]
        },
        {
          "prop": "blur",
          "kind": "range",
          "min": 2,
          "max": 20,
          "step": 1
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.2,
          "max": 2,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Make every word count"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/blur-reveal",
      "markdown": "https://textlab.dev/components/blur-reveal.md",
      "registry": "https://textlab.dev/r/blur-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/blur-reveal.json"
    },
    {
      "name": "word-reveal",
      "title": "Word reveal",
      "description": "Words rise out of a clip.",
      "category": "reveal",
      "exportName": "WordReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "rise",
        "fall",
        "scale"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"rise\" | \"fall\" | \"scale\"",
          "default": "\"rise\"",
          "description": "rise comes up from below the line, fall drops from above, scale grows from the baseline."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.06"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<WordReveal as=\"h1\" text=\"Ship the story, not the slides\" />",
      "whenToUse": [
        "Editorial headlines where each word should land with weight.",
        "Hero lines on load with trigger=\"mount\"."
      ],
      "whenNotToUse": [
        "Long paragraphs; the stagger gets slow past ~20 words.",
        "Text inside buttons or form labels."
      ],
      "accessibility": [
        "Screen readers get the full sentence once; the clipped words are aria-hidden.",
        "Pass as=\"h1\" or as=\"h2\" to keep the outline."
      ],
      "motion": [
        "Each word travels its own height, so the motion scales with font size.",
        "Words start 60 ms apart and settle with the enter ease."
      ],
      "reducedMotion": "Renders the sentence in place with no clip or movement.",
      "performance": [
        "Transform and opacity only.",
        "One motion span per word."
      ],
      "responsive": [
        "Words wrap naturally at any width; each word animates on its own line."
      ],
      "related": [
        "line-reveal",
        "blur-reveal",
        "in-view-title"
      ],
      "keywords": [
        "word",
        "rise",
        "clip",
        "mask",
        "heading",
        "stagger"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "rise",
            "fall",
            "scale"
          ]
        },
        {
          "prop": "stagger",
          "kind": "range",
          "min": 0.02,
          "max": 0.2,
          "step": 0.01
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.3,
          "max": 1.6,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Ship the story, not the slides"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/word-reveal",
      "markdown": "https://textlab.dev/components/word-reveal.md",
      "registry": "https://textlab.dev/r/word-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/word-reveal.json"
    },
    {
      "name": "line-reveal",
      "title": "Line reveal",
      "description": "Lines lift in, re-measured on resize.",
      "category": "reveal",
      "exportName": "LineReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "rise",
        "mask",
        "tilt"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"rise\" | \"mask\" | \"tilt\"",
          "default": "\"rise\"",
          "description": "rise lifts from a clip, mask uncovers top to bottom, tilt swings up on a hinge."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.1"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<LineReveal as=\"p\" text=\"Lines are measured from the real layout, so the reveal follows the wrap at every width.\" />",
      "whenToUse": [
        "Two to four line intros and pull quotes.",
        "Copy whose wrap changes across breakpoints."
      ],
      "whenNotToUse": [
        "Single words; use word-reveal.",
        "Body text longer than a short paragraph."
      ],
      "accessibility": [
        "The sentence is read once from a visually hidden copy.",
        "Line groups are aria-hidden presentation only."
      ],
      "motion": [
        "Lines start 100 ms apart.",
        "Line breaks are measured after fonts load and again whenever the width changes."
      ],
      "reducedMotion": "Shows the paragraph as normal text with no grouping or movement.",
      "performance": [
        "One measure per frame at most, triggered by ResizeObserver.",
        "Transform, opacity and clip-path only."
      ],
      "responsive": [
        "Re-groups lines on resize, so narrow screens get more, shorter lines."
      ],
      "related": [
        "word-reveal",
        "in-view-title",
        "split-text"
      ],
      "keywords": [
        "line",
        "paragraph",
        "resize",
        "wrap",
        "mask",
        "tilt"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "rise",
            "mask",
            "tilt"
          ]
        },
        {
          "prop": "stagger",
          "kind": "range",
          "min": 0.04,
          "max": 0.3,
          "step": 0.01
        }
      ],
      "preset": {
        "text": "Lines are measured from the real layout, so the reveal follows the wrap at every width."
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/line-reveal",
      "markdown": "https://textlab.dev/components/line-reveal.md",
      "registry": "https://textlab.dev/r/line-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/line-reveal.json"
    },
    {
      "name": "char-reveal",
      "title": "Character reveal",
      "description": "Letters arrive one at a time.",
      "category": "reveal",
      "exportName": "CharReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "fade",
        "drop",
        "spin",
        "scale"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"fade\" | \"drop\" | \"spin\" | \"scale\"",
          "default": "\"fade\"",
          "description": "fade is quiet, drop falls into place, spin turns up from the baseline, scale pops up from small."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.02"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.6 (fade), 0.7"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<CharReveal variant=\"drop\" text=\"Hello there\" />",
      "whenToUse": [
        "Short display words and names.",
        "Moments where the arrival itself is the point."
      ],
      "whenNotToUse": [
        "Anything over ~60 letters; the tail gets long.",
        "Text people need to read quickly."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Grapheme-aware, so emoji and accents never split."
      ],
      "motion": [
        "20 ms between letters; the total stays under a second for most words.",
        "Words are nowrap, so lines never jump while letters move."
      ],
      "reducedMotion": "Shows the text immediately.",
      "performance": [
        "One motion span per letter. Fine for headings, not paragraphs."
      ],
      "related": [
        "blur-reveal",
        "split-text",
        "focus-pull"
      ],
      "keywords": [
        "character",
        "letter",
        "stagger",
        "drop",
        "spin",
        "pop"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "fade",
            "drop",
            "spin",
            "scale"
          ]
        },
        {
          "prop": "stagger",
          "kind": "range",
          "min": 0.005,
          "max": 0.08,
          "step": 0.005
        }
      ],
      "preset": {
        "text": "Every letter lands"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/char-reveal",
      "markdown": "https://textlab.dev/components/char-reveal.md",
      "registry": "https://textlab.dev/r/char-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/char-reveal.json"
    },
    {
      "name": "wipe-reveal",
      "title": "Wipe reveal",
      "description": "A soft edge uncovers the line.",
      "category": "reveal",
      "exportName": "WipeReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "left",
        "right",
        "center",
        "diagonal"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"left\" | \"right\" | \"center\" | \"diagonal\"",
          "default": "\"left\"",
          "description": "Direction of the sweep; center opens from the middle."
        },
        {
          "name": "softness",
          "type": "number",
          "default": "0.18",
          "description": "Width of the feathered edge as a share of the text, 0 to 0.5."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"standard\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<WipeReveal as=\"h2\" variant=\"center\" text=\"Unveil the headline\" />",
      "whenToUse": [
        "Titles that should feel unveiled rather than assembled.",
        "Text that must stay selectable and unsplit."
      ],
      "whenNotToUse": [
        "Multi-line paragraphs; the sweep crosses the whole block at once.",
        "When you need per-word timing; use word-reveal."
      ],
      "accessibility": [
        "The text is never split, so assistive tech reads it as plain text.",
        "Mask padding keeps descenders inside without changing layout."
      ],
      "motion": [
        "The edge travels with the standard ease so the sweep speed feels even.",
        "It overshoots by the soft width so the last letter finishes fully opaque."
      ],
      "reducedMotion": "Shows the text with no mask.",
      "performance": [
        "Animates one CSS variable on one element; the mask is GPU composited."
      ],
      "related": [
        "in-view-title",
        "line-reveal",
        "outline-draw"
      ],
      "keywords": [
        "wipe",
        "mask",
        "sweep",
        "unveil",
        "gradient"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "left",
            "right",
            "center",
            "diagonal"
          ]
        },
        {
          "prop": "softness",
          "kind": "range",
          "min": 0,
          "max": 0.5,
          "step": 0.02
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.4,
          "max": 2.4,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Unveil the headline"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/wipe-reveal",
      "markdown": "https://textlab.dev/components/wipe-reveal.md",
      "registry": "https://textlab.dev/r/wipe-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/wipe-reveal.json"
    },
    {
      "name": "in-view-title",
      "title": "In-view title",
      "description": "Bring a section title in as it scrolls into view.",
      "category": "reveal",
      "exportName": "InViewTitle",
      "dependencies": [
        "motion"
      ],
      "uses": [
        "blur-reveal",
        "word-reveal",
        "line-reveal",
        "wipe-reveal"
      ],
      "variants": [
        "blur",
        "words",
        "lines",
        "wipe"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"blur\" | \"words\" | \"lines\" | \"wipe\"",
          "default": "\"blur\"",
          "description": "blur suits most headings; words and lines rise from a clip; wipe uncovers left to right."
        },
        {
          "name": "as",
          "type": "\"h1\" | \"h2\" | \"h3\" | \"h4\"",
          "default": "\"h2\"",
          "description": "Heading level."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "amount",
          "type": "number",
          "default": "0.45",
          "description": "Share of the title that must be visible before it starts."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "id",
          "type": "string",
          "description": "Forwarded to the heading, for anchor links."
        }
      ],
      "usage": "<InViewTitle variant=\"words\" text=\"Make every word count\" />",
      "whenToUse": [
        "Section headings that should animate as the reader arrives.",
        "Pages where every section title needs the same entrance."
      ],
      "whenNotToUse": [
        "Hero copy that animates on load; use blur-reveal with trigger=\"mount\".",
        "A paragraph that follows scroll progress; use scroll-fill."
      ],
      "accessibility": [
        "Renders a real heading tag with the full text for screen readers.",
        "Choose as to match the page outline."
      ],
      "motion": [
        "Starts when 45% of the title is visible.",
        "Titles already scrolled past show immediately.",
        "Hiding with once={false} is a quick fade with no stagger."
      ],
      "reducedMotion": "The title appears instantly with no blur, clip or mask.",
      "performance": [
        "One IntersectionObserver per title.",
        "Fine for headings, not long body text."
      ],
      "responsive": [
        "Words, blur and wipe wrap naturally; lines are re-measured on resize."
      ],
      "related": [
        "blur-reveal",
        "word-reveal",
        "wipe-reveal"
      ],
      "keywords": [
        "in view",
        "scroll",
        "section title",
        "heading",
        "reveal on scroll"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "blur",
            "words",
            "lines",
            "wipe"
          ]
        }
      ],
      "preset": {
        "text": "Make every word count"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/in-view-title",
      "markdown": "https://textlab.dev/components/in-view-title.md",
      "registry": "https://textlab.dev/r/in-view-title.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/in-view-title.json"
    },
    {
      "name": "split-text",
      "title": "Split text",
      "description": "Stagger anything by chars, words or lines.",
      "category": "reveal",
      "exportName": "SplitText",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "words",
        "chars",
        "lines"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"chars\" | \"words\" | \"lines\"",
          "default": "\"words\"",
          "description": "How the text is split. Lines are measured from layout and re-measured on resize."
        },
        {
          "name": "preset",
          "type": "\"fade-up\" | \"fade-down\" | \"zoom\" | \"blur\" | \"flip\"",
          "default": "\"fade-up\"",
          "description": "A named starting pose."
        },
        {
          "name": "from",
          "type": "TargetAndTransition | (index, total) => TargetAndTransition",
          "description": "Custom starting pose per part. Overrides preset."
        },
        {
          "name": "order",
          "type": "\"start\" | \"end\" | \"center\" | \"edges\"",
          "default": "\"start\"",
          "description": "Which part moves first."
        },
        {
          "name": "clip",
          "type": "boolean",
          "default": "false",
          "description": "Clip each part so it appears out of nothing."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<SplitText variant=\"chars\" order=\"center\" preset=\"zoom\" text=\"From the middle out\" />",
      "whenToUse": [
        "Custom entrances the other reveals do not cover.",
        "Choreography where order matters: center out, edges in."
      ],
      "whenNotToUse": [
        "When a named reveal already fits; they carry better defaults.",
        "Long body copy split by chars."
      ],
      "accessibility": [
        "One visually hidden copy for screen readers; every part is aria-hidden.",
        "Splits by grapheme, so emoji and combined letters stay whole."
      ],
      "motion": [
        "Default stagger: 20 ms per char, 60 ms per word, 100 ms per line.",
        "Opacity leads, filter follows, transform settles last."
      ],
      "reducedMotion": "Shows plain text with no splitting.",
      "performance": [
        "Up to ~200 parts per instance.",
        "Line mode measures once per frame on resize."
      ],
      "related": [
        "char-reveal",
        "line-reveal",
        "blur-reveal"
      ],
      "keywords": [
        "split",
        "stagger",
        "chars",
        "words",
        "lines",
        "splittext"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "words",
            "chars",
            "lines"
          ]
        },
        {
          "prop": "preset",
          "kind": "select",
          "options": [
            "fade-up",
            "fade-down",
            "zoom",
            "blur",
            "flip"
          ]
        },
        {
          "prop": "order",
          "kind": "select",
          "options": [
            "start",
            "end",
            "center",
            "edges"
          ]
        }
      ],
      "preset": {
        "text": "Split it any way you like"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/split-text",
      "markdown": "https://textlab.dev/components/split-text.md",
      "registry": "https://textlab.dev/r/split-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/split-text.json"
    },
    {
      "name": "tracking-reveal",
      "title": "Tracking reveal",
      "description": "Letter spacing breathes open as it appears.",
      "category": "reveal",
      "exportName": "TrackingReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "open",
        "close"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"open\" | \"close\"",
          "default": "\"open\"",
          "description": "open starts packed and breathes out; close starts wide and gathers in."
        },
        {
          "name": "spread",
          "type": "number",
          "default": "0.08",
          "description": "Distance each letter travels per step from the center, in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.1"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"emphasized\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<TrackingReveal as=\"h2\" text=\"Quiet confidence\" />",
      "whenToUse": [
        "Luxury or editorial titles where restraint matters.",
        "Short uppercase labels and wordmarks."
      ],
      "whenNotToUse": [
        "Long titles; the outer letters travel far.",
        "Body text."
      ],
      "accessibility": [
        "Letters are aria-hidden; the title is read once.",
        "No reflow, so surrounding layout never moves."
      ],
      "motion": [
        "Every letter moves together; distance grows with its offset from the center.",
        "Uses the emphasized ease for a long, soft landing."
      ],
      "reducedMotion": "Shows the text at its normal spacing.",
      "performance": [
        "Transforms instead of letter-spacing, so no layout work per frame."
      ],
      "related": [
        "focus-pull",
        "blur-reveal",
        "char-reveal"
      ],
      "keywords": [
        "tracking",
        "letter spacing",
        "kerning",
        "luxury",
        "breathe"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "open",
            "close"
          ]
        },
        {
          "prop": "spread",
          "kind": "range",
          "min": 0.02,
          "max": 0.3,
          "step": 0.01
        }
      ],
      "preset": {
        "text": "Quiet confidence"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/tracking-reveal",
      "markdown": "https://textlab.dev/components/tracking-reveal.md",
      "registry": "https://textlab.dev/r/tracking-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/tracking-reveal.json"
    },
    {
      "name": "slice-reveal",
      "title": "Slice reveal",
      "description": "The title arrives in offset horizontal slices.",
      "category": "reveal",
      "exportName": "SliceReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "5",
        "3",
        "8"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"3\" | \"5\" | \"8\"",
          "default": "\"5\"",
          "description": "Number of horizontal slices."
        },
        {
          "name": "offset",
          "type": "number",
          "default": "0.35",
          "description": "Starting distance of each slice as a share of the width."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.05"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"emphasized\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<SliceReveal as=\"h2\" variant=\"8\" text=\"Registered\" />",
      "whenToUse": [
        "Bold single-line display type, posters and title cards.",
        "Brands with a print or editorial feel."
      ],
      "whenNotToUse": [
        "Text that wraps onto several lines; slices are cut across the whole block.",
        "Small sizes where slices are thinner than a stroke."
      ],
      "accessibility": [
        "The slices are aria-hidden copies; screen readers read the text once.",
        "An invisible copy reserves the exact size, so nothing shifts."
      ],
      "motion": [
        "Slices alternate direction and start 50 ms apart.",
        "Opacity resolves early so the shape reads before the slices align."
      ],
      "reducedMotion": "Shows the line whole and still.",
      "performance": [
        "N copies of the text, each with a static clip-path and a transform."
      ],
      "responsive": [
        "Keeps the line on one row; size it with a fluid font size."
      ],
      "related": [
        "glitch-text",
        "word-reveal",
        "wipe-reveal"
      ],
      "keywords": [
        "slice",
        "stripes",
        "offset",
        "print",
        "registration",
        "poster"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "3",
            "5",
            "8"
          ]
        },
        {
          "prop": "offset",
          "kind": "range",
          "min": 0.05,
          "max": 1,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Registered"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/slice-reveal",
      "markdown": "https://textlab.dev/components/slice-reveal.md",
      "registry": "https://textlab.dev/r/slice-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/slice-reveal.json"
    },
    {
      "name": "focus-pull",
      "title": "Focus pull",
      "description": "Focus racks from the center letter outward.",
      "category": "reveal",
      "exportName": "FocusPull",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "center",
        "start",
        "end"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"center\" | \"start\" | \"end\"",
          "default": "\"center\"",
          "description": "Where focus lands first."
        },
        {
          "name": "blur",
          "type": "number",
          "default": "14",
          "description": "Starting blur in px."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.045"
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.9"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"enter\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<FocusPull as=\"h1\" text=\"Look closer\" />",
      "whenToUse": [
        "Cinematic title cards and intros.",
        "A single word that deserves a moment."
      ],
      "whenNotToUse": [
        "Text that must be readable immediately.",
        "More than about 30 letters."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Starts at low opacity rather than invisible, so the shape is present early."
      ],
      "motion": [
        "Letters sharpen in order of distance from the focal letter.",
        "Scale settles with blur, like a lens finding focus."
      ],
      "reducedMotion": "The text is sharp and still from the start.",
      "performance": [
        "Per-letter blur is costly; keep it to display words."
      ],
      "related": [
        "blur-reveal",
        "tracking-reveal",
        "char-reveal"
      ],
      "keywords": [
        "focus",
        "rack focus",
        "lens",
        "blur",
        "cinematic"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "center",
            "start",
            "end"
          ]
        },
        {
          "prop": "blur",
          "kind": "range",
          "min": 4,
          "max": 20,
          "step": 1
        }
      ],
      "preset": {
        "text": "Look closer"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/focus-pull",
      "markdown": "https://textlab.dev/components/focus-pull.md",
      "registry": "https://textlab.dev/r/focus-pull.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/focus-pull.json"
    },
    {
      "name": "rolling-number",
      "title": "Rolling number",
      "description": "Each digit rolls to its new value.",
      "category": "numbers",
      "exportName": "RollingNumber",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "shortest",
        "up",
        "down"
      ],
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The number to show.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"shortest\" | \"up\" | \"down\"",
          "default": "\"shortest\"",
          "description": "shortest takes the nearer way round; up and down always roll one way."
        },
        {
          "name": "format",
          "type": "Intl.NumberFormatOptions",
          "description": "Formatting, for example currency or compact notation."
        },
        {
          "name": "locale",
          "type": "string",
          "default": "\"en-US\"",
          "description": "Fixed by default so server and client render the same string."
        },
        {
          "name": "duration",
          "type": "number",
          "default": "0.6",
          "description": "Seconds a digit takes to settle."
        },
        {
          "name": "bounce",
          "type": "number",
          "default": "0.1",
          "description": "Spring bounce, 0 to 0.5."
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "false",
          "description": "Announce changes politely to screen readers."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<RollingNumber value={count} format={{ style: \"currency\", currency: \"USD\" }} />",
      "whenToUse": [
        "Counters, balances and scores that change while people watch.",
        "Dashboards where the change itself is information."
      ],
      "whenNotToUse": [
        "Numbers that never change after load; use stat-reveal.",
        "Values updating many times a second; the motion becomes noise."
      ],
      "accessibility": [
        "Screen readers read the formatted value, not the reels.",
        "Set live to announce updates; leave it off for values that tick constantly."
      ],
      "motion": [
        "Each reel runs on a spring, so rapid updates interrupt smoothly.",
        "Columns are keyed from the right; new leading digits slide in and old ones slide out."
      ],
      "reducedMotion": "The new value replaces the old one with no rolling.",
      "performance": [
        "One spring per digit. Reels re-center silently after each roll so the DOM stays fixed size."
      ],
      "related": [
        "odometer",
        "price-ticker",
        "stat-reveal"
      ],
      "keywords": [
        "number",
        "counter",
        "digits",
        "roll",
        "slot",
        "animated number",
        "ticker"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "shortest",
            "up",
            "down"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.2,
          "max": 1.5,
          "step": 0.05
        },
        {
          "prop": "bounce",
          "kind": "range",
          "min": 0,
          "max": 0.5,
          "step": 0.05
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/rolling-number",
      "markdown": "https://textlab.dev/components/rolling-number.md",
      "registry": "https://textlab.dev/r/rolling-number.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/rolling-number.json"
    },
    {
      "name": "odometer",
      "title": "Odometer",
      "description": "A mechanical wheel that spins past every digit.",
      "category": "numbers",
      "exportName": "Odometer",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "mechanical",
        "smooth"
      ],
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "A whole, non-negative number.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"mechanical\" | \"smooth\"",
          "default": "\"mechanical\"",
          "description": "mechanical clicks a wheel over only when its neighbour passes 9; smooth spins every wheel continuously."
        },
        {
          "name": "digits",
          "type": "number",
          "default": "1",
          "description": "Minimum number of wheels, padded with zeros."
        },
        {
          "name": "duration",
          "type": "number",
          "default": "1.6",
          "description": "Seconds to travel to the new value."
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"standard\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "framed",
          "type": "boolean",
          "default": "false",
          "description": "Thin separators between wheels."
        },
        {
          "name": "locale",
          "type": "string",
          "default": "\"en-US\"",
          "description": "Used for the screen-reader label."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<Odometer value={miles} digits={6} framed />",
      "whenToUse": [
        "Visitor counts, mileage and totals with a physical feel.",
        "Big jumps where you want people to sense the distance travelled."
      ],
      "whenNotToUse": [
        "Currency with decimals; use rolling-number or price-ticker.",
        "Negative numbers."
      ],
      "accessibility": [
        "Screen readers get the formatted number once; the wheels are aria-hidden."
      ],
      "motion": [
        "One motion value drives every wheel, so they always agree.",
        "Edges are masked so wheels read as curved drums."
      ],
      "reducedMotion": "The new value appears immediately.",
      "performance": [
        "One animation per change; wheel positions are derived transforms."
      ],
      "related": [
        "rolling-number",
        "split-flap",
        "stat-reveal"
      ],
      "keywords": [
        "odometer",
        "counter",
        "mechanical",
        "wheel",
        "mileage",
        "meter"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "mechanical",
            "smooth"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.4,
          "max": 4,
          "step": 0.1
        },
        {
          "prop": "framed",
          "kind": "toggle"
        }
      ],
      "preset": {
        "digits": 6
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/odometer",
      "markdown": "https://textlab.dev/components/odometer.md",
      "registry": "https://textlab.dev/r/odometer.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/odometer.json"
    },
    {
      "name": "split-flap",
      "title": "Split-flap board",
      "description": "An airport board that flips to the next word.",
      "category": "numbers",
      "exportName": "SplitFlap",
      "dependencies": [],
      "variants": [
        "dark",
        "light",
        "mini"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"dark\" | \"light\" | \"mini\"",
          "default": "\"dark\"",
          "description": "Classic black board, cream tiles, or compact tiles that take the text color."
        },
        {
          "name": "length",
          "type": "number",
          "description": "Pad or trim to this many tiles. Keep it fixed when the text changes."
        },
        {
          "name": "charset",
          "type": "string",
          "default": "\" A–Z 0–9 :.-/\"",
          "description": "Characters each tile cycles through. Others jump straight in."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "50",
          "description": "Milliseconds per half flip."
        },
        {
          "name": "stagger",
          "type": "number",
          "default": "35",
          "description": "Milliseconds between neighbouring tiles starting."
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "false",
          "description": "Announce changes politely to screen readers."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<SplitFlap text={destination} length={10} />",
      "whenToUse": [
        "Status boards, departures, scoreboards and countdown labels.",
        "Short uppercase words and codes."
      ],
      "whenNotToUse": [
        "Lowercase prose; the default charset is uppercase.",
        "Long sentences; each tile flips through the alphabet."
      ],
      "accessibility": [
        "Screen readers read the text once, not the flipping letters.",
        "Tiles have a fixed width, so the line never shifts."
      ],
      "motion": [
        "Each flip is two halves: the top leaf falls with ease-in, the bottom leaf lands with ease-out.",
        "Tiles start 35 ms apart from left to right."
      ],
      "reducedMotion": "Tiles show the final letters at once.",
      "performance": [
        "CSS keyframes only; no JavaScript animation loop.",
        "A tile does work only while it is flipping."
      ],
      "related": [
        "odometer",
        "countdown",
        "scramble-text"
      ],
      "keywords": [
        "split flap",
        "solari",
        "airport",
        "departures",
        "board",
        "flip"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "dark",
            "light",
            "mini"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 30,
          "max": 160,
          "step": 5
        }
      ],
      "preset": {
        "text": "LISBON",
        "length": 9
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/split-flap",
      "markdown": "https://textlab.dev/components/split-flap.md",
      "registry": "https://textlab.dev/r/split-flap.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/split-flap.json"
    },
    {
      "name": "price-ticker",
      "title": "Price ticker",
      "description": "Prices tick with a flash of up or down.",
      "category": "numbers",
      "exportName": "PriceTicker",
      "dependencies": [
        "motion"
      ],
      "uses": [
        "rolling-number"
      ],
      "variants": [
        "currency",
        "percent",
        "compact"
      ],
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The price, or a ratio of 1 for percent.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"currency\" | \"percent\" | \"compact\"",
          "default": "\"currency\"",
          "description": "Money with cents, a percentage, or compact money like $1.2K."
        },
        {
          "name": "currency",
          "type": "string",
          "default": "\"USD\"",
          "description": "ISO currency code."
        },
        {
          "name": "locale",
          "type": "string",
          "default": "\"en-US\"",
          "description": "Formatting locale."
        },
        {
          "name": "showDelta",
          "type": "boolean",
          "default": "true",
          "description": "Show the change next to the price."
        },
        {
          "name": "upColor",
          "type": "string",
          "default": "green",
          "description": "Flash and delta color for rises."
        },
        {
          "name": "downColor",
          "type": "string",
          "default": "red",
          "description": "Flash and delta color for falls."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<PriceTicker value={price} currency=\"EUR\" />",
      "whenToUse": [
        "Live prices, portfolio values and conversion rates.",
        "Any number where the direction of change matters as much as the value."
      ],
      "whenNotToUse": [
        "Static prices on a pricing page.",
        "Values that change more than a few times a second."
      ],
      "accessibility": [
        "The delta is announced politely, so screen readers hear the direction.",
        "Direction is shown by an arrow and a sign, not color alone."
      ],
      "motion": [
        "Digits roll up on a rise and down on a fall.",
        "The flash holds briefly, then fades to the text color over 1.2 s."
      ],
      "reducedMotion": "The price and delta update in place with no roll or flash.",
      "performance": [
        "Built on rolling-number; one spring per digit."
      ],
      "related": [
        "rolling-number",
        "stat-reveal",
        "countdown"
      ],
      "keywords": [
        "price",
        "stock",
        "ticker",
        "currency",
        "crypto",
        "delta",
        "up down"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "currency",
            "percent",
            "compact"
          ]
        },
        {
          "prop": "showDelta",
          "kind": "toggle"
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/price-ticker",
      "markdown": "https://textlab.dev/components/price-ticker.md",
      "registry": "https://textlab.dev/r/price-ticker.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/price-ticker.json"
    },
    {
      "name": "countdown",
      "title": "Countdown",
      "description": "Count down to a moment, second by second.",
      "category": "numbers",
      "exportName": "Countdown",
      "dependencies": [
        "motion"
      ],
      "uses": [
        "rolling-number",
        "split-flap"
      ],
      "variants": [
        "flip",
        "roll",
        "plain"
      ],
      "props": [
        {
          "name": "to",
          "type": "Date | string | number",
          "description": "The moment to count down to.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"flip\" | \"roll\" | \"plain\"",
          "default": "\"flip\"",
          "description": "Split-flap tiles, rolling digits, or a soft crossfade."
        },
        {
          "name": "units",
          "type": "(\"days\" | \"hours\" | \"minutes\" | \"seconds\")[]",
          "default": "all four",
          "description": "Units to show, largest first."
        },
        {
          "name": "labels",
          "type": "Partial<Record<Unit, string>> | null",
          "default": "English labels",
          "description": "Labels under each unit. null hides them."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<Countdown to=\"2026-12-31T23:59:59Z\" variant=\"roll\" />",
      "whenToUse": [
        "Launches, sales and events with a fixed start time.",
        "Timers people glance at rather than read continuously."
      ],
      "whenNotToUse": [
        "Elapsed-time stopwatches; this only counts down.",
        "Precise timing below one second."
      ],
      "accessibility": [
        "A visually hidden sentence reads the remaining time in words.",
        "It is not a live region, so it never interrupts every second."
      ],
      "motion": [
        "Ticks align to the wall clock, so every second changes on the beat.",
        "Seconds use the same flip or roll as the larger units."
      ],
      "reducedMotion": "Digits change in place with no flip, roll or fade.",
      "performance": [
        "One timer per countdown, scheduled for the next whole second.",
        "Only units that change animate."
      ],
      "responsive": [
        "Scales with font size; set a fluid size on the root."
      ],
      "related": [
        "split-flap",
        "rolling-number",
        "stat-reveal"
      ],
      "keywords": [
        "countdown",
        "timer",
        "launch",
        "clock",
        "days hours minutes seconds"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "flip",
            "roll",
            "plain"
          ]
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/countdown",
      "markdown": "https://textlab.dev/components/countdown.md",
      "registry": "https://textlab.dev/r/countdown.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/countdown.json"
    },
    {
      "name": "stat-reveal",
      "title": "Stat reveal",
      "description": "A stat counts up when it scrolls in.",
      "category": "numbers",
      "exportName": "StatReveal",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "count",
        "spring",
        "scramble"
      ],
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The final number.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"count\" | \"spring\" | \"scramble\"",
          "default": "\"count\"",
          "description": "Ease up, overshoot and settle, or shuffle digits then lock in."
        },
        {
          "name": "from",
          "type": "number",
          "default": "0",
          "description": "Where the count starts."
        },
        {
          "name": "prefix",
          "type": "string",
          "description": "Text before the number."
        },
        {
          "name": "suffix",
          "type": "string",
          "description": "Text after the number."
        },
        {
          "name": "format",
          "type": "Intl.NumberFormatOptions",
          "description": "Number formatting."
        },
        {
          "name": "locale",
          "type": "string",
          "default": "\"en-US\"",
          "description": "Formatting locale."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.8"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"emphasized\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<StatReveal value={12400} suffix=\"+\" />",
      "whenToUse": [
        "Stats bands and impact numbers on marketing pages.",
        "Any number shown once that deserves a moment of arrival."
      ],
      "whenNotToUse": [
        "Numbers that keep changing; use rolling-number.",
        "Several dozen stats on one screen."
      ],
      "accessibility": [
        "Screen readers read the final value, never the in-between numbers.",
        "The final width is reserved, so nothing shifts while counting."
      ],
      "motion": [
        "Counts with the emphasized ease: fast early, slow landing.",
        "Starts when 45% of the stat is visible."
      ],
      "reducedMotion": "Shows the final value immediately.",
      "performance": [
        "One motion value; text updates outside React renders for count and spring."
      ],
      "related": [
        "rolling-number",
        "odometer",
        "price-ticker"
      ],
      "keywords": [
        "stat",
        "count up",
        "counter",
        "metric",
        "kpi",
        "number reveal"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "count",
            "spring",
            "scramble"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.5,
          "max": 4,
          "step": 0.1
        }
      ],
      "preset": {
        "value": 12400,
        "suffix": "+"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/stat-reveal",
      "markdown": "https://textlab.dev/components/stat-reveal.md",
      "registry": "https://textlab.dev/r/stat-reveal.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/stat-reveal.json"
    },
    {
      "name": "typewriter",
      "title": "Typewriter",
      "description": "Types, deletes and retypes with a caret.",
      "category": "typing",
      "exportName": "Typewriter",
      "dependencies": [],
      "variants": [
        "bar",
        "block",
        "underscore"
      ],
      "props": [
        {
          "name": "words",
          "type": "string[]",
          "description": "Phrases to type in turn.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"bar\" | \"block\" | \"underscore\"",
          "default": "\"bar\"",
          "description": "Caret shape."
        },
        {
          "name": "typeSpeed",
          "type": "number",
          "default": "70",
          "description": "Milliseconds per typed letter, before jitter."
        },
        {
          "name": "deleteSpeed",
          "type": "number",
          "default": "38",
          "description": "Milliseconds per deleted letter."
        },
        {
          "name": "pause",
          "type": "number",
          "default": "1600",
          "description": "Milliseconds a finished phrase is held."
        },
        {
          "name": "loop",
          "type": "boolean",
          "default": "true",
          "description": "Keep cycling through the phrases."
        },
        {
          "name": "humanize",
          "type": "boolean",
          "default": "true",
          "description": "Small, repeatable variations in key timing."
        },
        {
          "name": "reserve",
          "type": "boolean",
          "default": "false",
          "description": "Reserve the longest phrase's width so nothing around it moves."
        },
        {
          "name": "label",
          "type": "string",
          "description": "What screen readers hear. Defaults to every phrase."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<Typewriter words={[\"for designers\", \"for engineers\", \"for everyone\"]} reserve />",
      "whenToUse": [
        "Rotating endings on hero lines.",
        "Terminal and chat-style interfaces."
      ],
      "whenNotToUse": [
        "Critical information that must be readable at once.",
        "More than a handful of phrases."
      ],
      "accessibility": [
        "Screen readers hear every phrase once from a hidden label, not each keystroke.",
        "Use reserve in centered layouts so surrounding text does not shift."
      ],
      "motion": [
        "The caret is solid while typing or deleting and blinks only at rest.",
        "Spaces take a little longer, like a real typist."
      ],
      "reducedMotion": "Shows a phrase in full with no typing and no caret.",
      "performance": [
        "One timer at a time; no animation frames.",
        "The caret blink is a CSS keyframe."
      ],
      "related": [
        "scramble-text",
        "caret-morph",
        "word-rotator"
      ],
      "keywords": [
        "typewriter",
        "typing",
        "caret",
        "cursor",
        "type and delete",
        "terminal"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "bar",
            "block",
            "underscore"
          ]
        },
        {
          "prop": "typeSpeed",
          "kind": "range",
          "min": 20,
          "max": 200,
          "step": 5
        },
        {
          "prop": "humanize",
          "kind": "toggle"
        }
      ],
      "preset": {
        "words": [
          "Type it out.",
          "Delete it.",
          "Try again."
        ],
        "reserve": true
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/typewriter",
      "markdown": "https://textlab.dev/components/typewriter.md",
      "registry": "https://textlab.dev/r/typewriter.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/typewriter.json"
    },
    {
      "name": "scramble-text",
      "title": "Scramble text",
      "description": "Letters decode into place.",
      "category": "typing",
      "exportName": "ScrambleText",
      "dependencies": [],
      "variants": [
        "random",
        "symbols",
        "binary"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"random\" | \"symbols\" | \"binary\"",
          "default": "\"random\"",
          "description": "Which glyphs flicker before each letter settles."
        },
        {
          "name": "charset",
          "type": "string",
          "description": "Custom glyphs; overrides the variant."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.2"
        },
        {
          "name": "tick",
          "type": "number",
          "default": "45",
          "description": "Milliseconds between glyph changes."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<ScrambleText text=\"Access granted\" variant=\"symbols\" />",
      "whenToUse": [
        "Tech, security and launch moments.",
        "Short labels that should feel computed."
      ],
      "whenNotToUse": [
        "Body copy.",
        "Anything that must be read during the first second."
      ],
      "accessibility": [
        "Screen readers only ever hear the real text.",
        "The real text reserves layout, so lines never jump."
      ],
      "motion": [
        "Settles left to right with a short lead, so the end of the word stays alive longest.",
        "Whitespace never scrambles, keeping word shapes."
      ],
      "reducedMotion": "Shows the real text, no glyphs.",
      "performance": [
        "One rAF loop while running; idle after it settles.",
        "Glyphs change every 45 ms, not every frame."
      ],
      "related": [
        "hover-scramble",
        "matrix-decode",
        "typewriter"
      ],
      "keywords": [
        "scramble",
        "decrypt",
        "decode",
        "hacker",
        "shuffle",
        "cipher"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "random",
            "symbols",
            "binary"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.3,
          "max": 3,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Access granted"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/scramble-text",
      "markdown": "https://textlab.dev/components/scramble-text.md",
      "registry": "https://textlab.dev/r/scramble-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/scramble-text.json"
    },
    {
      "name": "hover-scramble",
      "title": "Hover scramble",
      "description": "Scrambles on hover, settles on leave.",
      "category": "typing",
      "exportName": "HoverScramble",
      "dependencies": [],
      "variants": [
        "sweep",
        "all"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"sweep\" | \"all\"",
          "default": "\"sweep\"",
          "description": "sweep re-settles left to right; all snaps back together at the end."
        },
        {
          "name": "charset",
          "type": "string",
          "default": "symbols",
          "description": "Glyphs used while scrambled."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.5"
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<a href=\"/work\"><HoverScramble text=\"Selected work\" /></a>",
      "whenToUse": [
        "Navigation links, footers and small buttons on technical brands.",
        "Any short label where hover feedback should feel playful."
      ],
      "whenNotToUse": [
        "Primary calls to action; the label is briefly unreadable.",
        "Touch-first interfaces, where hover never happens."
      ],
      "accessibility": [
        "Keyboard focus triggers it too, through focus capture on the wrapper.",
        "Screen readers hear the label, never the glyphs."
      ],
      "motion": [
        "Half a second, under the 300 ms-per-step budget for UI feedback.",
        "Leaving mid-scramble settles straight back to the word."
      ],
      "reducedMotion": "Hover does nothing; the label stays still.",
      "performance": [
        "Runs only while hovered.",
        "No layout work; the real label holds the size."
      ],
      "related": [
        "scramble-text",
        "flip-roll",
        "magnetic-text"
      ],
      "keywords": [
        "hover",
        "scramble",
        "nav link",
        "button",
        "glitch hover"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "sweep",
            "all"
          ]
        }
      ],
      "preset": {
        "text": "Hover to scramble"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/hover-scramble",
      "markdown": "https://textlab.dev/components/hover-scramble.md",
      "registry": "https://textlab.dev/r/hover-scramble.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/hover-scramble.json"
    },
    {
      "name": "matrix-decode",
      "title": "Matrix decode",
      "description": "Glyph rain resolves into the word.",
      "category": "typing",
      "exportName": "MatrixDecode",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "rain",
        "flicker"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"rain\" | \"flicker\"",
          "default": "\"rain\"",
          "description": "rain drops a column of glyphs onto each letter; flicker cycles glyphs in place."
        },
        {
          "name": "glow",
          "type": "string",
          "default": "phosphor green",
          "description": "Color letters flash as they lock in."
        },
        {
          "name": "charset",
          "type": "string",
          "default": "half-width katakana and digits",
          "description": "Glyphs used in the rain."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.6"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<MatrixDecode as=\"h2\" text=\"Wake up\" />",
      "whenToUse": [
        "Developer tools, terminals and security products.",
        "Short titles on dark backgrounds."
      ],
      "whenNotToUse": [
        "Light, calm brands; the glow reads loud.",
        "Paragraphs."
      ],
      "accessibility": [
        "The rain is decorative and aria-hidden; the title is read once.",
        "Glyph positions are seeded, so server and client render the same frame."
      ],
      "motion": [
        "Each letter catches its column just as it lands, then cools from the glow color over 0.9 s.",
        "Order is loosely left to right, with seeded randomness."
      ],
      "reducedMotion": "The title appears in place with no rain or glow.",
      "performance": [
        "One short motion per letter; nothing runs after it resolves."
      ],
      "related": [
        "scramble-text",
        "neon-text",
        "typewriter"
      ],
      "keywords": [
        "matrix",
        "code rain",
        "decode",
        "hacker",
        "katakana",
        "terminal"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "rain",
            "flicker"
          ]
        }
      ],
      "preset": {
        "text": "Signal acquired"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/matrix-decode",
      "markdown": "https://textlab.dev/components/matrix-decode.md",
      "registry": "https://textlab.dev/r/matrix-decode.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/matrix-decode.json"
    },
    {
      "name": "text-morph",
      "title": "Text morph",
      "description": "One word turns into the next, letter by letter.",
      "category": "typing",
      "exportName": "TextMorph",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "slide",
        "blur",
        "flip"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"slide\" | \"blur\" | \"flip\"",
          "default": "\"slide\"",
          "description": "How new letters arrive and old ones leave. Shared letters always glide."
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "true",
          "description": "Announce changes politely to screen readers."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<button onClick={save}><TextMorph text={saved ? \"Saved\" : \"Save\"} /></button>",
      "whenToUse": [
        "Button labels that change state: Save to Saved, Follow to Following.",
        "Toggles and short status words."
      ],
      "whenNotToUse": [
        "Long sentences; most letters change, so the morph reads as noise.",
        "Labels that change many times a second."
      ],
      "accessibility": [
        "The new label is announced politely by default.",
        "Letters are aria-hidden; screen readers never hear them one by one."
      ],
      "motion": [
        "Shared letters glide on a smooth spring and can be interrupted mid-morph.",
        "Exits are quick (150 ms) so the new word never waits."
      ],
      "reducedMotion": "The label swaps instantly.",
      "performance": [
        "Layout animation per letter; keep labels short."
      ],
      "related": [
        "word-rotator",
        "typewriter",
        "flip-roll"
      ],
      "keywords": [
        "morph",
        "label",
        "flip",
        "layout animation",
        "button state",
        "text transition"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "slide",
            "blur",
            "flip"
          ]
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/text-morph",
      "markdown": "https://textlab.dev/components/text-morph.md",
      "registry": "https://textlab.dev/r/text-morph.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/text-morph.json"
    },
    {
      "name": "word-rotator",
      "title": "Word rotator",
      "description": "Cycles words while the width follows.",
      "category": "typing",
      "exportName": "WordRotator",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "slide",
        "blur",
        "flip"
      ],
      "props": [
        {
          "name": "words",
          "type": "string[]",
          "description": "Words to cycle.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"slide\" | \"blur\" | \"flip\"",
          "default": "\"slide\"",
          "description": "How each word arrives."
        },
        {
          "name": "interval",
          "type": "number",
          "default": "2.4",
          "description": "Seconds each word stays."
        },
        {
          "name": "pauseOnHover",
          "type": "boolean",
          "default": "true",
          "description": "Pause while hovered or focused."
        },
        {
          "name": "label",
          "type": "string",
          "description": "What screen readers hear. Defaults to every word."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<h1>Built for <WordRotator words={[\"designers\", \"engineers\", \"writers\"]} /></h1>",
      "whenToUse": [
        "Hero lines with a rotating noun.",
        "Short lists where each item fits the same sentence."
      ],
      "whenNotToUse": [
        "Words that differ wildly in length on mobile; the sentence reflows each time.",
        "Essential information hidden in later words."
      ],
      "accessibility": [
        "Screen readers hear all words once instead of a moving target.",
        "Hover and focus pause the rotation."
      ],
      "motion": [
        "The container's width animates on the same spring as the words, so neighbours glide.",
        "Old words leave as new ones arrive, overlapping instead of waiting."
      ],
      "reducedMotion": "Shows the first word, still.",
      "performance": [
        "One interval; layout animation on a single wrapper."
      ],
      "related": [
        "text-morph",
        "typewriter",
        "kinetic-beat"
      ],
      "keywords": [
        "rotating words",
        "text rotate",
        "cycle",
        "hero",
        "headline"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "slide",
            "blur",
            "flip"
          ]
        },
        {
          "prop": "interval",
          "kind": "range",
          "min": 1,
          "max": 5,
          "step": 0.2
        }
      ],
      "preset": {
        "words": [
          "designers",
          "engineers",
          "writers",
          "everyone"
        ]
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/word-rotator",
      "markdown": "https://textlab.dev/components/word-rotator.md",
      "registry": "https://textlab.dev/r/word-rotator.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/word-rotator.json"
    },
    {
      "name": "caret-morph",
      "title": "Caret morph",
      "description": "A caret that glides between words and reshapes to fit.",
      "category": "typing",
      "exportName": "CaretMorph",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "highlight",
        "bar",
        "block"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"highlight\" | \"bar\" | \"block\"",
          "default": "\"highlight\"",
          "description": "highlight selects the word, bar sits after it like a cursor, block inverts it."
        },
        {
          "name": "autoplay",
          "type": "boolean",
          "default": "true",
          "description": "Tour the words while nobody is pointing."
        },
        {
          "name": "interval",
          "type": "number",
          "default": "1.3",
          "description": "Seconds per word on the tour."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Highlight color."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"p\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<CaretMorph text=\"Point at any word in this sentence\" />",
      "whenToUse": [
        "Editor and writing-tool marketing where the caret is the product.",
        "Interactive pull quotes."
      ],
      "whenNotToUse": [
        "Real text inputs; use the browser caret.",
        "Long paragraphs; the tour takes too long to be noticed."
      ],
      "accessibility": [
        "The sentence is focusable and named; Left and Right move the caret.",
        "Pointer and focus pause the tour."
      ],
      "motion": [
        "The caret glides in 200 ms with a strong ease-out and retargets mid-move, so hopping across words stays quick.",
        "Position and width ride one transform, so the glide never touches layout."
      ],
      "reducedMotion": "The caret is hidden; the sentence is plain text.",
      "performance": [
        "One absolutely positioned element. Word boxes are measured on resize, not per frame."
      ],
      "related": [
        "typewriter",
        "read-along",
        "marker-highlight"
      ],
      "keywords": [
        "caret",
        "cursor",
        "selection",
        "highlight",
        "editor",
        "text cursor"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "highlight",
            "bar",
            "block"
          ]
        }
      ],
      "preset": {
        "text": "Point at any word in this sentence"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/caret-morph",
      "markdown": "https://textlab.dev/components/caret-morph.md",
      "registry": "https://textlab.dev/r/caret-morph.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/caret-morph.json"
    },
    {
      "name": "gradient-text",
      "title": "Gradient text",
      "description": "Color flows slowly through the letters.",
      "category": "light",
      "exportName": "GradientText",
      "dependencies": [],
      "variants": [
        "flow",
        "conic",
        "spectrum"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"flow\" | \"conic\" | \"spectrum\"",
          "default": "\"flow\"",
          "description": "Drifting linear gradient, a turning color wheel, or a hue cycle."
        },
        {
          "name": "colors",
          "type": "string[]",
          "default": "spectrum tokens",
          "description": "Gradient stops."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "8",
          "description": "Seconds per full cycle."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<GradientText as=\"h1\" text=\"Color in motion\" variant=\"conic\" />",
      "whenToUse": [
        "One accent word or short headline per screen.",
        "Brand moments that should feel alive but calm."
      ],
      "whenNotToUse": [
        "Body text or long headlines; contrast varies along the gradient.",
        "Several gradients competing on one page."
      ],
      "accessibility": [
        "The text is never split; it reads, selects and wraps normally.",
        "Pick stops that all pass contrast against the background."
      ],
      "motion": [
        "A full cycle takes 8 seconds by default, slow enough to read as ambient.",
        "Conic uses a registered angle property for a smooth rotation."
      ],
      "reducedMotion": "The gradient stays still.",
      "performance": [
        "Background position and a registered property; no JavaScript animation."
      ],
      "related": [
        "shimmer-text",
        "aurora-text",
        "holo-text"
      ],
      "keywords": [
        "gradient",
        "animated gradient",
        "rainbow",
        "conic",
        "background clip text"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "flow",
            "conic",
            "spectrum"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 2,
          "max": 20,
          "step": 1
        }
      ],
      "preset": {
        "text": "Color in motion"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/gradient-text",
      "markdown": "https://textlab.dev/components/gradient-text.md",
      "registry": "https://textlab.dev/r/gradient-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/gradient-text.json"
    },
    {
      "name": "shimmer-text",
      "title": "Shimmer text",
      "description": "A calm light passes across the words.",
      "category": "light",
      "exportName": "ShimmerText",
      "dependencies": [],
      "variants": [
        "subtle",
        "bright",
        "rainbow"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"subtle\" | \"bright\" | \"rainbow\"",
          "default": "\"subtle\"",
          "description": "A soft pass for status lines, a crisp sheen, or a hint of color."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "2.4",
          "description": "Seconds per pass, including the rest after it."
        },
        {
          "name": "spread",
          "type": "number",
          "default": "0.18",
          "description": "Width of the light band as a share of the text."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ShimmerText text=\"Thinking\" />",
      "whenToUse": [
        "Loading, thinking and generating states.",
        "Placeholders that should feel alive while content arrives."
      ],
      "whenNotToUse": [
        "Headings that are already loaded; the motion implies work in progress.",
        "Error or success messages."
      ],
      "accessibility": [
        "Plain, unsplit text. Pair it with aria-busy on the region that is loading."
      ],
      "motion": [
        "The light crosses in the first 70% of each cycle, then rests, so it never feels frantic."
      ],
      "reducedMotion": "The text shows at a steady, slightly muted color.",
      "performance": [
        "One CSS animation on background position."
      ],
      "related": [
        "gradient-text",
        "neon-text",
        "typewriter"
      ],
      "keywords": [
        "shimmer",
        "loading text",
        "skeleton",
        "thinking",
        "sheen",
        "ai loading"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "subtle",
            "bright",
            "rainbow"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 1,
          "max": 5,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Generating a reply"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/shimmer-text",
      "markdown": "https://textlab.dev/components/shimmer-text.md",
      "registry": "https://textlab.dev/r/shimmer-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/shimmer-text.json"
    },
    {
      "name": "neon-text",
      "title": "Neon sign",
      "description": "A neon sign that hums and flickers on.",
      "category": "light",
      "exportName": "NeonText",
      "dependencies": [],
      "variants": [
        "flicker",
        "steady",
        "pulse"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"flicker\" | \"steady\" | \"pulse\"",
          "default": "\"flicker\"",
          "description": "Powers on and stutters now and then, hums gently, or breathes its glow."
        },
        {
          "name": "color",
          "type": "string",
          "default": "hot pink",
          "description": "Tube color. The core is mixed toward white."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<NeonText text=\"Open late\" color=\"oklch(0.78 0.18 200)\" />",
      "whenToUse": [
        "Nightlife, music and retro brands on dark backgrounds.",
        "A single sign-like word."
      ],
      "whenNotToUse": [
        "Light backgrounds; the glow washes out.",
        "Running copy."
      ],
      "accessibility": [
        "Flicker stays under two flashes per second.",
        "The text is plain and unsplit."
      ],
      "motion": [
        "Power-on takes 1.4 s, then a rare stutter every 7 s.",
        "Pulse breathes the halo over 3 s, not the letters."
      ],
      "reducedMotion": "The sign glows steadily with no flicker.",
      "performance": [
        "Text-shadow only; animation is CSS.",
        "Pulse animates text-shadow, which repaints; use it once per screen."
      ],
      "related": [
        "ember-text",
        "aurora-text",
        "glitch-text"
      ],
      "keywords": [
        "neon",
        "glow",
        "flicker",
        "sign",
        "retro",
        "synthwave"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "flicker",
            "steady",
            "pulse"
          ]
        }
      ],
      "preset": {
        "text": "Open late"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/neon-text",
      "markdown": "https://textlab.dev/components/neon-text.md",
      "registry": "https://textlab.dev/r/neon-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/neon-text.json"
    },
    {
      "name": "aurora-text",
      "title": "Aurora text",
      "description": "Northern lights drift inside the letters.",
      "category": "light",
      "exportName": "AuroraText",
      "dependencies": [],
      "variants": [
        "polar",
        "ember",
        "mint"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"polar\" | \"ember\" | \"mint\"",
          "default": "\"polar\"",
          "description": "Palette."
        },
        {
          "name": "colors",
          "type": "string[]",
          "description": "Three or four custom colors; overrides the variant."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "14",
          "description": "Seconds for one drift cycle."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<AuroraText as=\"h1\" text=\"Northern lights\" />",
      "whenToUse": [
        "Large display headlines on dark or very light pages.",
        "Atmospheric brand moments."
      ],
      "whenNotToUse": [
        "Small text; the bands need room to read.",
        "Next to another gradient effect."
      ],
      "accessibility": [
        "Plain text clipped to color. Check contrast of the darkest band against the background."
      ],
      "motion": [
        "A 14 s ease-in-out drift; slow enough to feel like weather, not an animation."
      ],
      "reducedMotion": "A still aurora frame.",
      "performance": [
        "Four background layers moved by one CSS animation."
      ],
      "related": [
        "gradient-text",
        "holo-text",
        "neon-text"
      ],
      "keywords": [
        "aurora",
        "northern lights",
        "gradient",
        "mesh gradient",
        "ambient"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "polar",
            "ember",
            "mint"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 4,
          "max": 30,
          "step": 1
        }
      ],
      "preset": {
        "text": "Northern lights"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/aurora-text",
      "markdown": "https://textlab.dev/components/aurora-text.md",
      "registry": "https://textlab.dev/r/aurora-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/aurora-text.json"
    },
    {
      "name": "holo-text",
      "title": "Holographic text",
      "description": "Foil that shifts color as you move across it.",
      "category": "light",
      "exportName": "HoloText",
      "dependencies": [],
      "variants": [
        "holo",
        "foil",
        "chrome"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"holo\" | \"foil\" | \"chrome\"",
          "default": "\"holo\"",
          "description": "Rainbow hologram, warm iridescent leaf, or a cool metallic sweep."
        },
        {
          "name": "intensity",
          "type": "number",
          "default": "0.7",
          "description": "How far colors travel with the pointer, 0 to 1."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<HoloText as=\"h1\" text=\"Limited edition\" variant=\"foil\" />",
      "whenToUse": [
        "Collectibles, drops and premium tiers.",
        "Large display words people will hover or drag across."
      ],
      "whenNotToUse": [
        "Small text; the pattern breaks the letterforms.",
        "Text that needs high, steady contrast."
      ],
      "accessibility": [
        "Plain, unsplit text.",
        "Without a pointer it rests on a pleasant still frame."
      ],
      "motion": [
        "The pattern follows the pointer with damping, like light on a tilted card.",
        "The sheen brightens while the pointer is over the text."
      ],
      "reducedMotion": "A still foil frame with no pointer tracking.",
      "performance": [
        "Pointer position is written to CSS variables once per frame, only while it is settling."
      ],
      "related": [
        "aurora-text",
        "spotlight-text",
        "glass-text"
      ],
      "keywords": [
        "holographic",
        "foil",
        "iridescent",
        "chrome",
        "metallic",
        "premium"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "holo",
            "foil",
            "chrome"
          ]
        },
        {
          "prop": "intensity",
          "kind": "range",
          "min": 0,
          "max": 1,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Limited edition"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/holo-text",
      "markdown": "https://textlab.dev/components/holo-text.md",
      "registry": "https://textlab.dev/r/holo-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/holo-text.json"
    },
    {
      "name": "spotlight-text",
      "title": "Spotlight text",
      "description": "A light follows the cursor and reveals the copy.",
      "category": "light",
      "exportName": "SpotlightText",
      "dependencies": [],
      "variants": [
        "reveal",
        "glow",
        "outline"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"reveal\" | \"glow\" | \"outline\"",
          "default": "\"reveal\"",
          "description": "Lift dim text, add a warm light, or fill hollow letters under the light."
        },
        {
          "name": "radius",
          "type": "number",
          "default": "2.2",
          "description": "Radius of the light in em."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-fx-glow)",
          "description": "Light color for the glow variant."
        },
        {
          "name": "dim",
          "type": "number",
          "default": "0.22",
          "description": "Visibility outside the light, 0 to 1."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<SpotlightText as=\"h2\" text=\"Look around\" variant=\"glow\" />",
      "whenToUse": [
        "Hero statements meant to be explored.",
        "Dark, cinematic sections."
      ],
      "whenNotToUse": [
        "Instructions or anything people must read in full immediately.",
        "Light backgrounds with the glow variant."
      ],
      "accessibility": [
        "Screen readers get the text from a hidden copy.",
        "Outside the light the text stays legible at reduced contrast; raise dim if it must pass AA."
      ],
      "motion": [
        "The light trails the pointer slightly, like a heavy torch.",
        "When the pointer leaves, the light fades rather than snapping off."
      ],
      "reducedMotion": "Shows the text normally with no light.",
      "performance": [
        "A radial mask driven by CSS variables written once per frame while moving."
      ],
      "related": [
        "holo-text",
        "glass-text",
        "variable-proximity"
      ],
      "keywords": [
        "spotlight",
        "flashlight",
        "torch",
        "cursor reveal",
        "mask",
        "hover light"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "reveal",
            "glow",
            "outline"
          ]
        },
        {
          "prop": "radius",
          "kind": "range",
          "min": 0.8,
          "max": 6,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Look around"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/spotlight-text",
      "markdown": "https://textlab.dev/components/spotlight-text.md",
      "registry": "https://textlab.dev/r/spotlight-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/spotlight-text.json"
    },
    {
      "name": "glass-text",
      "title": "Glass text",
      "description": "Glass letters that bend the scene behind them.",
      "category": "light",
      "exportName": "GlassText",
      "dependencies": [],
      "variants": [
        "clear",
        "frost",
        "tinted"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"clear\" | \"frost\" | \"tinted\"",
          "default": "\"clear\"",
          "description": "Strong refraction, milky soft glass, or glass tinted with the accent."
        },
        {
          "name": "backdrop",
          "type": "string",
          "default": "a soft color field",
          "description": "The scene behind the glass, as a CSS background image. The letters refract it."
        },
        {
          "name": "tint",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Tint for the tinted variant."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<GlassText text=\"Clarity\" backdrop=\"url(/photo.jpg)\" />",
      "whenToUse": [
        "Display words over photography or rich color fields.",
        "Premium, Apple-like hero moments."
      ],
      "whenNotToUse": [
        "Plain backgrounds; there is nothing for the glass to bend.",
        "Small sizes; the rim and highlight need room."
      ],
      "accessibility": [
        "The text is read from a hidden copy; the glass layers are decorative.",
        "Use a backdrop that keeps the letters distinguishable, or pick frost."
      ],
      "motion": [
        "The refraction and highlight follow the pointer with damping.",
        "Nothing moves without a pointer, so it costs nothing at rest."
      ],
      "reducedMotion": "Glass stays still; no pointer tracking.",
      "performance": [
        "Three text layers with background clip; CSS variables update only while the pointer settles."
      ],
      "related": [
        "holo-text",
        "spotlight-text",
        "tilt-text"
      ],
      "keywords": [
        "glass",
        "glassmorphism",
        "frosted",
        "refraction",
        "liquid glass",
        "translucent"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "clear",
            "frost",
            "tinted"
          ]
        }
      ],
      "preset": {
        "text": "Clarity"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/glass-text",
      "markdown": "https://textlab.dev/components/glass-text.md",
      "registry": "https://textlab.dev/r/glass-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/glass-text.json"
    },
    {
      "name": "ember-text",
      "title": "Ember text",
      "description": "Letters land hot and cool down to the text color.",
      "category": "light",
      "exportName": "EmberText",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "ember",
        "steel",
        "ice"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"ember\" | \"steel\" | \"ice\"",
          "default": "\"ember\"",
          "description": "White-hot to red, blue-white steel, or cyan frost."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.045"
        },
        {
          "name": "duration",
          "type": "number",
          "default": "1.6",
          "description": "Seconds a letter takes to cool."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<EmberText as=\"h2\" text=\"Forged in fire\" />",
      "whenToUse": [
        "Titles for craft, hardware and energy brands.",
        "Dark sections where warmth should arrive with the words."
      ],
      "whenNotToUse": [
        "Light backgrounds with the ice variant; the frost disappears.",
        "Paragraphs."
      ],
      "accessibility": [
        "The glow is a decorative copy; the title is read once.",
        "The settled color is the inherited text color, so contrast is unchanged."
      ],
      "motion": [
        "Each letter settles a short drop with a long-tailed ease-out.",
        "Heat fades through four stops over 1.6 s, brightest right as it lands."
      ],
      "reducedMotion": "The title appears in its normal color.",
      "performance": [
        "Two motion spans per letter; nothing runs after the last one cools."
      ],
      "related": [
        "neon-text",
        "matrix-decode",
        "char-reveal"
      ],
      "keywords": [
        "ember",
        "fire",
        "hot metal",
        "forge",
        "glow",
        "cooling"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "ember",
            "steel",
            "ice"
          ]
        }
      ],
      "preset": {
        "text": "Forged in fire"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/ember-text",
      "markdown": "https://textlab.dev/components/ember-text.md",
      "registry": "https://textlab.dev/r/ember-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/ember-text.json"
    },
    {
      "name": "gooey-text",
      "title": "Gooey text",
      "description": "Words melt into each other.",
      "category": "distort",
      "exportName": "GooeyText",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "blob",
        "drip"
      ],
      "props": [
        {
          "name": "words",
          "type": "string[]",
          "description": "Words to melt between.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"blob\" | \"drip\"",
          "default": "\"blob\"",
          "description": "Melt in place, or let the old word drip away."
        },
        {
          "name": "interval",
          "type": "number",
          "default": "2.2",
          "description": "Seconds each word holds."
        },
        {
          "name": "duration",
          "type": "number",
          "default": "1.2",
          "description": "Seconds the melt takes."
        },
        {
          "name": "goo",
          "type": "number",
          "default": "12",
          "description": "Stickiness, 4 to 20. Higher merges letters more."
        },
        {
          "name": "label",
          "type": "string",
          "description": "What screen readers hear. Defaults to every word."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<GooeyText words={[\"Melt\", \"Merge\", \"Morph\"]} />",
      "whenToUse": [
        "Playful brands and creative portfolios.",
        "Bold display words with simple shapes."
      ],
      "whenNotToUse": [
        "Thin or small type; the threshold eats fine strokes.",
        "Long words that change length a lot."
      ],
      "accessibility": [
        "Screen readers hear all words once.",
        "The longest word reserves the space, so layout never shifts."
      ],
      "motion": [
        "Enter uses the enter ease; the leaving word uses the exit curve so they cross in the middle.",
        "The filter's alpha threshold turns the blur into liquid edges."
      ],
      "reducedMotion": "Shows the first word, still.",
      "performance": [
        "One SVG filter on the word. Blur and threshold are GPU-friendly in most browsers; avoid dozens on one page."
      ],
      "related": [
        "liquid-text",
        "text-morph",
        "ink-bleed"
      ],
      "keywords": [
        "gooey",
        "goo",
        "liquid",
        "metaball",
        "blob",
        "melt"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "blob",
            "drip"
          ]
        },
        {
          "prop": "goo",
          "kind": "range",
          "min": 4,
          "max": 24,
          "step": 1
        }
      ],
      "preset": {
        "words": [
          "Melt",
          "Merge",
          "Morph",
          "Mend"
        ]
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/gooey-text",
      "markdown": "https://textlab.dev/components/gooey-text.md",
      "registry": "https://textlab.dev/r/gooey-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/gooey-text.json"
    },
    {
      "name": "liquid-text",
      "title": "Liquid text",
      "description": "Ripples run through the letters under the cursor.",
      "category": "distort",
      "exportName": "LiquidText",
      "dependencies": [],
      "variants": [
        "ripple",
        "wave",
        "still"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"ripple\" | \"wave\" | \"still\"",
          "default": "\"ripple\"",
          "description": "Stir under the pointer, keep a slow current running, or stay calm until touched."
        },
        {
          "name": "strength",
          "type": "number",
          "default": "14",
          "description": "Displacement in px at full stir."
        },
        {
          "name": "radius",
          "type": "number",
          "default": "1.6",
          "description": "Radius of the stirred area in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<LiquidText as=\"h2\" text=\"Still water\" variant=\"still\" />",
      "whenToUse": [
        "Water, wellness and audio brands.",
        "Large display words people will hover."
      ],
      "whenNotToUse": [
        "Text that must be read while the pointer is over it.",
        "Small sizes; the ripple scale is in pixels."
      ],
      "accessibility": [
        "Only a decorative copy is distorted; screen readers read the text once.",
        "Outside the pointer's circle the text is untouched."
      ],
      "motion": [
        "The current drifts slowly on two sine waves so it never loops visibly.",
        "Stir fades in and out with the pointer instead of switching."
      ],
      "reducedMotion": "Plain, still text.",
      "performance": [
        "One SVG turbulence filter. The loop pauses offscreen and when the tab is hidden."
      ],
      "related": [
        "heat-haze",
        "gooey-text",
        "ink-bleed"
      ],
      "keywords": [
        "liquid",
        "water",
        "ripple",
        "displacement",
        "turbulence",
        "svg filter"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "ripple",
            "wave",
            "still"
          ]
        },
        {
          "prop": "strength",
          "kind": "range",
          "min": 2,
          "max": 40,
          "step": 1
        }
      ],
      "preset": {
        "text": "Still water"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/liquid-text",
      "markdown": "https://textlab.dev/components/liquid-text.md",
      "registry": "https://textlab.dev/r/liquid-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/liquid-text.json"
    },
    {
      "name": "ink-bleed",
      "title": "Ink bleed",
      "description": "Text soaks in like ink on paper.",
      "category": "distort",
      "exportName": "InkBleed",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "ink",
        "watercolor",
        "stamp"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"ink\" | \"watercolor\" | \"stamp\"",
          "default": "\"ink\"",
          "description": "Rough fibrous ink, soft watercolor bloom, or a grainy stamp."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.8"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<InkBleed as=\"h2\" text=\"Written by hand\" variant=\"watercolor\" />",
      "whenToUse": [
        "Editorial, stationery and craft brands.",
        "Serif display type on paper-like backgrounds."
      ],
      "whenNotToUse": [
        "Thin weights; the threshold can break hairlines.",
        "Busy photographic backgrounds."
      ],
      "accessibility": [
        "The filtered layer is decorative; the text is read from a hidden copy."
      ],
      "motion": [
        "Spreads with the emphasized ease: quick soak, slow settle.",
        "Edges stay slightly fibrous at rest, like real ink."
      ],
      "reducedMotion": "Plain text with no filter.",
      "performance": [
        "One SVG filter updated for the length of the animation only."
      ],
      "related": [
        "outline-draw",
        "signature",
        "gooey-text"
      ],
      "keywords": [
        "ink",
        "bleed",
        "watercolor",
        "stamp",
        "paper",
        "organic"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "ink",
            "watercolor",
            "stamp"
          ]
        }
      ],
      "preset": {
        "text": "Written by hand"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/ink-bleed",
      "markdown": "https://textlab.dev/components/ink-bleed.md",
      "registry": "https://textlab.dev/r/ink-bleed.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/ink-bleed.json"
    },
    {
      "name": "chromatic-text",
      "title": "Chromatic text",
      "description": "The color channels split and rejoin.",
      "category": "distort",
      "exportName": "ChromaticText",
      "dependencies": [],
      "variants": [
        "hover",
        "velocity",
        "pulse"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"hover\" | \"velocity\" | \"pulse\"",
          "default": "\"hover\"",
          "description": "Split while hovered, with pointer speed, or in a brief pulse every few seconds."
        },
        {
          "name": "amount",
          "type": "number",
          "default": "0.06",
          "description": "Largest channel offset in em."
        },
        {
          "name": "colors",
          "type": "[string, string]",
          "default": "red and cyan",
          "description": "Fringe colors."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ChromaticText as=\"h2\" text=\"Out of focus\" variant=\"velocity\" />",
      "whenToUse": [
        "Music, fashion and camera brands.",
        "Display headlines that should react to energy."
      ],
      "whenNotToUse": [
        "Body text; fringes reduce legibility.",
        "Interfaces where it would read as a rendering bug."
      ],
      "accessibility": [
        "The real text stays on top and unchanged; fringes are decorative copies.",
        "Pulse is brief and infrequent, never a flash."
      ],
      "motion": [
        "Channels slide on damped springs, so the split eases in and out.",
        "Velocity follows the direction you moved the pointer."
      ],
      "reducedMotion": "No fringes; plain text.",
      "performance": [
        "Two transformed copies; offsets are CSS variables written per frame only while visible."
      ],
      "related": [
        "glitch-text",
        "scroll-skew",
        "slice-reveal"
      ],
      "keywords": [
        "chromatic aberration",
        "rgb split",
        "fringe",
        "lens",
        "glitch"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "hover",
            "velocity",
            "pulse"
          ]
        },
        {
          "prop": "amount",
          "kind": "range",
          "min": 0.01,
          "max": 0.2,
          "step": 0.01
        }
      ],
      "preset": {
        "text": "Out of focus"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/chromatic-text",
      "markdown": "https://textlab.dev/components/chromatic-text.md",
      "registry": "https://textlab.dev/r/chromatic-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/chromatic-text.json"
    },
    {
      "name": "glitch-text",
      "title": "Glitch text",
      "description": "A controlled digital glitch on cue.",
      "category": "distort",
      "exportName": "GlitchText",
      "dependencies": [],
      "variants": [
        "slice",
        "shift",
        "scan"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"slice\" | \"shift\" | \"scan\"",
          "default": "\"slice\"",
          "description": "Tear bands sideways, jolt the word, or roll a bright scan band through it."
        },
        {
          "name": "trigger",
          "type": "\"interval\" | \"hover\" | \"mount\"",
          "default": "\"interval\"",
          "description": "When it glitches."
        },
        {
          "name": "every",
          "type": "number",
          "default": "3.5",
          "description": "Seconds between bursts for interval."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<GlitchText text=\"Signal lost\" trigger=\"hover\" />",
      "whenToUse": [
        "Gaming, music and cyber themes.",
        "Error and 404 pages with personality."
      ],
      "whenNotToUse": [
        "Serious or high-trust contexts like payments.",
        "More than one glitch on screen."
      ],
      "accessibility": [
        "Bursts are 500 ms with at least 3 s between them by default, well under flash thresholds.",
        "The readable text only jolts by a few hundredths of an em, and only during a burst."
      ],
      "motion": [
        "Hard steps, not tweens, so it looks digital.",
        "Long rests between bursts keep it intentional."
      ],
      "reducedMotion": "No glitching; plain text.",
      "performance": [
        "CSS keyframes on clip-path and transform; idle between bursts."
      ],
      "related": [
        "chromatic-text",
        "slice-reveal",
        "scramble-text"
      ],
      "keywords": [
        "glitch",
        "rgb split",
        "distortion",
        "cyberpunk",
        "error",
        "vhs"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "slice",
            "shift",
            "scan"
          ]
        }
      ],
      "preset": {
        "text": "Signal lost"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/glitch-text",
      "markdown": "https://textlab.dev/components/glitch-text.md",
      "registry": "https://textlab.dev/r/glitch-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/glitch-text.json"
    },
    {
      "name": "heat-haze",
      "title": "Heat haze",
      "description": "A mirage warps the air around the type.",
      "category": "distort",
      "exportName": "HeatHaze",
      "dependencies": [],
      "variants": [
        "haze",
        "boil"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"haze\" | \"boil\"",
          "default": "\"haze\"",
          "description": "A gentle shimmer, or fast rising air."
        },
        {
          "name": "strength",
          "type": "number",
          "default": "6",
          "description": "Distortion in px."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<HeatHaze as=\"h2\" text=\"Midday on the salt flats\" />",
      "whenToUse": [
        "Travel, summer and desert themes.",
        "A single atmospheric headline."
      ],
      "whenNotToUse": [
        "Text people must read closely.",
        "Multiple instances on one page; each runs a live filter."
      ],
      "accessibility": [
        "The distorted layer is decorative; the text is read from a hidden copy.",
        "Distortion is a few pixels, so the word stays legible."
      ],
      "motion": [
        "Noise rises continuously while its seed steps, so the shimmer never repeats exactly."
      ],
      "reducedMotion": "Plain, still text.",
      "performance": [
        "SVG SMIL animation on one filter; no JavaScript loop.",
        "Filters repaint every frame; use one per screen."
      ],
      "related": [
        "liquid-text",
        "aurora-text",
        "ink-bleed"
      ],
      "keywords": [
        "heat",
        "haze",
        "mirage",
        "shimmer",
        "turbulence",
        "desert"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "haze",
            "boil"
          ]
        },
        {
          "prop": "strength",
          "kind": "range",
          "min": 1,
          "max": 20,
          "step": 1
        }
      ],
      "preset": {
        "text": "Midday on the salt flats"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/heat-haze",
      "markdown": "https://textlab.dev/components/heat-haze.md",
      "registry": "https://textlab.dev/r/heat-haze.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/heat-haze.json"
    },
    {
      "name": "scroll-fill",
      "title": "Scroll fill",
      "description": "Words light up as you read down the page.",
      "category": "scroll",
      "exportName": "ScrollFill",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "word",
        "char",
        "line"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"word\" | \"char\" | \"line\"",
          "default": "\"word\"",
          "description": "What lights up as one unit."
        },
        {
          "name": "dim",
          "type": "number",
          "default": "0.18",
          "description": "Opacity of text not yet reached."
        },
        {
          "name": "container",
          "type": "RefObject<HTMLElement>",
          "description": "Scrollable ancestor to track. Defaults to the window."
        },
        {
          "name": "offset",
          "type": "[string, string]",
          "default": "[\"start 0.85\", \"end 0.45\"]",
          "description": "Where filling starts and ends, in motion scroll-offset syntax."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"p\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ScrollFill text=\"We believe the best interfaces disappear. You notice what you came for, not the tool in between.\" />",
      "whenToUse": [
        "Manifesto paragraphs and mission statements.",
        "A key paragraph you want people to actually read."
      ],
      "whenNotToUse": [
        "Every paragraph on a page; the effect loses meaning.",
        "Text above the fold that should be readable at once."
      ],
      "accessibility": [
        "The full paragraph is read once from a hidden copy.",
        "Dim text keeps its shape, so nothing is hidden from sighted readers who scroll fast."
      ],
      "motion": [
        "Progress is tied to scroll position, not time, so it reverses when you scroll up.",
        "Filling starts as the paragraph enters the lower part of the viewport."
      ],
      "reducedMotion": "The paragraph shows at full opacity.",
      "performance": [
        "Opacity only. One derived motion value per unit, no React renders while scrolling."
      ],
      "responsive": [
        "Line mode re-measures on resize."
      ],
      "related": [
        "read-along",
        "in-view-title",
        "scroll-skew"
      ],
      "keywords": [
        "scroll",
        "text fill",
        "scroll reveal",
        "highlight on scroll",
        "apple scroll text",
        "reading progress"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "word",
            "char",
            "line"
          ]
        },
        {
          "prop": "dim",
          "kind": "range",
          "min": 0,
          "max": 0.6,
          "step": 0.02
        }
      ],
      "preset": {
        "text": "We believe the best interfaces disappear. You notice what you came for, not the tool in between, and every word earns its place."
      },
      "scroll": true,
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/scroll-fill",
      "markdown": "https://textlab.dev/components/scroll-fill.md",
      "registry": "https://textlab.dev/r/scroll-fill.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/scroll-fill.json"
    },
    {
      "name": "scroll-skew",
      "title": "Scroll skew",
      "description": "Type leans with the speed of your scroll.",
      "category": "scroll",
      "exportName": "ScrollSkew",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "skew",
        "stretch"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"skew\" | \"stretch\"",
          "default": "\"skew\"",
          "description": "Lean with scroll speed, or squash and stretch."
        },
        {
          "name": "max",
          "type": "number",
          "default": "12 (skew), 0.35 (stretch)",
          "description": "Largest lean in degrees, or stretch above 1."
        },
        {
          "name": "container",
          "type": "RefObject<HTMLElement>",
          "description": "Scrollable ancestor. Defaults to the window."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ScrollSkew as=\"h2\" text=\"Keep scrolling\" />",
      "whenToUse": [
        "Big section titles on long, energetic pages.",
        "Portfolios with a lot of momentum."
      ],
      "whenNotToUse": [
        "Reading-heavy pages; motion during scroll can distract.",
        "Small text."
      ],
      "accessibility": [
        "The text is never split; it reads normally.",
        "Motion only happens while the user is scrolling."
      ],
      "motion": [
        "Velocity passes through a spring, so the lean rises and settles smoothly.",
        "Stretch preserves area, narrowing as it grows taller."
      ],
      "reducedMotion": "Upright, still text.",
      "performance": [
        "Transform only, driven by motion values; no React renders on scroll."
      ],
      "related": [
        "scroll-stretch",
        "velocity-marquee",
        "scroll-fill"
      ],
      "keywords": [
        "scroll velocity",
        "skew on scroll",
        "momentum",
        "stretch",
        "scroll effect"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "skew",
            "stretch"
          ]
        }
      ],
      "preset": {
        "text": "Keep scrolling"
      },
      "scroll": true,
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/scroll-skew",
      "markdown": "https://textlab.dev/components/scroll-skew.md",
      "registry": "https://textlab.dev/r/scroll-skew.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/scroll-skew.json"
    },
    {
      "name": "velocity-marquee",
      "title": "Velocity marquee",
      "description": "A marquee that speeds up, slows and reverses with scroll.",
      "category": "scroll",
      "exportName": "VelocityMarquee",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "single",
        "double",
        "outline"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"single\" | \"double\" | \"outline\"",
          "default": "\"single\"",
          "description": "One row, two rows in opposite directions, or alternating solid and hollow copies."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "6",
          "description": "Resting speed in percent of one copy per second."
        },
        {
          "name": "boost",
          "type": "number",
          "default": "4",
          "description": "How much scroll speed adds."
        },
        {
          "name": "separator",
          "type": "string",
          "default": "\"✦\"",
          "description": "Mark between copies."
        },
        {
          "name": "pauseOnHover",
          "type": "boolean",
          "default": "true",
          "description": "Pause while hovered or focused so it can be read."
        },
        {
          "name": "container",
          "type": "RefObject<HTMLElement>",
          "description": "Scrollable ancestor. Defaults to the window."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<VelocityMarquee text=\"Available for new projects\" />",
      "whenToUse": [
        "Bold bands between sections on marketing pages.",
        "Announcements and logos-as-text strips."
      ],
      "whenNotToUse": [
        "Essential information; moving text is hard to read.",
        "More than two marquees on one page."
      ],
      "accessibility": [
        "Screen readers read the text once.",
        "Hover and focus pause it, meeting WCAG 2.2.2 for moving content."
      ],
      "motion": [
        "Direction follows the last scroll direction and stays there.",
        "Speed eases back to rest on a spring when scrolling stops."
      ],
      "reducedMotion": "A single, still line.",
      "performance": [
        "One transform per row, updated in a frame loop that pauses offscreen."
      ],
      "related": [
        "scroll-skew",
        "kinetic-beat",
        "scroll-stretch"
      ],
      "keywords": [
        "marquee",
        "ticker",
        "scroll velocity",
        "infinite scroll text",
        "banner"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "single",
            "double",
            "outline"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 1,
          "max": 20,
          "step": 1
        }
      ],
      "preset": {
        "text": "Available for new projects"
      },
      "scroll": true,
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/velocity-marquee",
      "markdown": "https://textlab.dev/components/velocity-marquee.md",
      "registry": "https://textlab.dev/r/velocity-marquee.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/velocity-marquee.json"
    },
    {
      "name": "scroll-stretch",
      "title": "Scroll stretch",
      "description": "Letters stretch tall as you scroll fast.",
      "category": "scroll",
      "exportName": "ScrollStretch",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "vertical",
        "horizontal"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"vertical\" | \"horizontal\"",
          "default": "\"vertical\"",
          "description": "Pull letters tall, or smear them sideways."
        },
        {
          "name": "max",
          "type": "number",
          "default": "0.6",
          "description": "Largest stretch above 1."
        },
        {
          "name": "container",
          "type": "RefObject<HTMLElement>",
          "description": "Scrollable ancestor. Defaults to the window."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ScrollStretch as=\"h2\" text=\"Elastic\" />",
      "whenToUse": [
        "Short, heavy display words on playful pages.",
        "Section dividers that react to momentum."
      ],
      "whenNotToUse": [
        "Long titles; per-letter springs add up.",
        "Serious, text-first pages."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Moves only during fast scrolling."
      ],
      "motion": [
        "Each letter has its own spring, slightly softer along the word, so the stretch ripples.",
        "Volume is preserved: taller letters get narrower."
      ],
      "reducedMotion": "Still, normal letters.",
      "performance": [
        "One spring per letter. Keep it to short words."
      ],
      "related": [
        "scroll-skew",
        "elastic-text",
        "velocity-marquee"
      ],
      "keywords": [
        "stretch",
        "scroll velocity",
        "taffy",
        "elastic",
        "squash"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "vertical",
            "horizontal"
          ]
        },
        {
          "prop": "max",
          "kind": "range",
          "min": 0.1,
          "max": 1.5,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Elastic"
      },
      "scroll": true,
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/scroll-stretch",
      "markdown": "https://textlab.dev/components/scroll-stretch.md",
      "registry": "https://textlab.dev/r/scroll-stretch.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/scroll-stretch.json"
    },
    {
      "name": "depth-layers",
      "title": "Depth layers",
      "description": "Outline copies drift apart in depth as you scroll.",
      "category": "scroll",
      "exportName": "DepthLayers",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "stack",
        "fan"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"stack\" | \"fan\"",
          "default": "\"stack\"",
          "description": "Separate vertically like layered prints, or fan open around a corner."
        },
        {
          "name": "layers",
          "type": "number",
          "default": "5",
          "description": "Outline copies behind the solid one."
        },
        {
          "name": "spread",
          "type": "number",
          "default": "0.12em (stack), 4° (fan)",
          "description": "Largest gap between neighbouring copies."
        },
        {
          "name": "container",
          "type": "RefObject<HTMLElement>",
          "description": "Scrollable ancestor. Defaults to the window."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<DepthLayers as=\"h2\" text=\"Parallax\" layers={6} />",
      "whenToUse": [
        "Big, bold section titles on scroll-led pages.",
        "Posters and editorial covers."
      ],
      "whenNotToUse": [
        "Thin or small type; outlines disappear.",
        "Tight layouts; layers extend below the text."
      ],
      "accessibility": [
        "Copies are aria-hidden; the title is read once.",
        "The solid copy never moves, so the title stays readable."
      ],
      "motion": [
        "Widest at the middle of the viewport, closed at the edges.",
        "A spring smooths scroll jitter."
      ],
      "reducedMotion": "Only the solid title, no layers.",
      "performance": [
        "N transformed copies; transform and opacity only."
      ],
      "related": [
        "extrude-text",
        "scroll-skew",
        "tilt-text"
      ],
      "keywords": [
        "parallax",
        "depth",
        "layers",
        "outline",
        "stack",
        "scroll"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "stack",
            "fan"
          ]
        },
        {
          "prop": "layers",
          "kind": "range",
          "min": 2,
          "max": 10,
          "step": 1
        }
      ],
      "preset": {
        "text": "Parallax"
      },
      "scroll": true,
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/depth-layers",
      "markdown": "https://textlab.dev/components/depth-layers.md",
      "registry": "https://textlab.dev/r/depth-layers.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/depth-layers.json"
    },
    {
      "name": "outline-draw",
      "title": "Outline draw",
      "description": "The outline draws itself, then fills.",
      "category": "draw",
      "exportName": "OutlineDraw",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "draw-fill",
        "draw-only",
        "fill-sweep"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"draw-fill\" | \"draw-only\" | \"fill-sweep\"",
          "default": "\"draw-fill\"",
          "description": "Trace then flood, keep the outline, or trace then fill left to right."
        },
        {
          "name": "stroke",
          "type": "number",
          "default": "0.018",
          "description": "Outline width in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.6"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"standard\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<OutlineDraw as=\"h1\" text=\"Draft\" />",
      "whenToUse": [
        "Big, single-line display words: names, product marks, numbers.",
        "Architectural and blueprint aesthetics."
      ],
      "whenNotToUse": [
        "Text that wraps; the SVG draws a single line.",
        "Small sizes where the outline is thinner than a pixel."
      ],
      "accessibility": [
        "The SVG is decorative; the word is read from a hidden copy.",
        "An invisible copy holds the exact size so nothing shifts."
      ],
      "motion": [
        "All contours trace at once and finish together.",
        "The fill starts at 75% of the trace so the two overlap."
      ],
      "reducedMotion": "Shows the final state: filled, or outlined for draw-only.",
      "performance": [
        "One SVG text element; stroke-dashoffset and fill-opacity only."
      ],
      "responsive": [
        "Keep it on one line; size it with a fluid font size."
      ],
      "related": [
        "signature",
        "wipe-reveal",
        "ink-bleed"
      ],
      "keywords": [
        "outline",
        "stroke",
        "draw",
        "svg text",
        "line drawing",
        "dashoffset"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "draw-fill",
            "draw-only",
            "fill-sweep"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.6,
          "max": 4,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Draft"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/outline-draw",
      "markdown": "https://textlab.dev/components/outline-draw.md",
      "registry": "https://textlab.dev/r/outline-draw.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/outline-draw.json"
    },
    {
      "name": "signature",
      "title": "Signature",
      "description": "A handwritten signature writes itself.",
      "category": "draw",
      "exportName": "Signature",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "pen",
        "brush"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The name to sign.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"pen\" | \"brush\"",
          "default": "\"pen\"",
          "description": "A fine nib, or a fuller brush where ink pools sooner."
        },
        {
          "name": "font",
          "type": "string",
          "default": "var(--tl-font-script)",
          "description": "Script font family. Load one with next/font, for example Mrs Saint Delafield."
        },
        {
          "name": "color",
          "type": "string",
          "default": "currentColor",
          "description": "Ink color."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "2.2"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        },
        {
          "name": "onComplete",
          "type": "() => void",
          "description": "Called when a one-shot animation finishes."
        }
      ],
      "usage": "<Signature text=\"Ada Lovelace\" />",
      "whenToUse": [
        "Founder letters, about pages and certificates.",
        "Sign-offs at the end of a story."
      ],
      "whenNotToUse": [
        "Without a script font loaded; system cursive fonts vary a lot.",
        "Anything longer than a name."
      ],
      "accessibility": [
        "The name is read once from a hidden copy.",
        "Script fonts are hard to read; keep the plain name nearby if it matters."
      ],
      "motion": [
        "Writes left to right on a gentle in-out curve, like a hand gaining then losing speed.",
        "Ink fills in behind the nib, never ahead of it."
      ],
      "reducedMotion": "Shows the signature in full.",
      "performance": [
        "One SVG text and one animated mask rectangle."
      ],
      "related": [
        "outline-draw",
        "ink-bleed",
        "typewriter"
      ],
      "keywords": [
        "signature",
        "handwriting",
        "script",
        "autograph",
        "write on",
        "pen"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "pen",
            "brush"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.8,
          "max": 5,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Ada Lovelace"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/signature",
      "markdown": "https://textlab.dev/components/signature.md",
      "registry": "https://textlab.dev/r/signature.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/signature.json"
    },
    {
      "name": "marker-highlight",
      "title": "Marker highlight",
      "description": "A highlighter swipes behind the words.",
      "category": "draw",
      "exportName": "MarkerHighlight",
      "dependencies": [],
      "variants": [
        "marker",
        "underline",
        "box"
      ],
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "description": "The words to highlight.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"marker\" | \"underline\" | \"box\"",
          "default": "\"marker\"",
          "description": "Full highlighter stroke, thick low band, or soft block."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-fx-highlight)",
          "description": "Highlight color."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"mark\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.7"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"standard\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<p>Textlab is <MarkerHighlight>free and open source</MarkerHighlight>.</p>",
      "whenToUse": [
        "Key phrases inside paragraphs and testimonials.",
        "Phrases that wrap across lines."
      ],
      "whenNotToUse": [
        "Whole paragraphs.",
        "As the only way to convey meaning; color alone is not enough."
      ],
      "accessibility": [
        "Renders <mark>, which assistive tech can announce as highlighted.",
        "Text color is inherited and stays on top of the band."
      ],
      "motion": [
        "The band grows left to right with the standard ease, like a real marker stroke."
      ],
      "reducedMotion": "The highlight is already there.",
      "performance": [
        "One CSS transition on background-size; no JavaScript animation."
      ],
      "responsive": [
        "Each wrapped line gets its own highlight thanks to box-decoration-break: clone."
      ],
      "related": [
        "underline-draw",
        "circle-annotate",
        "read-along"
      ],
      "keywords": [
        "highlight",
        "marker",
        "highlighter",
        "mark",
        "emphasis",
        "text highlight"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "marker",
            "underline",
            "box"
          ]
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/marker-highlight",
      "markdown": "https://textlab.dev/components/marker-highlight.md",
      "registry": "https://textlab.dev/r/marker-highlight.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/marker-highlight.json"
    },
    {
      "name": "underline-draw",
      "title": "Underline draw",
      "description": "A hand-drawn underline sketches in.",
      "category": "draw",
      "exportName": "UnderlineDraw",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "line",
        "wave",
        "scribble"
      ],
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "description": "The words to underline.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"line\" | \"wave\" | \"scribble\"",
          "default": "\"line\"",
          "description": "A confident curve, a squiggle, or a quick double pass."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Pen color."
        },
        {
          "name": "thickness",
          "type": "number",
          "default": "3",
          "description": "Stroke width in px, constant at any size."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.6 (0.9 scribble)"
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"standard\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<h2>Ship <UnderlineDraw>today</UnderlineDraw>, not someday</h2>",
      "whenToUse": [
        "Emphasising one word or phrase inside a headline.",
        "Friendly, human brands."
      ],
      "whenNotToUse": [
        "Links; people expect a real underline there.",
        "Several underlines in one paragraph."
      ],
      "accessibility": [
        "The underline is decorative SVG; the words are real text.",
        "Emphasis is visual only; add <strong> or <em> if it matters semantically."
      ],
      "motion": [
        "Draws with the standard ease so the pen speeds up and settles like a real stroke."
      ],
      "reducedMotion": "The underline is drawn from the start.",
      "performance": [
        "One SVG path animated with pathLength."
      ],
      "related": [
        "marker-highlight",
        "circle-annotate",
        "strike-through"
      ],
      "keywords": [
        "underline",
        "hand drawn",
        "annotation",
        "squiggle",
        "rough notation",
        "emphasis"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "line",
            "wave",
            "scribble"
          ]
        },
        {
          "prop": "thickness",
          "kind": "range",
          "min": 1,
          "max": 8,
          "step": 0.5
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/underline-draw",
      "markdown": "https://textlab.dev/components/underline-draw.md",
      "registry": "https://textlab.dev/r/underline-draw.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/underline-draw.json"
    },
    {
      "name": "strike-through",
      "title": "Strike-through",
      "description": "Cross out a word and write the new one.",
      "category": "draw",
      "exportName": "StrikeThrough",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "replace",
        "line",
        "scribble"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"replace\" | \"line\" | \"scribble\"",
          "default": "\"line\"",
          "description": "Cross out and write a correction, a single line, or a scratch-out."
        },
        {
          "name": "replacement",
          "type": "string",
          "description": "The correction written above, for replace."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Pen color."
        },
        {
          "name": "thickness",
          "type": "number",
          "default": "3",
          "description": "Stroke width in px."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.5"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<p>Meetings that could have been an <StrikeThrough text=\"email\" variant=\"replace\" replacement=\"👍\" /></p>",
      "whenToUse": [
        "Witty headlines that correct themselves.",
        "Pricing before and after, changelog wording."
      ],
      "whenNotToUse": [
        "Legal or factual corrections; use <del> and <ins> styled plainly.",
        "More than one per line."
      ],
      "accessibility": [
        "Screen readers hear the meaning: the replacement and what it replaces, or that the word is struck out.",
        "The pen line and correction are decorative."
      ],
      "motion": [
        "The old word dims just after the pen passes, the correction arrives last."
      ],
      "reducedMotion": "Shown already struck, with the correction in place.",
      "performance": [
        "One path and one small text element."
      ],
      "related": [
        "underline-draw",
        "circle-annotate",
        "text-morph"
      ],
      "keywords": [
        "strikethrough",
        "cross out",
        "correction",
        "hand drawn",
        "edit"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "replace",
            "line",
            "scribble"
          ]
        }
      ],
      "preset": {
        "text": "email",
        "replacement": "👍"
      },
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/strike-through",
      "markdown": "https://textlab.dev/components/strike-through.md",
      "registry": "https://textlab.dev/r/strike-through.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/strike-through.json"
    },
    {
      "name": "circle-annotate",
      "title": "Circle annotate",
      "description": "A pen circles the word that matters.",
      "category": "draw",
      "exportName": "CircleAnnotate",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "circle",
        "box",
        "bracket"
      ],
      "props": [
        {
          "name": "children",
          "type": "ReactNode",
          "description": "The word or phrase to mark.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"circle\" | \"box\" | \"bracket\"",
          "default": "\"circle\"",
          "description": "Loop around it, frame it, or bracket it."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Pen color."
        },
        {
          "name": "thickness",
          "type": "number",
          "default": "2.5",
          "description": "Stroke width in px."
        },
        {
          "name": "padding",
          "type": "number",
          "default": "0.25",
          "description": "Room around the word in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"hover\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "delay",
          "type": "number",
          "default": "0",
          "description": "Seconds before the first part moves."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "0.9"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<p>The only plan that <CircleAnnotate>scales</CircleAnnotate> with you</p>",
      "whenToUse": [
        "Pointing at the one word that sells the sentence.",
        "Pricing tiers, comparisons and testimonials."
      ],
      "whenNotToUse": [
        "Several marks in one view; each one weakens the others.",
        "Buttons and links."
      ],
      "accessibility": [
        "Decorative SVG; the text is untouched.",
        "Add <strong> or <mark> if the emphasis must be conveyed to assistive tech."
      ],
      "motion": [
        "One continuous stroke with an overshoot, drawn with the standard ease."
      ],
      "reducedMotion": "The mark is drawn from the start.",
      "performance": [
        "One SVG path animated with pathLength."
      ],
      "related": [
        "underline-draw",
        "marker-highlight",
        "strike-through"
      ],
      "keywords": [
        "circle",
        "annotation",
        "hand drawn",
        "highlight word",
        "rough notation",
        "box"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "circle",
            "box",
            "bracket"
          ]
        }
      ],
      "preset": {},
      "added": "2026-10-17",
      "url": "https://textlab.dev/components/circle-annotate",
      "markdown": "https://textlab.dev/components/circle-annotate.md",
      "registry": "https://textlab.dev/r/circle-annotate.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/circle-annotate.json"
    },
    {
      "name": "magnetic-text",
      "title": "Magnetic text",
      "description": "Letters lean toward the cursor and spring back.",
      "category": "cursor",
      "exportName": "MagneticText",
      "dependencies": [],
      "variants": [
        "attract",
        "repel"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"attract\" | \"repel\"",
          "default": "\"attract\"",
          "description": "Lean toward the cursor, or shy away from it."
        },
        {
          "name": "radius",
          "type": "number",
          "default": "2.5",
          "description": "Reach of the field in em."
        },
        {
          "name": "strength",
          "type": "number",
          "default": "0.28",
          "description": "Largest offset in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<MagneticText as=\"h2\" text=\"Come closer\" />",
      "whenToUse": [
        "Hero words and footers that reward exploring with the mouse.",
        "Playful portfolios."
      ],
      "whenNotToUse": [
        "Touch-only layouts; nothing happens without a mouse or pen.",
        "Long text; every letter is tracked."
      ],
      "accessibility": [
        "Letters are aria-hidden; the text is read once.",
        "Offsets are small, so words stay readable while they move."
      ],
      "motion": [
        "Pull follows a smoothstep falloff and each letter eases toward its target.",
        "Letters glide home when the pointer leaves."
      ],
      "reducedMotion": "Still, plain text.",
      "performance": [
        "Letter centers are measured once and on resize; the frame loop only writes transforms."
      ],
      "related": [
        "variable-proximity",
        "cursor-trail",
        "jelly-text"
      ],
      "keywords": [
        "magnetic",
        "cursor",
        "hover",
        "attract",
        "repel",
        "interactive text"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "attract",
            "repel"
          ]
        },
        {
          "prop": "radius",
          "kind": "range",
          "min": 1,
          "max": 6,
          "step": 0.1
        },
        {
          "prop": "strength",
          "kind": "range",
          "min": 0.05,
          "max": 0.8,
          "step": 0.01
        }
      ],
      "preset": {
        "text": "Come closer"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/magnetic-text",
      "markdown": "https://textlab.dev/components/magnetic-text.md",
      "registry": "https://textlab.dev/r/magnetic-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/magnetic-text.json"
    },
    {
      "name": "variable-proximity",
      "title": "Variable proximity",
      "description": "Weight and width swell near the cursor.",
      "category": "cursor",
      "exportName": "VariableProximity",
      "dependencies": [],
      "variants": [
        "weight",
        "width",
        "slant"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"weight\" | \"width\" | \"slant\"",
          "default": "\"weight\"",
          "description": "Which variable axis responds."
        },
        {
          "name": "from",
          "type": "number",
          "default": "axis default",
          "description": "Axis value far from the cursor."
        },
        {
          "name": "to",
          "type": "number",
          "default": "axis default",
          "description": "Axis value under the cursor."
        },
        {
          "name": "radius",
          "type": "number",
          "default": "3",
          "description": "Reach in em."
        },
        {
          "name": "font",
          "type": "string",
          "default": "var(--tl-font-flex)",
          "description": "A variable font with the axis. Roboto Flex has weight, width and slant."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<VariableProximity as=\"h1\" text=\"Variable type\" variant=\"width\" />",
      "whenToUse": [
        "Type foundries, design tools and editorial heroes.",
        "Showing off a variable font."
      ],
      "whenNotToUse": [
        "Fonts without the axis; nothing will change.",
        "Layouts that cannot tolerate letters changing width."
      ],
      "accessibility": [
        "Letters are aria-hidden; the text is read once.",
        "Weight and width change the letters' footprint, so neighbours move slightly; keep it to headlines."
      ],
      "motion": [
        "Each letter eases toward its own target with damping.",
        "Updates stop once every letter has settled."
      ],
      "reducedMotion": "Static text at the resting axis value.",
      "performance": [
        "Writes font-variation-settings only while values change; text reflow is limited to the line."
      ],
      "related": [
        "breathing-text",
        "magnetic-text",
        "spotlight-text"
      ],
      "keywords": [
        "variable font",
        "font weight",
        "proximity",
        "cursor",
        "wght",
        "wdth",
        "roboto flex"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "weight",
            "width",
            "slant"
          ]
        },
        {
          "prop": "radius",
          "kind": "range",
          "min": 1,
          "max": 8,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Variable type"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/variable-proximity",
      "markdown": "https://textlab.dev/components/variable-proximity.md",
      "registry": "https://textlab.dev/r/variable-proximity.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/variable-proximity.json"
    },
    {
      "name": "cursor-trail",
      "title": "Cursor trail",
      "description": "A word trails behind the cursor letter by letter.",
      "category": "cursor",
      "exportName": "CursorTrail",
      "dependencies": [],
      "variants": [
        "chain",
        "scatter"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"chain\" | \"scatter\"",
          "default": "\"chain\"",
          "description": "String letters along your path, or let each chase the cursor."
        },
        {
          "name": "children",
          "type": "ReactNode",
          "description": "Content of the area the trail lives in."
        },
        {
          "name": "spacing",
          "type": "number",
          "default": "0.62",
          "description": "Distance between letters on the chain, in em."
        },
        {
          "name": "trailClassName",
          "type": "string",
          "description": "Class for the trailing letters."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<CursorTrail text=\"hello\" className=\"h-80 rounded-xl border\"><p>Move your mouse here</p></CursorTrail>",
      "whenToUse": [
        "Playful hero areas and 404 pages.",
        "Portfolio sections that invite exploring."
      ],
      "whenNotToUse": [
        "Over content people need to read or click; the trail sits on top.",
        "Touch-only experiences."
      ],
      "accessibility": [
        "The trail is decorative and aria-hidden; it never blocks clicks.",
        "Content inside the area works normally."
      ],
      "motion": [
        "Chain letters hold a fixed distance and turn to follow the path.",
        "Letters fade in and out with the pointer instead of popping."
      ],
      "reducedMotion": "No trail; the area renders its content only.",
      "performance": [
        "One frame loop that writes transforms; pauses when offscreen."
      ],
      "related": [
        "magnetic-text",
        "path-text",
        "spotlight-text"
      ],
      "keywords": [
        "cursor trail",
        "mouse trail",
        "follow cursor",
        "snake",
        "interactive"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "chain",
            "scatter"
          ]
        }
      ],
      "preset": {
        "text": "follow me"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/cursor-trail",
      "markdown": "https://textlab.dev/components/cursor-trail.md",
      "registry": "https://textlab.dev/r/cursor-trail.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/cursor-trail.json"
    },
    {
      "name": "flip-roll",
      "title": "Flip roll",
      "description": "Hover and the label rolls up into a fresh copy.",
      "category": "cursor",
      "exportName": "FlipRoll",
      "dependencies": [],
      "variants": [
        "stagger",
        "up",
        "down"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"stagger\" | \"up\" | \"down\"",
          "default": "\"stagger\"",
          "description": "Roll letter by letter, or the whole label up or down."
        },
        {
          "name": "duration",
          "type": "number",
          "default": "0.6",
          "description": "Seconds per roll."
        },
        {
          "name": "stagger",
          "type": "number",
          "default": "0.02",
          "description": "Seconds between letters."
        },
        {
          "name": "ease",
          "type": "EaseName | [number, number, number, number]",
          "default": "\"emphasized\"",
          "description": "Easing token or cubic bezier."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<a href=\"/contact\" className=\"inline-flex\"><FlipRoll text=\"Get in touch\" /></a>",
      "whenToUse": [
        "Navigation links, buttons and footer links.",
        "Any short label that deserves a tactile hover."
      ],
      "whenNotToUse": [
        "Long labels; the stagger tail drags.",
        "Inline links in body copy."
      ],
      "accessibility": [
        "Keyboard focus inside triggers it, through focus capture.",
        "Screen readers read the label once."
      ],
      "motion": [
        "A long-tailed ease over 0.6s with a cross-fade, so it starts quick and lands soft.",
        "Leaving reverses immediately, interrupting cleanly."
      ],
      "reducedMotion": "Hover changes nothing.",
      "performance": [
        "CSS transitions on transform only."
      ],
      "related": [
        "hover-scramble",
        "text-morph",
        "magnetic-text"
      ],
      "keywords": [
        "roll",
        "flip",
        "hover",
        "button text",
        "nav link",
        "text swap"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "stagger",
            "up",
            "down"
          ]
        }
      ],
      "preset": {
        "text": "Hover to roll"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/flip-roll",
      "markdown": "https://textlab.dev/components/flip-roll.md",
      "registry": "https://textlab.dev/r/flip-roll.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/flip-roll.json"
    },
    {
      "name": "tilt-text",
      "title": "Tilt text",
      "description": "Extruded type tilts toward the pointer.",
      "category": "cursor",
      "exportName": "TiltText",
      "dependencies": [],
      "variants": [
        "extrude",
        "float",
        "shadow"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"extrude\" | \"float\" | \"shadow\"",
          "default": "\"extrude\"",
          "description": "Solid depth, floating above a ground shadow, or a long pointer-driven shadow."
        },
        {
          "name": "tilt",
          "type": "number",
          "default": "18",
          "description": "Largest tilt in degrees."
        },
        {
          "name": "depth",
          "type": "number",
          "default": "12",
          "description": "Extrusion layers."
        },
        {
          "name": "sideColor",
          "type": "string",
          "default": "a darker mix of the text color",
          "description": "Color of the extruded sides."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<TiltText as=\"h1\" text=\"Depth\" />",
      "whenToUse": [
        "Bold single words in hero sections.",
        "Product names and numbers with a physical feel."
      ],
      "whenNotToUse": [
        "Long or wrapping text.",
        "Dense layouts; the tilt needs breathing room."
      ],
      "accessibility": [
        "Depth layers are aria-hidden copies; the word is read once.",
        "At rest the text faces forward and reads normally."
      ],
      "motion": [
        "The tilt eases toward the pointer and back to center when it leaves."
      ],
      "reducedMotion": "Still, front-facing text with its depth intact.",
      "performance": [
        "Transforms on one 3D group; layers are static inside it."
      ],
      "related": [
        "extrude-text",
        "depth-layers",
        "glass-text"
      ],
      "keywords": [
        "3d text",
        "tilt",
        "extrude",
        "perspective",
        "parallax",
        "hover"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "extrude",
            "float",
            "shadow"
          ]
        },
        {
          "prop": "tilt",
          "kind": "range",
          "min": 4,
          "max": 40,
          "step": 1
        }
      ],
      "preset": {
        "text": "Depth"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/tilt-text",
      "markdown": "https://textlab.dev/components/tilt-text.md",
      "registry": "https://textlab.dev/r/tilt-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/tilt-text.json"
    },
    {
      "name": "extrude-text",
      "title": "Extrude text",
      "description": "Depth grows from the letters on hover.",
      "category": "cursor",
      "exportName": "ExtrudeText",
      "dependencies": [],
      "variants": [
        "block",
        "long-shadow",
        "stack"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"block\" | \"long-shadow\" | \"stack\"",
          "default": "\"block\"",
          "description": "A solid slab, a fading diagonal shadow, or stacked outlines."
        },
        {
          "name": "depth",
          "type": "number",
          "default": "14",
          "description": "Depth steps at full extrusion."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Depth color."
        },
        {
          "name": "hover",
          "type": "boolean",
          "default": "true",
          "description": "Grow on hover from a shallow rest; false keeps it fully extruded."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ExtrudeText as=\"h2\" text=\"Pop\" variant=\"long-shadow\" />",
      "whenToUse": [
        "Retro, poster and toy-like brands.",
        "Big words that should feel pressable."
      ],
      "whenNotToUse": [
        "Small text; the depth smears it.",
        "Minimal, quiet interfaces."
      ],
      "accessibility": [
        "Plain text with shadows; nothing is split.",
        "Focus-visible grows it too when the element is focusable."
      ],
      "motion": [
        "The face lifts up and left as depth grows, so it feels like it rises off the page.",
        "260 ms with the enter ease; hover only counts on devices with a fine pointer, so taps never leave it stuck raised."
      ],
      "reducedMotion": "Fully extruded and still.",
      "performance": [
        "Pure CSS: a registered property interpolates the shadow stack."
      ],
      "related": [
        "tilt-text",
        "depth-layers",
        "neon-text"
      ],
      "keywords": [
        "3d text",
        "extrude",
        "long shadow",
        "retro",
        "text shadow",
        "block letters"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "block",
            "long-shadow",
            "stack"
          ]
        },
        {
          "prop": "depth",
          "kind": "range",
          "min": 4,
          "max": 30,
          "step": 1
        }
      ],
      "preset": {
        "text": "Press"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/extrude-text",
      "markdown": "https://textlab.dev/components/extrude-text.md",
      "registry": "https://textlab.dev/r/extrude-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/extrude-text.json"
    },
    {
      "name": "letter-drop",
      "title": "Letter drop",
      "description": "Letters fall, bounce and settle on the floor.",
      "category": "physics",
      "exportName": "LetterDrop",
      "dependencies": [],
      "variants": [
        "drop",
        "pile",
        "toss"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"drop\" | \"pile\" | \"toss\"",
          "default": "\"drop\"",
          "description": "Fall straight and bounce, tumble and land askew, or also fling up on click or tap."
        },
        {
          "name": "height",
          "type": "number",
          "default": "2.2",
          "description": "Fall height in em."
        },
        {
          "name": "restitution",
          "type": "number",
          "default": "0.45",
          "description": "Bounciness, 0 to 0.9."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "trigger",
          "type": "\"mount\" | \"inView\" | \"manual\"",
          "default": "\"inView\"",
          "description": "What starts the animation. manual follows `active`."
        },
        {
          "name": "active",
          "type": "boolean",
          "default": "false",
          "description": "Drives trigger=\"manual\"."
        },
        {
          "name": "once",
          "type": "boolean",
          "default": "true",
          "description": "In view: play once instead of on every entry."
        },
        {
          "name": "stagger",
          "type": "number",
          "description": "Seconds between parts.",
          "default": "0.06"
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<LetterDrop as=\"h2\" text=\"Gravity\" variant=\"pile\" />",
      "whenToUse": [
        "Playful launches, games and kids' products.",
        "A single word that should land with weight."
      ],
      "whenNotToUse": [
        "Serious contexts.",
        "Long sentences; each letter is a body."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Toss works with click and tap; it is a bonus, never needed to read the word."
      ],
      "motion": [
        "Real gravity with energy lost on every bounce.",
        "Pile letters spin as they fall and settle up to 8 degrees off true."
      ],
      "reducedMotion": "The word sits on the baseline from the start.",
      "performance": [
        "One frame loop that stops once every letter has settled."
      ],
      "related": [
        "elastic-text",
        "jelly-text",
        "char-reveal"
      ],
      "keywords": [
        "physics",
        "gravity",
        "bounce",
        "drop",
        "falling letters",
        "toss"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "drop",
            "pile",
            "toss"
          ]
        },
        {
          "prop": "restitution",
          "kind": "range",
          "min": 0,
          "max": 0.85,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Gravity"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/letter-drop",
      "markdown": "https://textlab.dev/components/letter-drop.md",
      "registry": "https://textlab.dev/r/letter-drop.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/letter-drop.json"
    },
    {
      "name": "elastic-text",
      "title": "Elastic text",
      "description": "Drag a letter and the word stretches like rubber.",
      "category": "physics",
      "exportName": "ElasticText",
      "dependencies": [],
      "variants": [
        "band",
        "sling"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"band\" | \"sling\"",
          "default": "\"band\"",
          "description": "Stretch and ease back, or snap back hard and overshoot."
        },
        {
          "name": "reach",
          "type": "number",
          "default": "3",
          "description": "How many neighbours follow a pulled letter."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ElasticText as=\"h2\" text=\"Pull me\" variant=\"sling\" />",
      "whenToUse": [
        "Toy-like hero moments and interactive posters.",
        "Touch devices, where dragging feels natural."
      ],
      "whenNotToUse": [
        "Text inside scrolling lists; dragging captures the pointer.",
        "Anything people should select and copy."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Dragging is decorative; nothing depends on it."
      ],
      "motion": [
        "Pull resistance grows with distance, like rubber.",
        "Neighbours follow with exponential falloff; release springs them home."
      ],
      "reducedMotion": "Plain text; dragging does nothing.",
      "performance": [
        "Springs update only while something is moving."
      ],
      "related": [
        "jelly-text",
        "scroll-stretch",
        "letter-drop"
      ],
      "keywords": [
        "elastic",
        "rubber band",
        "drag",
        "stretch",
        "spring",
        "physics"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "band",
            "sling"
          ]
        },
        {
          "prop": "reach",
          "kind": "range",
          "min": 1,
          "max": 8,
          "step": 1
        }
      ],
      "preset": {
        "text": "Pull me"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/elastic-text",
      "markdown": "https://textlab.dev/components/elastic-text.md",
      "registry": "https://textlab.dev/r/elastic-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/elastic-text.json"
    },
    {
      "name": "jelly-text",
      "title": "Jelly text",
      "description": "Letters wobble like jelly when touched.",
      "category": "physics",
      "exportName": "JellyText",
      "dependencies": [],
      "variants": [
        "squash",
        "wobble"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"squash\" | \"wobble\"",
          "default": "\"squash\"",
          "description": "Squish flat and spring tall, or rock side to side."
        },
        {
          "name": "strength",
          "type": "number",
          "default": "1",
          "description": "How hard a touch kicks the letter."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<JellyText as=\"h2\" text=\"Squishy\" />",
      "whenToUse": [
        "Playful brands, games and empty states.",
        "Words that invite poking, on mouse or touch."
      ],
      "whenNotToUse": [
        "Interactive controls; letters are not buttons.",
        "Body text."
      ],
      "accessibility": [
        "Letters are aria-hidden; the word is read once.",
        "Works with touch through pointer down."
      ],
      "motion": [
        "Underdamped springs ring out over about a second.",
        "Neighbours get a smaller kick so the wobble spreads."
      ],
      "reducedMotion": "Touch does nothing; plain text.",
      "performance": [
        "The frame loop runs only while a spring is moving."
      ],
      "related": [
        "elastic-text",
        "magnetic-text",
        "letter-drop"
      ],
      "keywords": [
        "jelly",
        "wobble",
        "squash and stretch",
        "springy",
        "hover",
        "bouncy"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "squash",
            "wobble"
          ]
        },
        {
          "prop": "strength",
          "kind": "range",
          "min": 0.2,
          "max": 3,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Squishy"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/jelly-text",
      "markdown": "https://textlab.dev/components/jelly-text.md",
      "registry": "https://textlab.dev/r/jelly-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/jelly-text.json"
    },
    {
      "name": "wave-text",
      "title": "Wave text",
      "description": "A sine wave runs through the line.",
      "category": "physics",
      "exportName": "WaveText",
      "dependencies": [],
      "variants": [
        "wave",
        "bounce",
        "float"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"wave\" | \"bounce\" | \"float\"",
          "default": "\"wave\"",
          "description": "A smooth sine, hops off the baseline, or a gentle drift and tilt."
        },
        {
          "name": "amplitude",
          "type": "number",
          "default": "0.2",
          "description": "Wave height in em."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "1.6",
          "description": "Seconds per cycle."
        },
        {
          "name": "spread",
          "type": "number",
          "default": "0.08",
          "description": "Seconds between neighbouring letters."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<WaveText text=\"Hello, sailor\" variant=\"bounce\" />",
      "whenToUse": [
        "Loading states, celebrations and playful labels.",
        "Short words that should feel alive."
      ],
      "whenNotToUse": [
        "Anything people must read carefully.",
        "Several looping effects on the same screen."
      ],
      "accessibility": [
        "Letters are aria-hidden; the text is read once.",
        "It never stops on its own, so keep it short and decorative."
      ],
      "motion": [
        "Negative delays start every letter mid-cycle, so the wave is already in motion on load."
      ],
      "reducedMotion": "Still, plain text.",
      "performance": [
        "Pure CSS keyframes on transform; no JavaScript after render."
      ],
      "related": [
        "breathing-text",
        "jelly-text",
        "path-text"
      ],
      "keywords": [
        "wave",
        "bounce",
        "wavy text",
        "sine",
        "float",
        "loop"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "wave",
            "bounce",
            "float"
          ]
        },
        {
          "prop": "amplitude",
          "kind": "range",
          "min": 0.05,
          "max": 0.6,
          "step": 0.01
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 0.6,
          "max": 4,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Riding the wave"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/wave-text",
      "markdown": "https://textlab.dev/components/wave-text.md",
      "registry": "https://textlab.dev/r/wave-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/wave-text.json"
    },
    {
      "name": "breathing-text",
      "title": "Breathing text",
      "description": "Weight breathes in and out over time.",
      "category": "physics",
      "exportName": "BreathingText",
      "dependencies": [],
      "variants": [
        "breathe",
        "heartbeat"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"breathe\" | \"heartbeat\"",
          "default": "\"breathe\"",
          "description": "A slow swell through the word, or a double thump and rest."
        },
        {
          "name": "from",
          "type": "number",
          "default": "250",
          "description": "Weight at rest."
        },
        {
          "name": "to",
          "type": "number",
          "default": "800",
          "description": "Weight at the peak."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "3.6",
          "description": "Seconds per breath."
        },
        {
          "name": "font",
          "type": "string",
          "default": "var(--tl-font-flex)",
          "description": "A variable font with a weight axis."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<BreathingText as=\"h2\" text=\"Inhale, exhale\" />",
      "whenToUse": [
        "Meditation, health and calm brands.",
        "Idle and waiting states that should feel alive, not busy."
      ],
      "whenNotToUse": [
        "Fonts without a weight axis.",
        "Layouts where width changes would push neighbours."
      ],
      "accessibility": [
        "Letters are aria-hidden; the text is read once.",
        "Slow and continuous, with no flashing."
      ],
      "motion": [
        "The breath travels through the word 90 ms per letter.",
        "Heartbeat thumps twice in the first 40% of each cycle, then rests."
      ],
      "reducedMotion": "A still word at a medium weight.",
      "performance": [
        "A registered CSS property drives font-variation-settings; text reflows within the line each frame, so use it on short words."
      ],
      "related": [
        "variable-proximity",
        "wave-text",
        "neon-text"
      ],
      "keywords": [
        "variable font",
        "breathing",
        "weight",
        "pulse",
        "heartbeat",
        "calm"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "breathe",
            "heartbeat"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": 1,
          "max": 8,
          "step": 0.2
        }
      ],
      "preset": {
        "text": "Inhale, exhale"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/breathing-text",
      "markdown": "https://textlab.dev/components/breathing-text.md",
      "registry": "https://textlab.dev/r/breathing-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/breathing-text.json"
    },
    {
      "name": "path-text",
      "title": "Path text",
      "description": "Text travels along a wave, circle or spiral.",
      "category": "path",
      "exportName": "PathText",
      "dependencies": [],
      "variants": [
        "wave",
        "circle",
        "spiral"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"wave\" | \"circle\" | \"spiral\"",
          "default": "\"wave\"",
          "description": "Flow along a sine, turn as a badge, or wind inward."
        },
        {
          "name": "speed",
          "type": "number",
          "default": "60",
          "description": "Path units per second. Negative reverses."
        },
        {
          "name": "separator",
          "type": "string",
          "default": "\"·\"",
          "description": "Mark between repeats."
        },
        {
          "name": "fontSize",
          "type": "number",
          "description": "Size in path units. Defaults per variant."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<PathText text=\"Open daily\" variant=\"circle\" className=\"w-48\" />",
      "whenToUse": [
        "Badges, stamps and seals.",
        "Decorative bands between sections."
      ],
      "whenNotToUse": [
        "Essential copy; curved text is slower to read.",
        "Long sentences on the spiral."
      ],
      "accessibility": [
        "The SVG is decorative; the text is read once from a hidden copy."
      ],
      "motion": [
        "The loop wraps at a measured seam, so it never visibly jumps.",
        "The circle badge turns once every 15 seconds at the default speed."
      ],
      "reducedMotion": "Text sits still on its path.",
      "performance": [
        "Wave and spiral update one attribute per frame; circle is a CSS rotation."
      ],
      "responsive": [
        "Scales with its width; set a width class."
      ],
      "related": [
        "velocity-marquee",
        "cursor-trail",
        "wave-text"
      ],
      "keywords": [
        "text on path",
        "circular text",
        "badge",
        "spiral",
        "svg textpath",
        "rotating text"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "wave",
            "circle",
            "spiral"
          ]
        },
        {
          "prop": "speed",
          "kind": "range",
          "min": -200,
          "max": 200,
          "step": 10
        }
      ],
      "preset": {
        "text": "Text that follows the line"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/path-text",
      "markdown": "https://textlab.dev/components/path-text.md",
      "registry": "https://textlab.dev/r/path-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/path-text.json"
    },
    {
      "name": "ascii-text",
      "title": "ASCII text",
      "description": "The title rendered in ASCII glyphs that ripple.",
      "category": "pixel",
      "exportName": "AsciiText",
      "dependencies": [],
      "variants": [
        "classic",
        "blocks",
        "braille"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"classic\" | \"blocks\" | \"braille\"",
          "default": "\"classic\"",
          "description": "Character ramp: .:-=+*#%@, shade blocks, or braille dots for fine detail."
        },
        {
          "name": "density",
          "type": "number",
          "default": "0.1",
          "description": "Cell size as a share of the font size."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<AsciiText as=\"h1\" text=\"TERMINAL\" className=\"font-black text-8xl\" />",
      "whenToUse": [
        "Developer tools, retro computing and terminal aesthetics.",
        "Large, heavy display words; thin type has too little to sample."
      ],
      "whenNotToUse": [
        "Small text; cells need room.",
        "When the word must be crisp; it is deliberately coarse."
      ],
      "accessibility": [
        "The canvas is decorative; screen readers read the hidden text.",
        "A hidden copy sizes the box, so layout matches the real text."
      ],
      "motion": [
        "Glyphs resolve in a scattered order over 0.9 s when the title scrolls in.",
        "The pointer sends a decaying ripple through the character ramp."
      ],
      "reducedMotion": "Shows the plain text instead of the canvas.",
      "performance": [
        "Rasterises once per resize; draws only while revealing or while the pointer is over it.",
        "Device pixel ratio capped at 2."
      ],
      "related": [
        "dither-text",
        "pixel-sort",
        "matrix-decode"
      ],
      "keywords": [
        "ascii",
        "ascii art",
        "braille",
        "terminal",
        "canvas text",
        "retro"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "classic",
            "blocks",
            "braille"
          ]
        },
        {
          "prop": "density",
          "kind": "range",
          "min": 0.06,
          "max": 0.3,
          "step": 0.01
        }
      ],
      "preset": {
        "text": "ASCII"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/ascii-text",
      "markdown": "https://textlab.dev/components/ascii-text.md",
      "registry": "https://textlab.dev/r/ascii-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/ascii-text.json"
    },
    {
      "name": "dither-text",
      "title": "Dither text",
      "description": "Ordered dither and halftone dots form the type.",
      "category": "pixel",
      "exportName": "DitherText",
      "dependencies": [],
      "variants": [
        "dither",
        "halftone"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"dither\" | \"halftone\"",
          "default": "\"dither\"",
          "description": "Square Bayer pixels, or dots on an offset screen."
        },
        {
          "name": "pitch",
          "type": "number",
          "default": "0.035",
          "description": "Pixel or dot pitch as a share of the font size."
        },
        {
          "name": "shade",
          "type": "number",
          "default": "0.55",
          "description": "How much lighter the bottom of the letters gets, 0 to 1."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<DitherText as=\"h1\" text=\"Print\" variant=\"halftone\" />",
      "whenToUse": [
        "Editorial, zine and print-inspired brands.",
        "Heavy display type with room to show texture."
      ],
      "whenNotToUse": [
        "Small or thin text.",
        "Busy photographic backgrounds behind it."
      ],
      "accessibility": [
        "Decorative canvas; the text is read from a hidden copy."
      ],
      "motion": [
        "Dithers in over 1.1 s as it enters the viewport, in Bayer order.",
        "The pointer adds a soft spot of extra ink."
      ],
      "reducedMotion": "Plain text instead of the canvas.",
      "performance": [
        "Rasterises on resize; draws only during the reveal and while the pointer moves."
      ],
      "related": [
        "ascii-text",
        "pixel-sort",
        "particle-text"
      ],
      "keywords": [
        "dither",
        "halftone",
        "bayer",
        "print",
        "pixel",
        "risograph"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "dither",
            "halftone"
          ]
        },
        {
          "prop": "pitch",
          "kind": "range",
          "min": 0.015,
          "max": 0.08,
          "step": 0.005
        }
      ],
      "preset": {
        "text": "Print"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/dither-text",
      "markdown": "https://textlab.dev/components/dither-text.md",
      "registry": "https://textlab.dev/r/dither-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/dither-text.json"
    },
    {
      "name": "pixel-sort",
      "title": "Pixel sort",
      "description": "Rows sort into place like a glitch artwork.",
      "category": "pixel",
      "exportName": "PixelSort",
      "dependencies": [],
      "variants": [
        "smear",
        "sort"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"smear\" | \"sort\"",
          "default": "\"smear\"",
          "description": "Retracting streaks, or bands of rows sliding into register."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.4"
        },
        {
          "name": "hover",
          "type": "boolean",
          "default": "true",
          "description": "Tear the image again briefly on hover."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<PixelSort as=\"h1\" text=\"Corrupted\" />",
      "whenToUse": [
        "Music, art and experimental brands.",
        "Heavy display words on dark or light flat backgrounds."
      ],
      "whenNotToUse": [
        "Small text; rows are too few to read as bands.",
        "Gradients behind the text; the canvas is flat color."
      ],
      "accessibility": [
        "Decorative canvas; the text is read from a hidden copy.",
        "Settles into clean type; nothing keeps flashing."
      ],
      "motion": [
        "Settles from full glitch to clean in about 1.4 s after it enters view.",
        "Hover re-tears to 45% and settles again."
      ],
      "reducedMotion": "Plain text instead of the canvas.",
      "performance": [
        "Per-pixel work runs only while settling; device pixel ratio capped at 1.5 for this effect."
      ],
      "related": [
        "glitch-text",
        "dither-text",
        "slice-reveal"
      ],
      "keywords": [
        "pixel sort",
        "glitch art",
        "datamosh",
        "smear",
        "canvas",
        "experimental"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "smear",
            "sort"
          ]
        }
      ],
      "preset": {
        "text": "Corrupted"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/pixel-sort",
      "markdown": "https://textlab.dev/components/pixel-sort.md",
      "registry": "https://textlab.dev/r/pixel-sort.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/pixel-sort.json"
    },
    {
      "name": "particle-text",
      "title": "Particle text",
      "description": "Particles gather into letters and scatter on touch.",
      "category": "pixel",
      "exportName": "ParticleText",
      "dependencies": [],
      "variants": [
        "dust",
        "sand",
        "stars"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"dust\" | \"sand\" | \"stars\"",
          "default": "\"dust\"",
          "description": "Fine quick dust, heavy warm sand that sags under the pointer, or twinkling stars."
        },
        {
          "name": "gap",
          "type": "number",
          "default": "3",
          "description": "Sampling gap in px; smaller means more particles."
        },
        {
          "name": "radius",
          "type": "number",
          "default": "1.2",
          "description": "Pointer push radius in em."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ParticleText as=\"h1\" text=\"Stardust\" variant=\"stars\" />",
      "whenToUse": [
        "Launch pages and immersive heroes.",
        "One large word per screen."
      ],
      "whenNotToUse": [
        "Several instances; each one simulates thousands of particles.",
        "Low-power contexts where battery matters."
      ],
      "accessibility": [
        "Decorative canvas; the text is read from a hidden copy.",
        "Particles settle into readable letters within a second or two."
      ],
      "motion": [
        "Particles gather from a wide cloud when the word scrolls in.",
        "The pointer pushes them away; springs pull them home."
      ],
      "reducedMotion": "Plain text instead of the canvas.",
      "performance": [
        "Capped at 9,000 particles; the loop sleeps once they settle (except stars, which twinkle).",
        "Typed arrays, one draw call per frame for dust."
      ],
      "related": [
        "dither-text",
        "noise-dissolve",
        "letter-drop"
      ],
      "keywords": [
        "particles",
        "particle text",
        "canvas",
        "interactive",
        "stardust",
        "sand"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "dust",
            "sand",
            "stars"
          ]
        },
        {
          "prop": "gap",
          "kind": "range",
          "min": 2,
          "max": 6,
          "step": 0.5
        }
      ],
      "preset": {
        "text": "Stardust"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/particle-text",
      "markdown": "https://textlab.dev/components/particle-text.md",
      "registry": "https://textlab.dev/r/particle-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/particle-text.json"
    },
    {
      "name": "shader-ripple",
      "title": "Shader ripple",
      "description": "A WebGL ripple bends the title under the pointer.",
      "category": "pixel",
      "exportName": "ShaderRipple",
      "dependencies": [],
      "files": [
        "gl.ts"
      ],
      "variants": [
        "ripple",
        "lens"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"ripple\" | \"lens\"",
          "default": "\"ripple\"",
          "description": "Rings travelling out from the pointer, or a magnifying circle under it."
        },
        {
          "name": "strength",
          "type": "number",
          "default": "1",
          "description": "Effect strength, 0 to 2."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ShaderRipple as=\"h1\" text=\"Surface tension\" />",
      "whenToUse": [
        "Signature hero moments that should feel physical.",
        "Large display words people will hover."
      ],
      "whenNotToUse": [
        "Several on one page; each needs a WebGL context.",
        "Small text."
      ],
      "accessibility": [
        "The canvas is decorative; the text is read from a hidden copy.",
        "Without WebGL, or before it loads, the real text shows in place."
      ],
      "motion": [
        "Rings travel outward and fade with distance; the effect eases in and out with hover."
      ],
      "reducedMotion": "Plain text; the shader never loads.",
      "performance": [
        "The shader module is imported only when the title enters the viewport.",
        "Draws only while hovered; one texture upload per resize."
      ],
      "related": [
        "noise-dissolve",
        "liquid-text",
        "particle-text"
      ],
      "keywords": [
        "webgl",
        "shader",
        "ripple",
        "lens",
        "distortion",
        "glsl"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "ripple",
            "lens"
          ]
        },
        {
          "prop": "strength",
          "kind": "range",
          "min": 0,
          "max": 2,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Surface"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/shader-ripple",
      "markdown": "https://textlab.dev/components/shader-ripple.md",
      "registry": "https://textlab.dev/r/shader-ripple.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/shader-ripple.json"
    },
    {
      "name": "noise-dissolve",
      "title": "Noise dissolve",
      "description": "The title forms from noise and dissolves back into it.",
      "category": "pixel",
      "exportName": "NoiseDissolve",
      "dependencies": [],
      "files": [
        "gl.ts"
      ],
      "variants": [
        "burn",
        "dissolve"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"burn\" | \"dissolve\"",
          "default": "\"burn\"",
          "description": "A glowing ember edge, or a soft dissolve."
        },
        {
          "name": "duration",
          "type": "number",
          "description": "Seconds each part takes.",
          "default": "1.4"
        },
        {
          "name": "hover",
          "type": "boolean",
          "default": "true",
          "description": "Dissolve on hover and re-form on leave."
        },
        {
          "name": "edge",
          "type": "[number, number, number]",
          "default": "[1, 0.45, 0.1]",
          "description": "Burn edge color as RGB from 0 to 1."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<NoiseDissolve as=\"h1\" text=\"Ashes\" />",
      "whenToUse": [
        "Dramatic reveals for launches, games and film.",
        "Moments of transformation: before and after, old and new."
      ],
      "whenNotToUse": [
        "Text that must stay readable while hovered.",
        "Several WebGL effects on one page."
      ],
      "accessibility": [
        "Decorative canvas; the text is read from a hidden copy.",
        "Without WebGL, a CSS fade shows the real text."
      ],
      "motion": [
        "Forms over 1.4 s at a constant rate, so the burn edge travels evenly.",
        "Hover eats roughly half the letters, never all of them."
      ],
      "reducedMotion": "Plain text; the shader never loads.",
      "performance": [
        "Loaded only in view; draws only while the threshold is moving."
      ],
      "related": [
        "shader-ripple",
        "ember-text",
        "ink-bleed"
      ],
      "keywords": [
        "dissolve",
        "burn",
        "webgl",
        "shader",
        "noise",
        "disintegrate"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "burn",
            "dissolve"
          ]
        },
        {
          "prop": "duration",
          "kind": "range",
          "min": 0.5,
          "max": 3,
          "step": 0.1
        }
      ],
      "preset": {
        "text": "Ashes"
      },
      "heavy": true,
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/noise-dissolve",
      "markdown": "https://textlab.dev/components/noise-dissolve.md",
      "registry": "https://textlab.dev/r/noise-dissolve.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/noise-dissolve.json"
    },
    {
      "name": "kinetic-beat",
      "title": "Kinetic beat",
      "description": "Words hit on a beat.",
      "category": "kinetic",
      "exportName": "KineticBeat",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "pop",
        "slam",
        "bounce"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "Words to hit, one per beat.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"pop\" | \"slam\" | \"bounce\"",
          "default": "\"pop\"",
          "description": "Scale up with overshoot, crash down from large, or hop in."
        },
        {
          "name": "bpm",
          "type": "number",
          "default": "120",
          "description": "Beats per minute."
        },
        {
          "name": "loop",
          "type": "boolean",
          "default": "true",
          "description": "Start over after the last word."
        },
        {
          "name": "rest",
          "type": "number",
          "default": "2",
          "description": "Silent beats at the end of each loop."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<KineticBeat text=\"Make it loud\" bpm={128} variant=\"slam\" />",
      "whenToUse": [
        "Music, events and launch teasers.",
        "Short punchy slogans of three to six words."
      ],
      "whenNotToUse": [
        "Information people need to read at their own pace.",
        "Long sentences."
      ],
      "accessibility": [
        "The whole sentence is read once from a hidden copy.",
        "Pauses when scrolled out of view."
      ],
      "motion": [
        "One word per beat; at 120 BPM that is two words a second.",
        "Exits are quick so each hit lands clean."
      ],
      "reducedMotion": "The sentence appears whole and still.",
      "performance": [
        "One interval and one animated word at a time."
      ],
      "related": [
        "word-sequence",
        "word-rotator",
        "velocity-marquee"
      ],
      "keywords": [
        "kinetic typography",
        "beat",
        "bpm",
        "lyric",
        "slogan",
        "music"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "pop",
            "slam",
            "bounce"
          ]
        },
        {
          "prop": "bpm",
          "kind": "range",
          "min": 60,
          "max": 180,
          "step": 2
        }
      ],
      "preset": {
        "text": "Make it loud tonight"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/kinetic-beat",
      "markdown": "https://textlab.dev/components/kinetic-beat.md",
      "registry": "https://textlab.dev/r/kinetic-beat.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/kinetic-beat.json"
    },
    {
      "name": "word-sequence",
      "title": "Word sequence",
      "description": "A sentence choreographed word by word.",
      "category": "kinetic",
      "exportName": "WordSequence",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "stack",
        "stage",
        "cut"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"stack\" | \"stage\" | \"cut\"",
          "default": "\"stack\"",
          "description": "Build the sentence, spotlight each word, or hard-cut between scales."
        },
        {
          "name": "interval",
          "type": "number",
          "default": "0.45",
          "description": "Seconds per word."
        },
        {
          "name": "hold",
          "type": "number",
          "default": "2",
          "description": "Seconds the finished sentence holds before looping."
        },
        {
          "name": "loop",
          "type": "boolean",
          "default": "true",
          "description": "Replay after the hold."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"div\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<WordSequence text=\"Every word gets its moment\" variant=\"stage\" />",
      "whenToUse": [
        "Manifestos and intro sequences.",
        "Title cards for video-like landing pages."
      ],
      "whenNotToUse": [
        "Navigation or interface copy.",
        "Sentences over about ten words."
      ],
      "accessibility": [
        "The whole sentence is read once from a hidden copy.",
        "Every sequence ends on the full sentence, readable at rest."
      ],
      "motion": [
        "Stack eases each word up into place; stage blurs words in and out; cut uses hard cuts with no tween."
      ],
      "reducedMotion": "The sentence appears whole and still.",
      "performance": [
        "One timer; one or a handful of animated spans."
      ],
      "related": [
        "kinetic-beat",
        "word-reveal",
        "read-along"
      ],
      "keywords": [
        "kinetic typography",
        "sequence",
        "title card",
        "storytelling",
        "word by word"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "stack",
            "stage",
            "cut"
          ]
        },
        {
          "prop": "interval",
          "kind": "range",
          "min": 0.15,
          "max": 1.2,
          "step": 0.05
        }
      ],
      "preset": {
        "text": "Every word gets its moment"
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/word-sequence",
      "markdown": "https://textlab.dev/components/word-sequence.md",
      "registry": "https://textlab.dev/r/word-sequence.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/word-sequence.json"
    },
    {
      "name": "read-along",
      "title": "Read along",
      "description": "A highlight follows the reading, word by word.",
      "category": "kinetic",
      "exportName": "ReadAlong",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "highlight",
        "karaoke"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"highlight\" | \"karaoke\"",
          "default": "\"highlight\"",
          "description": "A selection box that hops word to word, or a color fill through each word."
        },
        {
          "name": "wpm",
          "type": "number",
          "default": "220",
          "description": "Reading pace in words per minute."
        },
        {
          "name": "color",
          "type": "string",
          "default": "var(--tl-accent)",
          "description": "Highlight or fill color."
        },
        {
          "name": "loop",
          "type": "boolean",
          "default": "true",
          "description": "Start over after a pause at the end."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"p\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<ReadAlong text=\"Reading is easier when something keeps your place.\" variant=\"karaoke\" />",
      "whenToUse": [
        "Audio and text-to-speech products.",
        "Language learning, lyrics and accessibility demos."
      ],
      "whenNotToUse": [
        "Long articles; the pace is fixed.",
        "As a substitute for real synced captions."
      ],
      "accessibility": [
        "The paragraph is read once from a hidden copy.",
        "The highlight pauses when the paragraph leaves the viewport."
      ],
      "motion": [
        "The selection box glides between words in 200 ms with a strong ease-out, moved and stretched by one transform.",
        "Longer words and punctuation get extra time, like natural reading."
      ],
      "reducedMotion": "Plain paragraph, no highlight.",
      "performance": [
        "One timer and one moving box; word boxes are read once per step."
      ],
      "related": [
        "scroll-fill",
        "caret-morph",
        "marker-highlight"
      ],
      "keywords": [
        "karaoke",
        "read along",
        "highlight",
        "text to speech",
        "captions",
        "follow along"
      ],
      "controls": [
        {
          "prop": "text",
          "kind": "text"
        },
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "highlight",
            "karaoke"
          ]
        },
        {
          "prop": "wpm",
          "kind": "range",
          "min": 80,
          "max": 400,
          "step": 10
        }
      ],
      "preset": {
        "text": "Reading is easier when something gently keeps your place, one word at a time."
      },
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/read-along",
      "markdown": "https://textlab.dev/components/read-along.md",
      "registry": "https://textlab.dev/r/read-along.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/read-along.json"
    },
    {
      "name": "echo-text",
      "title": "Echo text",
      "description": "Every change leaves fading echoes behind.",
      "category": "kinetic",
      "exportName": "EchoText",
      "dependencies": [
        "motion"
      ],
      "variants": [
        "trail",
        "ghost"
      ],
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The copy.",
          "required": true
        },
        {
          "name": "variant",
          "type": "\"trail\" | \"ghost\"",
          "default": "\"trail\"",
          "description": "A row of offset copies that fade in sequence, or a single bloom outward."
        },
        {
          "name": "echoes",
          "type": "number",
          "default": "4",
          "description": "Copies per change, for trail."
        },
        {
          "name": "live",
          "type": "boolean",
          "default": "true",
          "description": "Announce changes politely."
        },
        {
          "name": "as",
          "type": "ElementType",
          "default": "\"span\"",
          "description": "Rendered element."
        },
        {
          "name": "reducedMotion",
          "type": "\"user\" | \"always\" | \"never\"",
          "default": "\"user\"",
          "description": "user follows the OS setting; always shows the still version."
        },
        {
          "name": "className",
          "type": "string",
          "description": "Merged onto the root."
        }
      ],
      "usage": "<EchoText text={status} variant=\"ghost\" />",
      "whenToUse": [
        "Status words, scores and mode labels where the change matters.",
        "Expressive headlines that cycle."
      ],
      "whenNotToUse": [
        "Values that change many times a second; echoes pile up.",
        "Dense UI where trailing copies would overlap neighbours."
      ],
      "accessibility": [
        "The new text is announced politely; echoes are aria-hidden.",
        "Echoes never block pointer events."
      ],
      "motion": [
        "Echoes drift down and right, each a beat behind the last, and fade over about 0.7 s.",
        "Only the last three changes keep echoes, so rapid updates stay clean."
      ],
      "reducedMotion": "The text swaps in place with no echoes.",
      "performance": [
        "A few short-lived motion spans per change, removed when they finish."
      ],
      "related": [
        "text-morph",
        "word-rotator",
        "chromatic-text"
      ],
      "keywords": [
        "echo",
        "ghost",
        "trail",
        "afterimage",
        "text change",
        "transition"
      ],
      "controls": [
        {
          "prop": "variant",
          "kind": "select",
          "options": [
            "trail",
            "ghost"
          ]
        },
        {
          "prop": "echoes",
          "kind": "range",
          "min": 1,
          "max": 8,
          "step": 1
        }
      ],
      "preset": {},
      "added": "2026-10-18",
      "url": "https://textlab.dev/components/echo-text",
      "markdown": "https://textlab.dev/components/echo-text.md",
      "registry": "https://textlab.dev/r/echo-text.json",
      "install": "npx shadcn@latest add https://textlab.dev/r/echo-text.json"
    }
  ]
}
