{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FoldText-TS-CSS",
	"title": "FoldText",
	"description": "Lines unfold into place like creased paper opening flat.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "FoldText.css",
			"target": "@components/FoldText.css",
			"content": ".fold-text {\n  display: inline-block;\n  color: var(--fold-text-color, currentColor);\n  font-size: var(--fold-text-font-size, inherit);\n  font-weight: var(--fold-text-font-weight, inherit);\n  line-height: 0.95;\n  letter-spacing: -0.04em;\n  white-space: pre-wrap;\n  user-select: text;\n}\n\n.fold-text-sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n\n.fold-text-visual {\n  display: inline;\n}\n\n.fold-text-line {\n  display: block;\n}\n\n.fold-text-whitespace {\n  display: inline;\n}\n\n.fold-text-segment {\n  display: inline-block;\n  line-height: inherit;\n  perspective: var(--fold-perspective, 700px);\n  transform-style: preserve-3d;\n  vertical-align: baseline;\n}\n\n.fold-text-segment[data-fold-split='line'] {\n  display: block;\n}\n\n.fold-text-piece {\n  position: relative;\n  display: inline-block;\n  color: inherit;\n  line-height: inherit;\n  transform-style: preserve-3d;\n  backface-visibility: hidden;\n  will-change: transform, opacity;\n}\n\n.fold-text-piece::after {\n  content: '';\n  position: absolute;\n  inset: -0.08em -0.02em;\n  pointer-events: none;\n  opacity: var(--fold-crease, 0);\n  mix-blend-mode: multiply;\n  border-radius: 0.08em;\n}\n\n.fold-text-piece[data-fold-hinge='top']::after {\n  background: linear-gradient(180deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='bottom']::after {\n  background: linear-gradient(0deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='left']::after {\n  background: linear-gradient(90deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n.fold-text-piece[data-fold-hinge='right']::after {\n  background: linear-gradient(270deg, rgba(0, 0, 0, 0.58) 0%, rgba(0, 0, 0, 0.22) 42%, rgba(255, 255, 255, 0.26) 100%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .fold-text-piece {\n    transform: none !important;\n  }\n\n  .fold-text-piece::after {\n    opacity: 0 !important;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "FoldText.tsx",
			"content": "import { useEffect, useMemo, useRef, type CSSProperties, type ReactNode } from 'react';\nimport { gsap } from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\nimport './FoldText.css';\n\ngsap.registerPlugin(ScrollTrigger);\n\ntype SplitBy = 'char' | 'word' | 'line';\ntype Hinge = 'top' | 'bottom' | 'left' | 'right';\ntype Trigger = 'mount' | 'hover' | 'scroll' | 'loop';\n\nexport interface FoldTextProps {\n  text?: string;\n  splitBy?: SplitBy;\n  hinge?: Hinge;\n  duration?: number;\n  stagger?: number;\n  ease?: string;\n  perspective?: number;\n  creaseShading?: number;\n  trigger?: Trigger;\n  fontSize?: string | number;\n  fontWeight?: string | number;\n  color?: string;\n  className?: string;\n  style?: CSSProperties;\n}\n\ntype HingeConfig = {\n  origin: string;\n  rotateX: number;\n  rotateY: number;\n};\n\nconst HINGE_CONFIG: Record<Hinge, HingeConfig> = {\n  top: { origin: '50% 0%', rotateX: -92, rotateY: 0 },\n  bottom: { origin: '50% 100%', rotateX: 92, rotateY: 0 },\n  left: { origin: '0% 50%', rotateX: 0, rotateY: 92 },\n  right: { origin: '100% 50%', rotateX: 0, rotateY: -92 }\n};\n\nconst clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));\n\nconst renderWhitespace = (value: string, key: string): ReactNode[] =>\n  value.split(/(\\n)/).map((part, index) => {\n    if (part === '\\n') return <br key={`${key}-br-${index}`} />;\n    if (!part) return null;\n\n    return (\n      <span className=\"fold-text-whitespace\" key={`${key}-space-${index}`}>\n        {part.replace(/ /g, '\\u00A0')}\n      </span>\n    );\n  });\n\nconst FoldText = ({\n  text = 'Design unfolds',\n  splitBy = 'char',\n  hinge = 'top',\n  duration = 0.65,\n  stagger = 0.045,\n  ease = 'power3.out',\n  perspective = 700,\n  creaseShading = 0.55,\n  trigger = 'mount',\n  fontSize = 80,\n  fontWeight = 800,\n  color = '#f7f2e8',\n  className = '',\n  style = {}\n}: FoldTextProps) => {\n  const rootRef = useRef<HTMLSpanElement | null>(null);\n  const timelineRef = useRef<gsap.core.Timeline | null>(null);\n  const hingeConfig = HINGE_CONFIG[hinge] || HINGE_CONFIG.top;\n  const safeCrease = clamp(creaseShading, 0, 1);\n  const safePerspective = Math.max(120, perspective);\n\n  const segments = useMemo(() => {\n    let segmentIndex = 0;\n\n    const renderSegment = (content: string, key: string, split: SplitBy = splitBy): ReactNode => {\n      segmentIndex += 1;\n      return (\n        <span\n          className=\"fold-text-segment\"\n          data-fold-split={split}\n          key={key}\n          style={{ '--fold-perspective': `${safePerspective}px` } as CSSProperties}\n        >\n          <span\n            className=\"fold-text-piece\"\n            data-fold-hinge={hinge}\n            style={{ transformOrigin: hingeConfig.origin, '--fold-crease': 0 } as CSSProperties}\n          >\n            {content || '\\u00A0'}\n          </span>\n        </span>\n      );\n    };\n\n    if (splitBy === 'line') {\n      return text.split('\\n').map((line, index) => (\n        <span className=\"fold-text-line\" key={`line-${index}`}>\n          {renderSegment(line || '\\u00A0', `segment-line-${index}`, 'line')}\n        </span>\n      ));\n    }\n\n    if (splitBy === 'word') {\n      return text.split(/(\\s+)/).flatMap((part, index) => {\n        if (!part) return [];\n        if (/^\\s+$/.test(part)) return renderWhitespace(part, `ws-${index}`);\n        return renderSegment(part, `segment-word-${segmentIndex}`);\n      });\n    }\n\n    return Array.from(text).map((char, index) => {\n      if (char === '\\n') return <br key={`br-${index}`} />;\n      return renderSegment(char === ' ' ? '\\u00A0' : char, `segment-char-${index}`);\n    });\n  }, [text, splitBy, hinge, hingeConfig.origin, safePerspective]);\n\n  useEffect(() => {\n    if (typeof window === 'undefined') return undefined;\n\n    const root = rootRef.current;\n    if (!root) return undefined;\n\n    const pieces = Array.from(root.querySelectorAll<HTMLElement>('.fold-text-piece'));\n    if (!pieces.length) return undefined;\n\n    const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n    const activeDuration = reduceMotion ? Math.min(duration, 0.22) : duration;\n    const activeStagger = reduceMotion ? Math.min(stagger, 0.02) : stagger;\n    const fromVars = {\n      opacity: 0,\n      rotateX: reduceMotion ? 0 : hingeConfig.rotateX,\n      rotateY: reduceMotion ? 0 : hingeConfig.rotateY,\n      '--fold-crease': reduceMotion ? 0 : safeCrease,\n      transformOrigin: hingeConfig.origin,\n      force3D: true\n    };\n    const toVars = {\n      opacity: 1,\n      rotateX: 0,\n      rotateY: 0,\n      '--fold-crease': 0,\n      duration: activeDuration,\n      ease: reduceMotion ? 'power1.out' : ease,\n      stagger: activeStagger,\n      clearProps: 'willChange'\n    };\n\n    const killTimeline = () => {\n      timelineRef.current?.kill();\n      timelineRef.current = null;\n      gsap.killTweensOf(pieces);\n    };\n\n    const play = (repeat: boolean): gsap.core.Timeline => {\n      killTimeline();\n      timelineRef.current = gsap.timeline({ repeat: repeat ? -1 : 0, repeatDelay: repeat ? 0.75 : 0 });\n      timelineRef.current.fromTo(pieces, fromVars, toVars);\n      return timelineRef.current;\n    };\n\n    let scrollTrigger: ReturnType<typeof ScrollTrigger.create> | undefined;\n    let hoverHandler: (() => void) | undefined;\n\n    if (trigger === 'hover') {\n      gsap.set(pieces, { opacity: 1, rotateX: 0, rotateY: 0, '--fold-crease': 0, transformOrigin: hingeConfig.origin });\n      hoverHandler = () => play(false);\n      root.addEventListener('mouseenter', hoverHandler);\n    } else if (trigger === 'scroll') {\n      gsap.set(pieces, fromVars);\n      scrollTrigger = ScrollTrigger.create({\n        trigger: root,\n        start: 'top 82%',\n        once: true,\n        onEnter: () => play(false)\n      });\n    } else if (trigger === 'loop') {\n      play(true);\n    } else {\n      play(false);\n    }\n\n    return () => {\n      if (hoverHandler) root.removeEventListener('mouseenter', hoverHandler);\n      scrollTrigger?.kill();\n      killTimeline();\n    };\n  }, [\n    text,\n    splitBy,\n    hinge,\n    duration,\n    stagger,\n    ease,\n    perspective,\n    safeCrease,\n    trigger,\n    hingeConfig.origin,\n    hingeConfig.rotateX,\n    hingeConfig.rotateY\n  ]);\n\n  const rootStyle: CSSProperties = {\n    '--fold-text-font-size': typeof fontSize === 'number' ? `${fontSize}px` : fontSize,\n    '--fold-text-font-weight': fontWeight,\n    '--fold-text-color': color,\n    ...style\n  } as CSSProperties;\n\n  return (\n    <span ref={rootRef} className={`fold-text ${className}`.trim()} style={rootStyle}>\n      <span className=\"fold-text-sr-only\">{text}</span>\n      <span className=\"fold-text-visual\" aria-hidden=\"true\">\n        {segments}\n      </span>\n    </span>\n  );\n};\n\nexport default FoldText;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}