{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ScrollExpand-JS-TW",
	"title": "ScrollExpand",
	"description": "A rounded media frame that grows to full bleed as it scrolls through the viewport.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "ScrollExpand/ScrollExpand.jsx",
			"content": "'use client';\n\nimport { useCallback, useEffect, useRef } from 'react';\n\nconst clamp = (v, a, b) => (v < a ? a : v > b ? b : v);\n\nconst smoothstep = (edge0, edge1, x) => {\n  const t = clamp((x - edge0) / (edge1 - edge0 || 1e-6), 0, 1);\n  return t * t * (3 - 2 * t);\n};\n\nconst ScrollExpand = ({\n  src = '',\n  mediaType = 'image',\n  poster = '',\n  alt = '',\n  title = '',\n  scrollHint = '',\n  startWidth = 42,\n  startHeight = 58,\n  startRadius = 24,\n  endRadius = 0,\n  mediaZoom = 1.35,\n  scrollDistance = 1.2,\n  holdDistance = 0.35,\n  smoothing = 0.1,\n  overlayScrim = 0.45,\n  useWindowScroll = false,\n  enabled = true,\n  children,\n  className = '',\n  style,\n  ...rest\n}) => {\n  const rootRef = useRef(null);\n  const trackRef = useRef(null);\n  const stageRef = useRef(null);\n  const frameRef = useRef(null);\n  const mediaRef = useRef(null);\n  const titleRef = useRef(null);\n  const overlayRef = useRef(null);\n  const scrimRef = useRef(null);\n  const hintRef = useRef(null);\n\n  const propsRef = useRef({});\n  propsRef.current = {\n    startWidth,\n    startHeight,\n    startRadius,\n    endRadius,\n    mediaZoom,\n    scrollDistance,\n    holdDistance,\n    smoothing,\n    overlayScrim,\n    useWindowScroll,\n    enabled\n  };\n\n  const applyProgress = useCallback(p => {\n    const frame = frameRef.current;\n    const media = mediaRef.current;\n    if (!frame || !media) return;\n    const c = propsRef.current;\n\n    const e = smoothstep(0, 1, p);\n\n    const w = c.startWidth + (100 - c.startWidth) * e;\n    const h = c.startHeight + (100 - c.startHeight) * e;\n    const ix = Math.max(0, (100 - w) / 2);\n    const iy = Math.max(0, (100 - h) / 2);\n    const r = c.startRadius + (c.endRadius - c.startRadius) * e;\n    frame.style.clipPath = `inset(${iy}% ${ix}% ${iy}% ${ix}% round ${r}px)`;\n\n    media.style.transform = `scale(${c.mediaZoom + (1 - c.mediaZoom) * e})`;\n\n    if (scrimRef.current) scrimRef.current.style.opacity = `${c.overlayScrim * e}`;\n\n    if (titleRef.current) {\n      const out = smoothstep(0.4, 0.88, p);\n      titleRef.current.style.opacity = `${1 - out}`;\n      titleRef.current.style.transform = `translate3d(0, ${-28 * out}px, 0) scale(${1 + 0.06 * out})`;\n    }\n\n    if (hintRef.current) {\n      const gone = smoothstep(0, 0.12, p);\n      hintRef.current.style.opacity = `${1 - gone}`;\n      hintRef.current.style.transform = `translate3d(0, ${8 * gone}px, 0)`;\n    }\n\n    if (overlayRef.current) {\n      const inn = smoothstep(0.68, 1, p);\n      overlayRef.current.style.opacity = `${inn}`;\n      overlayRef.current.style.transform = `translate3d(0, ${18 * (1 - inn)}px, 0)`;\n    }\n  }, []);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const track = trackRef.current;\n    const stage = stageRef.current;\n    if (!root || !track || !stage) return;\n\n    const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    let raf = 0;\n    let current = 0;\n    let target = 0;\n    let stageH = 0;\n    let running = false;\n\n    const measure = () => {\n      const c = propsRef.current;\n      stageH = c.useWindowScroll ? window.innerHeight : root.clientHeight;\n      if (stageH <= 0) return;\n      stage.style.height = `${stageH}px`;\n      track.style.height = `${stageH * (1 + Math.max(0, c.scrollDistance) + Math.max(0, c.holdDistance))}px`;\n\n      const w = root.clientWidth || stageH;\n      stage.style.setProperty('--se-title-size', `${clamp(w * 0.075, 20, 84)}px`);\n    };\n\n    const readProgress = () => {\n      const c = propsRef.current;\n      if (!c.enabled) return 1;\n      const span = stageH * Math.max(0.01, c.scrollDistance);\n      if (c.useWindowScroll) {\n        const top = track.getBoundingClientRect().top;\n        return clamp(-top / span, 0, 1);\n      }\n      return clamp(root.scrollTop / span, 0, 1);\n    };\n\n    const tick = () => {\n      const c = propsRef.current;\n      const k = c.smoothing <= 0 ? 1 : 1 - Math.exp(-1 / (60 * c.smoothing));\n      current += (target - current) * k;\n      if (Math.abs(target - current) < 0.0004) {\n        current = target;\n        running = false;\n      }\n      applyProgress(current);\n      raf = running ? requestAnimationFrame(tick) : 0;\n    };\n\n    const kick = () => {\n      if (running) return;\n      running = true;\n      if (!raf) raf = requestAnimationFrame(tick);\n    };\n\n    const onScroll = () => {\n      target = readProgress();\n      if (propsRef.current.smoothing <= 0 || reduceMotion) {\n        current = target;\n        applyProgress(current);\n        return;\n      }\n      kick();\n    };\n\n    const onResize = () => {\n      measure();\n      target = readProgress();\n      current = target;\n      applyProgress(current);\n    };\n\n    measure();\n    target = readProgress();\n    current = target;\n    applyProgress(current);\n\n    const scroller = useWindowScroll ? window : root;\n    scroller.addEventListener('scroll', onScroll, { passive: true });\n    window.addEventListener('resize', onResize);\n    const ro = new ResizeObserver(onResize);\n    ro.observe(root);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      scroller.removeEventListener('scroll', onScroll);\n      window.removeEventListener('resize', onResize);\n      ro.disconnect();\n    };\n  }, [applyProgress, useWindowScroll]);\n\n  const media =\n    mediaType === 'video' ? (\n      <video\n        ref={mediaRef}\n        className=\"absolute inset-0 w-full h-full object-cover origin-center select-none [will-change:transform]\"\n        src={src}\n        poster={poster}\n        autoPlay\n        muted\n        loop\n        playsInline\n      />\n    ) : (\n      <img\n        ref={mediaRef}\n        className=\"absolute inset-0 w-full h-full object-cover origin-center select-none [will-change:transform]\"\n        src={src}\n        alt={alt}\n        draggable={false}\n      />\n    );\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative w-full h-full ${useWindowScroll ? '' : 'overflow-y-auto overflow-x-hidden overscroll-contain [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden'} ${className}`.trim()}\n      style={style}\n      {...rest}\n    >\n      <div ref={trackRef} className=\"relative w-full\">\n        <div ref={stageRef} className=\"sticky top-0 w-full overflow-hidden [--se-title-size:4rem]\">\n          <div\n            ref={frameRef}\n            className=\"absolute inset-0 [clip-path:inset(21%_29%_21%_29%_round_24px)] [will-change:clip-path]\"\n          >\n            {media}\n            <div\n              ref={scrimRef}\n              className=\"absolute inset-0 opacity-0 pointer-events-none bg-[linear-gradient(to_top,rgba(0,0,0,0.75),rgba(0,0,0,0.1)_45%,rgba(0,0,0,0.35))]\"\n            />\n            {children ? (\n              <div\n                ref={overlayRef}\n                className=\"absolute inset-0 flex flex-col items-center justify-center text-center p-[6%] opacity-0 [will-change:opacity,transform]\"\n              >\n                {children}\n              </div>\n            ) : null}\n          </div>\n          {title ? (\n            <div\n              ref={titleRef}\n              className=\"absolute inset-0 flex items-center justify-center m-0 px-[6%] text-center font-bold leading-none tracking-[-0.03em] text-white [font-size:var(--se-title-size)] [text-shadow:0_2px_24px_rgba(0,0,0,0.45)] pointer-events-none [will-change:opacity,transform]\"\n            >\n              {title}\n            </div>\n          ) : null}\n          {scrollHint ? (\n            <div\n              ref={hintRef}\n              className=\"absolute inset-x-0 bottom-5 text-center text-[0.8125rem] tracking-[0.02em] text-white/55 pointer-events-none [will-change:opacity,transform]\"\n            >\n              {scrollHint}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default ScrollExpand;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}