{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-reveal",
  "title": "Search Reveal",
  "description": "Expanding construction circles snap into a search pill, type editable text, and reveal an abstract graphic panel.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/search-reveal/index.tsx",
      "content": "\"use client\";\n\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type SearchRevealProps = {\n  text?: string;\n  fieldWidth?: number;\n  fontSize?: number;\n  framesPerCharacter?: number;\n  showPanel?: boolean;\n  showGuides?: boolean;\n  color?: string;\n  fieldColor?: string;\n  ringColor?: string;\n  panelColor?: string;\n  accentColor?: string;\n  backgroundColor?: string;\n  speed?: number;\n  reducedMotion?: boolean;\n  className?: string;\n};\n\nexport const searchRevealLength = 90;\nconst finite = (value: number | undefined, fallback: number) =>\n  typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst letters = (text: string) =>\n  Array.from(\n    new Intl.Segmenter(\"en\", { granularity: \"grapheme\" }).segment(text),\n    (part) => part.segment,\n  );\nconst curve = Easing.bezier(0.22, 1, 0.36, 1);\nconst tween = (frame: number, start: number, end: number, from = 0, to = 1) =>\n  interpolate(frame, [start, end], [from, to], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: curve,\n  });\n\nfunction timing(props: SearchRevealProps) {\n  const characters = letters(props.text ?? \"remocn\");\n  const step = clamp(finite(props.framesPerCharacter, 4), 1, 12);\n  const end = Math.max(searchRevealLength, 54 + characters.length * step + 10);\n  return { characters, step, end, speed: Math.max(0, finite(props.speed, 1)) };\n}\n\n/** Demo duration at 30 fps, including an 18-frame final hold. */\nexport function getSearchRevealDuration(props: SearchRevealProps = {}) {\n  const { end, speed } = timing(props);\n  return speed === 0 ? 1 : Math.ceil((end + 18) / speed);\n}\n\n/** Frame is expressed at 30 fps; playback speed is applied here. */\nexport function getSearchRevealState(\n  frame: number,\n  props: SearchRevealProps = {},\n) {\n  const { characters, step, end, speed } = timing(props);\n  const t = props.reducedMotion ? end : Math.max(0, finite(frame, 0)) * speed;\n  const typedCount = clamp(Math.floor((t - 54) / step), 0, characters.length);\n  const width = clamp(finite(props.fieldWidth, 464), 320, 640);\n  const reveal = props.showPanel === false ? 0 : tween(t, 56, 90);\n  const fieldWidth = tween(t, 26, 48, 130, width);\n  const fieldHeight = tween(t, 26, 46, 24, 80);\n  const panelWidth =\n    fieldWidth + (Math.max(774, width + 170) - fieldWidth) * reveal;\n  const panelHeight = fieldHeight + (374 - fieldHeight) * reveal;\n  return {\n    t,\n    end,\n    typedCount,\n    text: characters.slice(0, typedCount).join(\"\"),\n    circleRadius: tween(t, 0, 26, 250, 410),\n    circleOffset: tween(t, 0, 26, 62, 185),\n    circlesVisible: t < 26,\n    fieldVisible: t >= 26,\n    fieldWidth,\n    fieldHeight,\n    tilt: tween(t, 26, 47, -4, 0),\n    iconOpacity: tween(t, 34, 46),\n    panelWidth,\n    panelHeight,\n    panelRadius: 40 * (1 - reveal),\n    panelOpacity: tween(t, 56, 60),\n    reveal,\n    caretVisible:\n      t >= 52 &&\n      t < end &&\n      (typedCount < characters.length || Math.floor((t - 52) / 12) % 2 === 0),\n  };\n}\n\nfunction Cross({ x, y, size = 9 }: { x: number; y: number; size?: number }) {\n  return (\n    <path\n      d={`M ${x - size} ${y} H ${x + size} M ${x} ${y - size} V ${y + size}`}\n    />\n  );\n}\n\n/** A video scene: expanding construction circles, a search pill, and typed copy. */\nexport function SearchReveal({\n  text = \"remocn\",\n  fieldWidth = 464,\n  fontSize = 34,\n  framesPerCharacter = 4,\n  showPanel = true,\n  showGuides = true,\n  color = \"#26232b\",\n  fieldColor = \"#ffffff\",\n  ringColor = \"#cbc5cf\",\n  panelColor = \"#a800b7\",\n  accentColor = \"#e8f99a\",\n  backgroundColor = \"#f5f1f5\",\n  speed = 1,\n  reducedMotion = false,\n  className,\n}: SearchRevealProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const state = getSearchRevealState((frame * 30) / fps, {\n    text,\n    fieldWidth,\n    framesPerCharacter,\n    showPanel,\n    speed,\n    reducedMotion,\n  });\n  const scale = Math.min(width / 1280, height / 720);\n  const fittedFont = Math.min(\n    clamp(finite(fontSize, 34), 18, 54),\n    (state.fieldWidth - 120) / Math.max(1, letters(text).length * 0.65),\n  );\n  const markerWidth = showPanel ? state.panelWidth : state.fieldWidth;\n  const markerHeight = showPanel ? state.panelHeight : state.fieldHeight;\n\n  return (\n    <AbsoluteFill\n      className={className}\n      style={{\n        backgroundColor,\n        overflow: \"hidden\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          width: 1280,\n          height: 720,\n          flexShrink: 0,\n          position: \"relative\",\n          transform: `scale(${scale})`,\n        }}\n      >\n        {state.circlesVisible && (\n          <svg\n            aria-hidden=\"true\"\n            width=\"1280\"\n            height=\"720\"\n            viewBox=\"-640 -360 1280 720\"\n            style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n          >\n            <g fill=\"none\" stroke={ringColor} strokeWidth=\"1\">\n              {[-1, 1].map((side) => (\n                <g key={side}>\n                  <circle\n                    cx={side * state.circleOffset}\n                    r={state.circleRadius}\n                  />\n                  {showGuides &&\n                    [-1, 1].map((edge) => (\n                      <Cross\n                        key={edge}\n                        x={\n                          side * state.circleOffset + edge * state.circleRadius\n                        }\n                        y={0}\n                        size={6}\n                      />\n                    ))}\n                </g>\n              ))}\n            </g>\n          </svg>\n        )}\n        {state.fieldVisible && (\n          <div\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: \"50%\",\n              width: 0,\n              height: 0,\n              transform: `rotate(${state.tilt}deg)`,\n            }}\n          >\n            {showPanel && (\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: -state.panelWidth / 2,\n                  top: -state.panelHeight / 2,\n                  width: state.panelWidth,\n                  height: state.panelHeight,\n                  borderRadius: state.panelRadius,\n                  overflow: \"hidden\",\n                  opacity: state.panelOpacity,\n                  backgroundColor: panelColor,\n                }}\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  width=\"844\"\n                  height=\"374\"\n                  viewBox=\"0 0 844 374\"\n                  style={{\n                    position: \"absolute\",\n                    left: \"50%\",\n                    top: \"50%\",\n                    transform: `translate(-50%, -50%) scale(${1.07 - state.reveal * 0.07})`,\n                  }}\n                >\n                  <g\n                    transform={`translate(310 -45) rotate(${-24 + 10 * state.reveal})`}\n                    fill=\"none\"\n                    stroke={fieldColor}\n                  >\n                    {Array.from({ length: 30 }, (_, i) => (\n                      <ellipse\n                        key={i}\n                        rx={82 + i * 3}\n                        ry={118}\n                        transform={`rotate(${i * 6})`}\n                        strokeWidth=\"1.4\"\n                        opacity={0.35 + i / 70}\n                      />\n                    ))}\n                  </g>\n                  <g\n                    transform={`translate(65 233) rotate(${state.reveal * 25})`}\n                    fill={accentColor}\n                  >\n                    {Array.from({ length: 10 }, (_, i) => (\n                      <ellipse\n                        key={i}\n                        cy=\"-41\"\n                        rx=\"19\"\n                        ry=\"48\"\n                        transform={`rotate(${i * 36})`}\n                      />\n                    ))}\n                  </g>\n                  <g\n                    transform={`translate(720 357) rotate(${-18 * state.reveal})`}\n                    fill=\"none\"\n                    stroke={accentColor}\n                    strokeWidth=\"22\"\n                  >\n                    <path d=\"M -68 -115 V 80 M -122 -60 L -14 26 M -125 18 L -12 -61\" />\n                    <circle cx=\"103\" cy=\"-35\" r=\"54\" />\n                  </g>\n                </svg>\n              </div>\n            )}\n            {showGuides && (\n              <svg\n                aria-hidden=\"true\"\n                width=\"1280\"\n                height=\"720\"\n                viewBox=\"-640 -360 1280 720\"\n                style={{\n                  position: \"absolute\",\n                  left: -640,\n                  top: -360,\n                  overflow: \"visible\",\n                }}\n              >\n                <g\n                  fill=\"none\"\n                  stroke={ringColor}\n                  strokeWidth=\"1\"\n                  opacity={0.5 * state.iconOpacity}\n                >\n                  <path\n                    d={`M ${markerWidth / 2} ${-markerHeight / 2 - 14} V -360 M ${-markerWidth / 2} ${markerHeight / 2 + 14} V 360`}\n                  />\n                </g>\n                <g fill=\"none\" stroke={color} strokeWidth=\"1.5\">\n                  <Cross x={-markerWidth / 2} y={markerHeight / 2} />\n                  <Cross x={markerWidth / 2} y={-markerHeight / 2} />\n                </g>\n              </svg>\n            )}\n            <div\n              style={{\n                position: \"absolute\",\n                left: -state.fieldWidth / 2,\n                top: -state.fieldHeight / 2,\n                width: state.fieldWidth,\n                height: state.fieldHeight,\n                borderRadius: 999,\n                backgroundColor: fieldColor,\n                boxShadow: \"0 3px 12px #26232b08, inset 0 0 0 1px #26232b0a\",\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                overflow: \"hidden\",\n                color,\n                fontFamily: \"Arial, Helvetica, sans-serif\",\n                fontSize: fittedFont,\n                fontWeight: 400,\n              }}\n            >\n              <div\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                  maxWidth: state.fieldWidth - 116,\n                  overflow: \"hidden\",\n                  whiteSpace: \"pre\",\n                }}\n              >\n                <span>{state.text}</span>\n                <span\n                  aria-hidden=\"true\"\n                  style={{\n                    width: 1.5,\n                    height: fittedFont * 1.12,\n                    marginLeft: 2,\n                    flexShrink: 0,\n                    backgroundColor: color,\n                    opacity: state.caretVisible ? 1 : 0,\n                  }}\n                />\n              </div>\n              <svg\n                aria-hidden=\"true\"\n                width=\"28\"\n                height=\"28\"\n                viewBox=\"0 0 28 28\"\n                fill=\"none\"\n                stroke={ringColor}\n                strokeWidth=\"1.5\"\n                strokeLinecap=\"round\"\n                style={{\n                  position: \"absolute\",\n                  right: 24,\n                  opacity: state.iconOpacity,\n                }}\n              >\n                <circle cx=\"11.5\" cy=\"11.5\" r=\"7.5\" />\n                <path d=\"m17 17 7 7\" />\n              </svg>\n            </div>\n          </div>\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/search-reveal.tsx"
    }
  ],
  "type": "registry:component"
}
