{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "text-reveal",
  "title": "Text Reveal",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/text-reveal/TextReveal.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface TextRevealProps extends HTMLAttributes<HTMLElement> {\n    text: string\n    as?: string\n    mode?: 'words' | 'chars'\n    /** ms between segment starts */\n    stagger?: number\n    /** ms per segment transition */\n    duration?: number\n    /** ms before the first segment starts */\n    delay?: number\n    blur?: boolean\n    /** reveal only on first intersection; false re-hides when scrolled away */\n    once?: boolean\n    ref?: HTMLElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    text,\n    as: tag = 'span',\n    mode = 'words',\n    stagger = 40,\n    duration = 600,\n    delay = 0,\n    blur = true,\n    once = true,\n    class: className,\n    ref = $bindable(null),\n    ...restProps\n  }: TextRevealProps = $props()\n\n  interface Segment {\n    text: string\n    space: boolean\n  }\n\n  const segments: Segment[] = $derived.by(() => {\n    const source = text.replace(/\\s+/g, ' ').trim()\n    if (mode === 'chars') {\n      return Array.from(source).map((ch) => ({ text: ch, space: ch === ' ' }))\n    }\n    const words = source.split(' ')\n    // Interleave plain spaces so word spacing stays natural.\n    return words.flatMap((word, i) =>\n      i < words.length - 1\n        ? [\n            { text: word, space: false },\n            { text: '', space: true },\n          ]\n        : [{ text: word, space: false }],\n    )\n  })\n\n  let revealed = $state(false)\n\n  function observe(node: HTMLElement) {\n    ref = node\n    if (typeof window === 'undefined') return\n    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches\n    if (reduce || typeof IntersectionObserver === 'undefined') {\n      revealed = true\n      return\n    }\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) {\n            revealed = true\n            if (once) observer.disconnect()\n          } else if (!once) {\n            revealed = false\n          }\n        }\n      },\n      { threshold: 0.2 },\n    )\n    observer.observe(node)\n    return {\n      destroy() {\n        observer.disconnect()\n      },\n    }\n  }\n</script>\n\n<svelte:element\n  this={tag}\n  use:observe\n  data-uipkge=\"\"\n  data-slot=\"text-reveal\"\n  aria-label={text}\n  class={cn('inline-block', revealed && 'is-revealed', className)}\n  {...restProps}\n>\n  {#each segments as seg, i (`${seg.text}-${i}`)}\n    {#if seg.space}\n      <span aria-hidden=\"true\">&nbsp;</span>\n    {:else}\n      <span\n        aria-hidden=\"true\"\n        data-slot=\"text-reveal-segment\"\n        class={cn('text-reveal-seg inline-block will-change-transform', blur && 'text-reveal-blur')}\n        style={`transition-delay: ${delay + i * stagger}ms; transition-duration: ${duration}ms;`}\n        >{seg.text}</span\n      >\n    {/if}\n  {/each}\n</svelte:element>\n\n<style>\n  :global([data-slot='text-reveal'] .text-reveal-seg) {\n    opacity: 0;\n    transform: translateY(0.5em);\n    transition-property: opacity, transform, filter;\n    transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);\n  }\n\n  :global([data-slot='text-reveal'].is-revealed .text-reveal-seg) {\n    opacity: 1;\n    transform: translateY(0);\n  }\n\n  :global([data-slot='text-reveal'] .text-reveal-blur) {\n    filter: blur(8px);\n  }\n\n  :global([data-slot='text-reveal'].is-revealed .text-reveal-blur) {\n    filter: blur(0);\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    :global([data-slot='text-reveal'] .text-reveal-seg) {\n      opacity: 1 !important;\n      transform: none !important;\n      filter: none !important;\n      transition: none !important;\n    }\n  }\n</style>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/text-reveal/TextReveal.svelte"
    },
    {
      "path": "packages/registry-svelte/components/text-reveal/index.ts",
      "content": "export { default as TextReveal, type TextRevealProps } from './TextReveal.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/text-reveal/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Scroll-triggered staggered entrance for text — each word or character fades in, rises and un-blurs with a per-index delay. IntersectionObserver based, `once` toggle for replay-on-scroll, honors prefers-reduced-motion.",
  "categories": [
    "display",
    "motion"
  ]
}