{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "ScrollExpand-JS-CSS",
	"title": "ScrollExpand",
	"description": "A rounded media frame that grows to full bleed as it scrolls through the viewport.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "ScrollExpand.css",
			"target": "@components/ScrollExpand.css",
			"content": ".scroll-expand {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.scroll-expand--scroller {\n  overflow-y: auto;\n  overflow-x: hidden;\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n  overscroll-behavior: contain;\n}\n\n.scroll-expand--scroller::-webkit-scrollbar {\n  display: none;\n}\n\n.scroll-expand__track {\n  position: relative;\n  width: 100%;\n}\n\n.scroll-expand__stage {\n  position: sticky;\n  top: 0;\n  width: 100%;\n  overflow: hidden;\n  --se-title-size: 4rem;\n}\n\n.scroll-expand__frame {\n  position: absolute;\n  inset: 0;\n  clip-path: inset(21% 29% 21% 29% round 24px);\n  will-change: clip-path;\n}\n\n.scroll-expand__media {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  will-change: transform;\n  transform-origin: center;\n  user-select: none;\n  -webkit-user-drag: none;\n}\n\n.scroll-expand__scrim {\n  position: absolute;\n  inset: 0;\n  pointer-events: none;\n  background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.1) 45%, rgba(0, 0, 0, 0.35));\n  opacity: 0;\n}\n\n.scroll-expand__overlay {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  padding: 6%;\n  opacity: 0;\n  will-change: opacity, transform;\n}\n\n.scroll-expand__title {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  margin: 0;\n  padding: 0 6%;\n  text-align: center;\n  font-size: var(--se-title-size);\n  font-weight: 700;\n  letter-spacing: -0.03em;\n  line-height: 1;\n  color: #fff;\n  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);\n  pointer-events: none;\n  will-change: opacity, transform;\n}\n\n.scroll-expand__hint {\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 1.25rem;\n  text-align: center;\n  font-size: 0.8125rem;\n  letter-spacing: 0.02em;\n  color: rgba(255, 255, 255, 0.55);\n  pointer-events: none;\n  will-change: opacity, transform;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "ScrollExpand.jsx",
			"content": "import { useCallback, useEffect, useRef } from 'react';\n\nimport './ScrollExpand.css';\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=\"scroll-expand__media\"\n        src={src}\n        poster={poster}\n        autoPlay\n        muted\n        loop\n        playsInline\n      />\n    ) : (\n      <img ref={mediaRef} className=\"scroll-expand__media\" src={src} alt={alt} draggable={false} />\n    );\n\n  return (\n    <div\n      ref={rootRef}\n      className={`scroll-expand ${useWindowScroll ? '' : 'scroll-expand--scroller'} ${className}`.trim()}\n      style={style}\n      {...rest}\n    >\n      <div ref={trackRef} className=\"scroll-expand__track\">\n        <div ref={stageRef} className=\"scroll-expand__stage\">\n          <div ref={frameRef} className=\"scroll-expand__frame\">\n            {media}\n            <div ref={scrimRef} className=\"scroll-expand__scrim\" />\n            {children ? (\n              <div ref={overlayRef} className=\"scroll-expand__overlay\">\n                {children}\n              </div>\n            ) : null}\n          </div>\n          {title ? (\n            <div ref={titleRef} className=\"scroll-expand__title\">\n              {title}\n            </div>\n          ) : null}\n          {scrollHint ? (\n            <div ref={hintRef} className=\"scroll-expand__hint\">\n              {scrollHint}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default ScrollExpand;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}