{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-scroll",
  "title": "Phone Scroll",
  "description": "A phone app screen flicks through stops while a headline sits beside it.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/phone-scroll/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Figtree\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  Img,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst { fontFamily } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serifFamily } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PhoneScrollProps {\n  /** Tall screenshot, rendered 393pt wide and scrolled. */\n  src?: string;\n  /** Scroll offsets (screen points) to flick to, one flick per stop. */\n  stops?: number[];\n  /** Headline that slides in beside the phone. Pass \"\" to hide. */\n  label?: string;\n  /** Part of `label` set in the italic serif. */\n  labelAccent?: string;\n  eyebrow?: string;\n  /** Custom screen content (393pt wide), scrolled like the screenshot. */\n  children?: React.ReactNode;\n  accent?: string;\n  speed?: number;\n  theme?: \"light\" | \"dark\";\n  ground?: GshPresetId;\n  groundParams?: Partial<GradientParams>;\n}\n\nconst SW = 393;\nconst SH = 852;\nconst BEZEL = 11;\nconst BW = SW + BEZEL * 2;\nconst BH = SH + BEZEL * 2;\nconst HEADER_H = 196;\n\nconst FLICK_START = 40;\nconst PRESS = 5;\nconst DRAG = 6;\nconst TAU = 10;\nconst SEG = 46;\n\nconst clamp = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst LIGHT = Easing.bezier(0.22, 1, 0.36, 1);\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\n\nfunction scrollAt(f: number, stops: number[], fps: number): number {\n  let base = 0;\n  for (let i = 0; i < stops.length; i++) {\n    const local = f - (FLICK_START + i * SEG) - PRESS;\n    if (local <= 0) return base;\n    const dist = stops[i] - base;\n    const over = Math.sign(dist) * Math.min(26, Math.abs(dist) * 0.035);\n    const v0 = (dist + over) / (TAU + DRAG / 2);\n    const d0 = (v0 * DRAG) / 2;\n    let y: number;\n    if (local < DRAG) {\n      y = base + d0 * (local / DRAG) ** 2;\n    } else {\n      const t = local - DRAG;\n      const momentum =\n        base + d0 + (dist + over - d0) * (1 - Math.exp(-t / TAU));\n      const back = spring({\n        frame: Math.max(0, t - TAU * 1.9),\n        fps,\n        config: CARD,\n      });\n      y = momentum - over * back;\n    }\n    if (i === stops.length - 1 || local < SEG - PRESS) return y;\n    base = stops[i];\n  }\n  return base;\n}\n\ntype Scene = \"coast\" | \"alpine\" | \"desert\" | \"lake\" | \"aurora\";\ntype Look = {\n  sky: [string, string, string];\n  sun: string;\n  haze: string;\n  far: string;\n  mid: string;\n  near: string;\n  fg: \"trees\" | \"dunes\" | \"cliff\";\n  sunY: number;\n  water?: string;\n  snow?: boolean;\n  lights?: boolean;\n};\nconst SCENES: Record<Scene, Look> = {\n  coast: {\n    sky: [\"#5F82B6\", \"#E2AE90\", \"#FBD6A8\"],\n    sun: \"#FFF1D6\",\n    haze: \"#F4C9A2\",\n    far: \"#9C7A8C\",\n    mid: \"#5A465A\",\n    near: \"#1E1820\",\n    fg: \"cliff\",\n    sunY: 0.5,\n    water: \"#46607F\",\n  },\n  alpine: {\n    sky: [\"#2F66AE\", \"#86B1E0\", \"#DAE7F3\"],\n    sun: \"#FFFFFF\",\n    haze: \"#D3E1EF\",\n    far: \"#8DA4C2\",\n    mid: \"#465D7B\",\n    near: \"#111C26\",\n    fg: \"trees\",\n    sunY: 0.06,\n    snow: true,\n    lights: true,\n  },\n  desert: {\n    sky: [\"#4F7DB2\", \"#E3B489\", \"#F5D4A2\"],\n    sun: \"#FFF2D8\",\n    haze: \"#EFC38D\",\n    far: \"#D29E70\",\n    mid: \"#B26A40\",\n    near: \"#6A3720\",\n    fg: \"dunes\",\n    sunY: 0.45,\n  },\n  lake: {\n    sky: [\"#527FA6\", \"#B2CDD2\", \"#ECEFE4\"],\n    sun: \"#FFFBEA\",\n    haze: \"#DBE5DE\",\n    far: \"#7B9995\",\n    mid: \"#3A5A51\",\n    near: \"#0F1E1A\",\n    fg: \"trees\",\n    sunY: 0.28,\n    water: \"#557F88\",\n    lights: true,\n  },\n  aurora: {\n    sky: [\"#040914\", \"#0D1F3A\", \"#223F63\"],\n    sun: \"#E8F7FF\",\n    haze: \"#2A4A6C\",\n    far: \"#28405F\",\n    mid: \"#132238\",\n    near: \"#050A13\",\n    fg: \"trees\",\n    sunY: -1,\n    lights: true,\n  },\n};\n\nfunction hash(n: number) {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nfunction ridge(\n  seed: number,\n  base: number,\n  amp: number,\n  sharp: boolean,\n  w: number,\n  h: number,\n) {\n  const pts: string[] = [];\n  for (let x = -8; x <= w + 8; x += 3) {\n    let v = 0;\n    for (let o = 0, a = 1, fq = 0.011; o < 5; o++, a *= 0.47, fq *= 2.2) {\n      const sn = Math.sin(x * fq + seed * (3.1 + o * 1.7));\n      v += (sharp && o < 2 ? 1 - Math.abs(sn) * 1.7 : sn) * a;\n    }\n    pts.push(`${x},${(base - v * amp).toFixed(1)}`);\n  }\n  return `M-8,${h} L${pts.join(\" L\")} L${w + 8},${h} Z`;\n}\n\nfunction treeline(\n  seed: number,\n  base: number,\n  tall: number,\n  w: number,\n  h: number,\n) {\n  let d = `M-8,${h} L-8,${base} L${w + 8},${base} L${w + 8},${h} Z`;\n  for (let x = -6, i = 0; x < w + 8; i++) {\n    const edge = Math.abs(x / w - 0.5) * 2;\n    const ht = tall * (0.12 + edge ** 1.5 * 0.7 + hash(seed + i * 1.37) * 0.3);\n    const tw = ht * (0.26 + hash(seed + i * 2.11) * 0.12);\n    for (let k = 0; k < 4; k++) {\n      const yb = base - ht * (0.06 + k * 0.19);\n      const wk = tw * (1 - k * 0.22);\n      d += ` M${(x - wk / 2).toFixed(1)},${yb.toFixed(1)} L${x.toFixed(1)},${(yb - ht * 0.5).toFixed(1)} L${(x + wk / 2).toFixed(1)},${yb.toFixed(1)} Z`;\n    }\n    x += 2 + hash(seed + i * 3.7) * tw * 0.8;\n  }\n  return d;\n}\n\nfunction headland(seed: number, w: number, h: number) {\n  const pts: string[] = [];\n  for (let x = -8; x <= w * 0.55; x += 3) {\n    const t = Math.max(0, x) / (w * 0.55);\n    const n =\n      Math.sin(x * 0.09 + seed) * 0.5 + Math.sin(x * 0.23 + seed * 2.3) * 0.3;\n    pts.push(\n      `${x},${(h * 0.66 + t ** 1.8 * h * 0.36 + n * h * 0.02).toFixed(1)}`,\n    );\n  }\n  return `M-8,${h} L${pts.join(\" L\")} L${w * 0.55},${h} Z`;\n}\n\nfunction Scenery({\n  scene,\n  seed,\n  w,\n  h,\n}: {\n  scene: Scene;\n  seed: number;\n  w: number;\n  h: number;\n}) {\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const p = SCENES[scene];\n  const night = p.sunY < 0;\n  const hz = h * (p.water ? 0.6 : 0.66);\n  const sunX = w * (0.28 + hash(seed) * 0.44);\n  const sunY = h * p.sunY;\n  const ns = Math.round(seed * 10);\n  const u = (k: string) => `url(#${id}${k})`;\n  const lit = (\n    k: string,\n    c: string,\n    top: number,\n    bottom: number,\n    snow = false,\n  ) => (\n    <linearGradient\n      id={`${id}${k}`}\n      gradientUnits=\"userSpaceOnUse\"\n      x1=\"0\"\n      y1={top}\n      x2=\"0\"\n      y2={bottom}\n    >\n      {snow ? (\n        <stop offset=\"0\" stopColor=\"#F4F7FB\" />\n      ) : (\n        <stop\n          offset=\"0\"\n          stopColor={`color-mix(in srgb, ${c} 70%, ${p.haze})`}\n        />\n      )}\n      {snow ? <stop offset=\"0.3\" stopColor=\"#D9E2EC\" /> : null}\n      <stop offset={snow ? \"0.42\" : \"0.55\"} stopColor={c} />\n      <stop offset=\"1\" stopColor={`color-mix(in srgb, ${c} 80%, #000)`} />\n    </linearGradient>\n  );\n  const peaks = scene === \"alpine\" || scene === \"aurora\";\n  const farD = ridge(\n    seed + 9,\n    hz - 26,\n    h * (scene === \"alpine\" ? 0.2 : peaks ? 0.13 : 0.07),\n    peaks,\n    w,\n    h,\n  );\n  const midD = ridge(\n    seed,\n    hz - 2,\n    h * (scene === \"alpine\" ? 0.16 : peaks ? 0.1 : 0.06),\n    peaks,\n    w,\n    h,\n  );\n  const scenery = (\n    <>\n      <path d={farD} fill={u(\"lf\")} filter={u(\"t\")} opacity={0.92} />\n      <rect y={hz - 60} width={w} height={70} fill={u(\"hz\")} />\n      <path d={midD} fill={u(\"lm\")} filter={u(\"t\")} />\n      <rect y={hz - 18} width={w} height={34} fill={u(\"hz\")} opacity={0.6} />\n    </>\n  );\n  return (\n    <svg\n      width={w}\n      height={h}\n      viewBox={`0 0 ${w} ${h}`}\n      style={{ display: \"block\" }}\n    >\n      <defs>\n        <linearGradient id={`${id}s`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor={p.sky[0]} />\n          <stop offset=\"0.45\" stopColor={p.sky[1]} />\n          <stop offset={p.water ? \"0.6\" : \"0.66\"} stopColor={p.sky[2]} />\n        </linearGradient>\n        <radialGradient id={`${id}b`}>\n          <stop offset=\"0\" stopColor={p.sun} stopOpacity=\"0.9\" />\n          <stop offset=\"0.12\" stopColor={p.sun} stopOpacity=\"0.5\" />\n          <stop offset=\"0.45\" stopColor={p.haze} stopOpacity=\"0.18\" />\n          <stop offset=\"1\" stopColor={p.haze} stopOpacity=\"0\" />\n        </radialGradient>\n        <linearGradient id={`${id}hz`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor={p.haze} stopOpacity=\"0\" />\n          <stop\n            offset=\"0.65\"\n            stopColor={p.haze}\n            stopOpacity={night ? 0.25 : 0.6}\n          />\n          <stop offset=\"1\" stopColor={p.haze} stopOpacity=\"0\" />\n        </linearGradient>\n        {lit(\"lf\", p.far, hz - 26 - h * 0.15, hz + 10, p.snow)}\n        {lit(\"lm\", p.mid, hz - 2 - h * 0.17, hz + 40, p.snow)}\n        <linearGradient id={`${id}fg`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n          <stop\n            offset=\"0\"\n            stopColor={\n              scene === \"desert\"\n                ? `color-mix(in srgb, ${p.near} 55%, ${p.haze})`\n                : p.near\n            }\n          />\n          <stop offset=\"1\" stopColor={p.near} />\n        </linearGradient>\n        <radialGradient id={`${id}v`} cx=\"0.5\" cy=\"0.45\" r=\"0.75\">\n          <stop offset=\"0.55\" stopColor=\"#000\" stopOpacity=\"0\" />\n          <stop offset=\"1\" stopColor=\"#000\" stopOpacity=\"0.38\" />\n        </radialGradient>\n        <filter id={`${id}t`} x=\"-2%\" y=\"-6%\" width=\"104%\" height=\"112%\">\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.035 0.1\"\n            numOctaves=\"4\"\n            seed={ns}\n            result=\"n\"\n          />\n          <feDisplacementMap\n            in=\"SourceGraphic\"\n            in2=\"n\"\n            scale=\"6\"\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n            result=\"d\"\n          />\n          <feColorMatrix\n            in=\"n\"\n            type=\"matrix\"\n            values=\"0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 -1.5 0.85\"\n            result=\"s\"\n          />\n          <feComposite in=\"s\" in2=\"d\" operator=\"in\" result=\"sd\" />\n          <feComposite in=\"sd\" in2=\"d\" operator=\"over\" />\n        </filter>\n        <filter id={`${id}r`} x=\"0\" y=\"0\" width=\"100%\" height=\"100%\">\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.006 0.12\"\n            numOctaves=\"2\"\n            seed={ns + 3}\n            result=\"n\"\n          />\n          <feDisplacementMap\n            in=\"SourceGraphic\"\n            in2=\"n\"\n            scale=\"10\"\n            xChannelSelector=\"R\"\n            yChannelSelector=\"G\"\n          />\n          <feGaussianBlur stdDeviation=\"0.6\" />\n        </filter>\n        <filter id={`${id}g`} x=\"0\" y=\"0\" width=\"100%\" height=\"100%\">\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.9\"\n            numOctaves=\"2\"\n            seed={ns + 7}\n          />\n          <feColorMatrix type=\"saturate\" values=\"0\" />\n        </filter>\n        <filter id={`${id}f`}>\n          <feGaussianBlur stdDeviation=\"0.8\" />\n        </filter>\n        <filter id={`${id}glow`} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n          <feGaussianBlur stdDeviation=\"2.4\" />\n        </filter>\n        <linearGradient id={`${id}wd`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor={p.water} stopOpacity=\"0.1\" />\n          <stop\n            offset=\"1\"\n            stopColor={`color-mix(in srgb, ${p.water} 60%, #000)`}\n            stopOpacity=\"0.75\"\n          />\n        </linearGradient>\n        <clipPath id={`${id}wc`}>\n          <rect y={hz} width={w} height={h - hz} />\n        </clipPath>\n      </defs>\n      <rect width={w} height={h} fill={u(\"s\")} />\n      {night\n        ? Array.from({ length: 70 }, (_, i) => (\n            <circle\n              key={i}\n              cx={hash(seed + i * 1.3) * w}\n              cy={hash(seed + i * 2.9) * hz * 0.85}\n              r={0.35 + hash(seed + i * 5.1) * 0.75}\n              fill=\"#fff\"\n              opacity={0.35 + hash(seed + i * 7.3) * 0.6}\n            />\n          ))\n        : null}\n      {night ? (\n        <g style={{ filter: \"blur(10px)\" }} opacity={0.75}>\n          <path\n            d={`M-30,${h * 0.36} C${w * 0.25},${h * 0.12} ${w * 0.55},${h * 0.46} ${w + 30},${h * 0.2}`}\n            stroke=\"#4FF0B0\"\n            strokeWidth={26}\n            fill=\"none\"\n            opacity={0.7}\n          />\n          <path\n            d={`M-30,${h * 0.28} C${w * 0.3},${h * 0.06} ${w * 0.6},${h * 0.36} ${w + 30},${h * 0.1}`}\n            stroke=\"#3BC8B8\"\n            strokeWidth={46}\n            fill=\"none\"\n            opacity={0.35}\n          />\n          <path\n            d={`M${w * 0.2},${h * 0.1} C${w * 0.45},${h * 0.02} ${w * 0.7},${h * 0.2} ${w + 30},${h * 0.04}`}\n            stroke=\"#B07CF0\"\n            strokeWidth={30}\n            fill=\"none\"\n            opacity={0.25}\n          />\n        </g>\n      ) : (\n        <>\n          <circle\n            cx={sunX}\n            cy={sunY}\n            r={h * (0.2 + p.sunY * 0.7)}\n            fill={u(\"b\")}\n          />\n          <circle\n            cx={sunX}\n            cy={sunY}\n            r={h * 0.032}\n            fill={p.sun}\n            style={{ filter: \"blur(1.2px)\" }}\n          />\n        </>\n      )}\n      {scenery}\n      {p.water ? (\n        <g clipPath={u(\"wc\")}>\n          <rect y={hz} width={w} height={h - hz} fill={p.water} />\n          <g\n            transform={`translate(0 ${2 * hz}) scale(1 -1)`}\n            opacity={0.85}\n            filter={u(\"r\")}\n          >\n            <rect width={w} height={hz} fill={u(\"s\")} />\n            {night ? null : (\n              <circle cx={sunX} cy={sunY} r={h * 0.3} fill={u(\"b\")} />\n            )}\n            {scenery}\n          </g>\n          <rect y={hz} width={w} height={h - hz} fill={u(\"wd\")} />\n          {night\n            ? null\n            : Array.from({ length: 14 }, (_, i) => (\n                <rect\n                  key={i}\n                  x={sunX - (6 + i * 1.6) * hash(seed + i * 4.1) - 4}\n                  y={hz + 3 + i * i * 0.55}\n                  width={(8 + i * 2.2) * (0.5 + hash(seed + i * 6.7))}\n                  height={1 + i * 0.08}\n                  rx={0.6}\n                  fill={p.sun}\n                  opacity={0.75 - i * 0.045}\n                />\n              ))}\n        </g>\n      ) : null}\n      <g filter={u(\"f\")}>\n        {p.fg === \"trees\" ? (\n          <path\n            d={treeline(seed + 4, h * 0.97, h * 0.3, w, h)}\n            fill={p.near}\n            filter={u(\"t\")}\n          />\n        ) : p.fg === \"dunes\" ? (\n          <path\n            d={ridge(seed + 5, h * 0.9, h * 0.07, false, w, h)}\n            fill={u(\"fg\")}\n            filter={u(\"t\")}\n          />\n        ) : (\n          <path d={headland(seed + 5, w, h)} fill={p.near} filter={u(\"t\")} />\n        )}\n      </g>\n      {p.lights\n        ? [0, 1, 2].map((i) => {\n            const lx = w * (0.58 + hash(seed + 11) * 0.2) + i * 6;\n            const ly = h * 0.9 - (i % 2) * 3;\n            return (\n              <g key={i}>\n                <rect\n                  x={lx - 3}\n                  y={ly - 3}\n                  width={8}\n                  height={7}\n                  fill=\"#FFB35C\"\n                  opacity={0.7}\n                  filter={u(\"glow\")}\n                />\n                <rect x={lx} y={ly} width={2.2} height={2.6} fill=\"#FFD9A0\" />\n              </g>\n            );\n          })\n        : null}\n      <rect width={w} height={h} fill={u(\"v\")} />\n      <rect\n        width={w}\n        height={h}\n        filter={u(\"g\")}\n        opacity={0.22}\n        style={{ mixBlendMode: \"overlay\" }}\n      />\n    </svg>\n  );\n}\n\nconst LISTINGS: {\n  title: string;\n  meta: string;\n  dates: string;\n  price: number;\n  rating: string;\n  scene: Scene;\n}[] = [\n  {\n    title: \"Halcyon Cove, Portugal\",\n    meta: \"Ocean views · 2 beds\",\n    dates: \"Nov 12 – 17\",\n    price: 218,\n    rating: \"4.96\",\n    scene: \"coast\",\n  },\n  {\n    title: \"Fernway Lodge, Norway\",\n    meta: \"Mountain views · 3 beds\",\n    dates: \"Dec 2 – 7\",\n    price: 304,\n    rating: \"4.92\",\n    scene: \"alpine\",\n  },\n  {\n    title: \"Quarry House, Morocco\",\n    meta: \"Desert stay · 1 bed\",\n    dates: \"Jan 8 – 13\",\n    price: 142,\n    rating: \"4.88\",\n    scene: \"desert\",\n  },\n  {\n    title: \"Tessel Cabin, Canada\",\n    meta: \"Lakefront · 2 beds\",\n    dates: \"Nov 20 – 25\",\n    price: 189,\n    rating: \"4.97\",\n    scene: \"lake\",\n  },\n  {\n    title: \"Lumen Dome, Iceland\",\n    meta: \"Northern lights · 1 bed\",\n    dates: \"Feb 3 – 8\",\n    price: 276,\n    rating: \"4.99\",\n    scene: \"aurora\",\n  },\n];\n\ninterface AppColors {\n  bg: string;\n  ink: string;\n  muted: string;\n  hair: string;\n  pill: string;\n  pillShadow: string;\n  bar: string;\n  dark: boolean;\n}\n\nfunction appColors(dark: boolean): AppColors {\n  return dark\n    ? {\n        bg: \"#121214\",\n        ink: \"#F2F2F3\",\n        muted: \"#9C9CA3\",\n        hair: \"rgba(255,255,255,0.1)\",\n        pill: \"#1D1D20\",\n        pillShadow:\n          \"0 0 0 0.5px rgba(255,255,255,0.12), 0 4px 14px rgba(0,0,0,0.4)\",\n        bar: \"rgba(18,18,20,0.96)\",\n        dark,\n      }\n    : {\n        bg: \"#FFFFFF\",\n        ink: \"#222222\",\n        muted: \"#6A6A6A\",\n        hair: \"#EBEBEB\",\n        pill: \"#FFFFFF\",\n        pillShadow: \"0 0 0 0.5px rgba(0,0,0,0.08), 0 3px 12px rgba(0,0,0,0.1)\",\n        bar: \"rgba(255,255,255,0.97)\",\n        dark,\n      };\n}\n\nfunction Icon({\n  d,\n  size = 24,\n  color,\n}: {\n  d: string;\n  size?: number;\n  color: string;\n}) {\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    >\n      <path d={d} />\n    </svg>\n  );\n}\n\nconst ICONS = {\n  search: \"M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4\",\n  heart: \"M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10z\",\n  trips: \"M4 8h16v11H4zM9 8V5h6v3\",\n  inbox: \"M4 5h16v11H9l-5 4z\",\n  user: \"M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21c1-4 4.5-6 8-6s7 2 8 6\",\n  filter: \"M4 7h10M18 7h2M4 17h4M12 17h8M16 5v4M10 15v4\",\n  cabin: \"M3 20h18M5 20V11l7-6 7 6v9M10 20v-5h4v5\",\n  wave: \"M3 15c2-2 4-2 6 0s4 2 6 0 4-2 6 0M3 10c2-2 4-2 6 0s4 2 6 0 4-2 6 0\",\n  peak: \"M3 20l6-11 4 6 2-3 6 8z\",\n  sun: \"M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M5 19l1.5-1.5M17.5 6.5L19 5\",\n  lake: \"M3 17c3 2 15 2 18 0M6 13l3-5 3 5M12 13l3-6 3 6\",\n  tag: \"M3.5 12.5V4.5h8l9 9-8 8zM8 8.5h.01\",\n};\n\nfunction StatusBar({ color }: { color: string }) {\n  return (\n    <div\n      style={{\n        height: 54,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"space-between\",\n        padding: \"6px 34px 0 50px\",\n      }}\n    >\n      <span\n        style={{\n          fontSize: 17,\n          fontWeight: 400,\n          color,\n          letterSpacing: -0.2,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        9:41\n      </span>\n      <div style={{ display: \"flex\", gap: 6, alignItems: \"center\" }}>\n        <svg width=\"18\" height=\"12\" viewBox=\"0 0 18 12\">\n          {[0, 1, 2, 3].map((i) => (\n            <rect\n              key={i}\n              x={i * 4.6}\n              y={9 - i * 3}\n              width={3.2}\n              height={3 + i * 3}\n              rx={0.8}\n              fill={color}\n            />\n          ))}\n        </svg>\n        <div\n          style={{\n            width: 25,\n            height: 12,\n            borderRadius: 3.5,\n            boxShadow: `inset 0 0 0 1px ${color}`,\n            padding: 1.5,\n            boxSizing: \"border-box\",\n            opacity: 0.9,\n          }}\n        >\n          <div\n            style={{\n              width: \"78%\",\n              height: \"100%\",\n              borderRadius: 2,\n              background: color,\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction ListingCard({\n  l,\n  i,\n  c,\n}: {\n  l: (typeof LISTINGS)[number];\n  i: number;\n  c: AppColors;\n}) {\n  return (\n    <div style={{ marginBottom: 30 }}>\n      <div\n        style={{\n          position: \"relative\",\n          borderRadius: 14,\n          overflow: \"hidden\",\n          height: 322,\n          outline: `1px solid ${c.dark ? \"rgba(255,255,255,0.1)\" : \"rgba(0,0,0,0.1)\"}`,\n          outlineOffset: -1,\n        }}\n      >\n        <Scenery scene={l.scene} seed={i * 1.7 + 0.4} w={345} h={322} />\n        {i === 0 ? (\n          <div\n            style={{\n              position: \"absolute\",\n              top: 14,\n              left: 14,\n              background: \"rgba(255,255,255,0.95)\",\n              borderRadius: 999,\n              padding: \"6px 11px\",\n              fontSize: 13,\n              fontWeight: 400,\n              color: \"#222\",\n            }}\n          >\n            Guest favourite\n          </div>\n        ) : null}\n        <div style={{ position: \"absolute\", top: 14, right: 14 }}>\n          <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\">\n            <path\n              d={ICONS.heart}\n              fill=\"rgba(0,0,0,0.3)\"\n              stroke=\"#fff\"\n              strokeWidth={1.5}\n            />\n          </svg>\n        </div>\n        <div\n          style={{\n            position: \"absolute\",\n            bottom: 12,\n            left: 0,\n            right: 0,\n            display: \"flex\",\n            justifyContent: \"center\",\n            gap: 5,\n          }}\n        >\n          {[0, 1, 2, 3, 4].map((d) => (\n            <div\n              key={d}\n              style={{\n                width: 6,\n                height: 6,\n                borderRadius: 3,\n                background: \"#fff\",\n                opacity: d === 0 ? 1 : 0.55,\n              }}\n            />\n          ))}\n        </div>\n      </div>\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          marginTop: 12,\n          fontSize: 15.5,\n          color: c.ink,\n        }}\n      >\n        <span style={{ fontWeight: 400 }}>{l.title}</span>\n        <span\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: 4,\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" aria-hidden>\n            <path\n              d=\"M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6-4.9-4.6 6.6-.8z\"\n              fill={c.ink}\n            />\n          </svg>\n          {l.rating}\n        </span>\n      </div>\n      <div style={{ fontSize: 15, color: c.muted, marginTop: 2 }}>{l.meta}</div>\n      <div style={{ fontSize: 15, color: c.muted, marginTop: 2 }}>\n        {l.dates}\n      </div>\n      <div\n        style={{\n          fontSize: 15,\n          color: c.muted,\n          marginTop: 6,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        <span style={{ color: c.ink, textDecoration: \"underline\" }}>\n          ${l.price}\n        </span>{\" \"}\n        night\n      </div>\n    </div>\n  );\n}\n\nfunction FeesNotice({ c }: { c: AppColors }) {\n  return (\n    <div\n      style={{\n        marginBottom: 30,\n        padding: \"14px 0\",\n        borderTop: `1px solid ${c.hair}`,\n        borderBottom: `1px solid ${c.hair}`,\n        display: \"flex\",\n        gap: 12,\n        alignItems: \"center\",\n        fontSize: 15,\n        color: c.ink,\n      }}\n    >\n      <Icon d={ICONS.tag} size={22} color={c.ink} />\n      <span>\n        Prices include all fees\n        <span style={{ color: c.muted }}> · 1,240 stays this week</span>\n      </span>\n    </div>\n  );\n}\n\nfunction AppHeader({ scrolled, c }: { scrolled: number; c: AppColors }) {\n  const cats: [keyof typeof ICONS, string][] = [\n    [\"cabin\", \"Cabins\"],\n    [\"wave\", \"Beachfront\"],\n    [\"peak\", \"Peaks\"],\n    [\"sun\", \"Desert\"],\n    [\"lake\", \"Lakes\"],\n  ];\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        right: 0,\n        height: HEADER_H,\n        background: c.bg,\n        zIndex: 2,\n        boxShadow: `0 1px 0 ${c.hair}, 0 6px 14px rgba(0,0,0,${scrolled * (c.dark ? 0.3 : 0.06)})`,\n      }}\n    >\n      <StatusBar color={c.ink} />\n      <div\n        style={{\n          margin: \"8px 24px 0\",\n          height: 58,\n          borderRadius: 29,\n          background: c.pill,\n          boxShadow: c.pillShadow,\n          display: \"flex\",\n          alignItems: \"center\",\n          padding: \"0 10px 0 20px\",\n          gap: 14,\n        }}\n      >\n        <Icon d={ICONS.search} size={20} color={c.ink} />\n        <div style={{ flex: 1 }}>\n          <div style={{ fontSize: 15, fontWeight: 400, color: c.ink }}>\n            Where to?\n          </div>\n          <div style={{ fontSize: 12.5, color: c.muted, marginTop: 1 }}>\n            Anywhere · Any week · Add guests\n          </div>\n        </div>\n        <div\n          style={{\n            width: 38,\n            height: 38,\n            borderRadius: 19,\n            boxShadow: `inset 0 0 0 1px ${c.hair}`,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <Icon d={ICONS.filter} size={17} color={c.ink} />\n        </div>\n      </div>\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          padding: \"16px 26px 0\",\n        }}\n      >\n        {cats.map(([ic, name], i) => (\n          <div\n            key={name}\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              gap: 5,\n              opacity: i === 0 ? 1 : 0.6,\n              paddingBottom: 9,\n              borderBottom: `2px solid ${i === 0 ? c.ink : \"transparent\"}`,\n            }}\n          >\n            <Icon d={ICONS[ic]} size={24} color={c.ink} />\n            <span style={{ fontSize: 12, fontWeight: 400, color: c.ink }}>\n              {name}\n            </span>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction TabBar({ accent, c }: { accent: string; c: AppColors }) {\n  const tabs: [keyof typeof ICONS, string][] = [\n    [\"search\", \"Explore\"],\n    [\"heart\", \"Wishlists\"],\n    [\"trips\", \"Trips\"],\n    [\"inbox\", \"Inbox\"],\n    [\"user\", \"Profile\"],\n  ];\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        bottom: 0,\n        left: 0,\n        right: 0,\n        height: 86,\n        background: c.bar,\n        borderTop: `1px solid ${c.hair}`,\n        zIndex: 2,\n        display: \"flex\",\n        justifyContent: \"space-around\",\n        paddingTop: 9,\n      }}\n    >\n      {tabs.map(([ic, name], i) => (\n        <div\n          key={name}\n          style={{\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            gap: 4,\n            width: 64,\n          }}\n        >\n          <Icon d={ICONS[ic]} size={24} color={i === 0 ? accent : c.muted} />\n          <span\n            style={{\n              fontSize: 11,\n              fontWeight: 400,\n              color: i === 0 ? c.ink : c.muted,\n            }}\n          >\n            {name}\n          </span>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction BuiltInFeed({ c }: { c: AppColors }) {\n  return (\n    <div style={{ padding: `${HEADER_H + 22}px 24px 120px`, background: c.bg }}>\n      {LISTINGS.map((l, i) => (\n        <div key={l.title}>\n          <ListingCard l={l} i={i} c={c} />\n          {i === 1 ? <FeesNotice c={c} /> : null}\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction PhoneBody({\n  children,\n  c,\n}: {\n  children: React.ReactNode;\n  c: AppColors;\n}) {\n  const btn = (side: \"left\" | \"right\", top: number, h: number) => (\n    <div\n      style={{\n        position: \"absolute\",\n        [side]: -3,\n        top,\n        width: 4,\n        height: h,\n        borderRadius: 2,\n        background: c.dark ? \"#2C2C30\" : \"#B8B8BE\",\n      }}\n    />\n  );\n  return (\n    <div style={{ position: \"relative\", width: BW, height: BH }}>\n      {btn(\"left\", 150, 32)}\n      {btn(\"left\", 210, 62)}\n      {btn(\"left\", 284, 62)}\n      {btn(\"right\", 236, 98)}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          borderRadius: 68,\n          background: c.dark ? \"#0B0B0C\" : \"#E6E6EA\",\n          boxShadow: c.dark\n            ? \"inset 0 0 0 2px #3A3A3E, inset 0 0 0 3.5px #18181A\"\n            : \"inset 0 0 0 2px #C9C9CE, inset 0 0 0 3.5px #F6F6F8\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: BEZEL,\n          top: BEZEL,\n          width: SW,\n          height: SH,\n          borderRadius: 56,\n          overflow: \"hidden\",\n          background: c.bg,\n        }}\n      >\n        {children}\n        <div\n          style={{\n            position: \"absolute\",\n            top: 11,\n            left: (SW - 126) / 2,\n            width: 126,\n            height: 37,\n            borderRadius: 19,\n            background: \"#000\",\n            zIndex: 5,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            bottom: 8,\n            left: (SW - 136) / 2,\n            width: 136,\n            height: 5,\n            borderRadius: 3,\n            background: c.ink,\n            zIndex: 5,\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport function PhoneScroll({\n  src,\n  stops = [930],\n  label = \"Find your next stay in two taps.\",\n  labelAccent = \"two taps.\",\n  eyebrow = \"remocn for iOS\",\n  children,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"light\",\n  ground,\n  groundParams,\n}: PhoneScrollProps) {\n  const frame = useCurrentFrame();\n  const { fps, width, height } = useVideoConfig();\n  const f = frame * speed;\n  const blurId = `${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}mb`;\n  const dark = theme === \"dark\";\n  const c = appColors(dark);\n  const tall = height > width;\n\n  const hasLabel = Boolean(label);\n  const k = tall ? (height * 0.6) / BH : (height * 0.9) / BH;\n  const phoneCx = tall || !hasLabel ? width / 2 : width * 0.33;\n  const phoneCy = tall && hasLabel ? height * 0.63 : height / 2;\n\n  const rise = spring({ frame: f, fps, config: HEAVY });\n  const level = spring({ frame: f - 6, fps, config: HEAVY });\n  const ty = (1 - rise) * height * 0.95;\n  const rot = -8 * (1 - level);\n\n  const y = scrollAt(f, stops, fps);\n  const v = Math.abs(y - scrollAt(f - speed, stops, fps));\n  const blur = Math.min(9, v * 0.09);\n\n  const flick = Math.max(\n    0,\n    Math.min(stops.length - 1, Math.floor((f - FLICK_START + 4) / SEG)),\n  );\n  const fl = f - (FLICK_START + flick * SEG);\n  const prev = flick === 0 ? 0 : stops[flick - 1];\n  const dist = stops[flick] - prev;\n  const d0 =\n    (((dist + Math.sign(dist) * Math.min(26, Math.abs(dist) * 0.035)) /\n      (TAU + DRAG / 2)) *\n      DRAG) /\n    2;\n  const dragT = interpolate(fl, [PRESS, PRESS + DRAG], [0, 1], clamp);\n  const dotY = SH * 0.74 - d0 * 0.55 * dragT * dragT;\n  const dotIn = interpolate(fl, [0, 4], [0, 1], clamp);\n  const dotOut = interpolate(\n    fl,\n    [PRESS + DRAG, PRESS + DRAG + 6],\n    [1, 0],\n    clamp,\n  );\n  const press = spring({ frame: fl, fps, config: CARD });\n  const dotScale =\n    interpolate(press, [0, 1], [1.3, 0.96]) *\n    interpolate(fl, [PRESS + DRAG, PRESS + DRAG + 6], [1, 1.25], clamp);\n\n  const typeIn = (d: number) =>\n    interpolate(f, [16 + d, 32 + d], [0, 1], { ...clamp, easing: ENTER });\n  const eyeIn = interpolate(f, [12, 26], [0, 1], { ...clamp, easing: LIGHT });\n  const push = interpolate(f, [0, 120], [1, 1.03], clamp);\n\n  const content = children ? (\n    <div style={{ width: SW }}>{children}</div>\n  ) : src ? (\n    <Img src={src} style={{ width: SW, display: \"block\" }} />\n  ) : (\n    <BuiltInFeed c={c} />\n  );\n\n  const split =\n    labelAccent && label.endsWith(labelAccent)\n      ? [label.slice(0, label.length - labelAccent.length), labelAccent]\n      : [label, \"\"];\n  const ink = dark ? \"#F4F2EE\" : \"#16161A\";\n  const sub = dark ? \"rgba(244,242,238,0.58)\" : \"rgba(22,22,26,0.55)\";\n  const headSize = tall ? width * 0.105 : height * 0.088;\n\n  return (\n    <AbsoluteFill\n      style={{\n        fontFamily,\n        overflow: \"hidden\",\n        WebkitFontSmoothing: \"antialiased\",\n      }}\n    >\n      <GshGround\n        preset={ground ?? (dark ? \"hearth\" : \"paper-crease\")}\n        params={groundParams}\n        timeOffset={dark ? 2 : 4}\n        timeScale={0.3}\n      />\n      <AbsoluteFill\n        style={{\n          background: dark ? \"rgba(4,3,3,0.34)\" : \"rgba(246,244,240,0.28)\",\n        }}\n      />\n      <svg width=\"0\" height=\"0\" style={{ position: \"absolute\" }}>\n        <filter id={blurId} x=\"0\" y=\"-10%\" width=\"100%\" height=\"120%\">\n          <feGaussianBlur stdDeviation={`0 ${blur.toFixed(2)}`} />\n        </filter>\n      </svg>\n      <AbsoluteFill\n        style={{ transform: `scale(${push})`, transformOrigin: \"50% 50%\" }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: phoneCx - BW / 2,\n            top: phoneCy - BH / 2,\n            width: BW,\n            height: BH,\n            transform: `translateY(${ty}px) scale(${k}) rotate(${rot}deg)`,\n            transformOrigin: \"50% 50%\",\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              borderRadius: 68,\n              boxShadow: dark\n                ? \"0 2px 6px rgba(0,0,0,0.5), 0 30px 80px -20px rgba(0,0,0,0.7)\"\n                : \"0 2px 6px rgba(40,32,24,0.12), 0 30px 80px -20px rgba(40,32,24,0.3)\",\n            }}\n          />\n          <PhoneBody c={c}>\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: SW,\n                transform: `translateY(${-y}px)`,\n                filter: blur > 0.4 ? `url(#${blurId})` : undefined,\n              }}\n            >\n              {content}\n            </div>\n            {!children && !src ? (\n              <>\n                <AppHeader\n                  scrolled={interpolate(y, [0, 60], [0, 1], clamp)}\n                  c={c}\n                />\n                <TabBar accent={accent} c={c} />\n              </>\n            ) : null}\n            <div\n              style={{\n                position: \"absolute\",\n                left: SW * 0.56 - 21,\n                top: dotY - 21,\n                width: 42,\n                height: 42,\n                borderRadius: 21,\n                background: \"rgba(30,30,36,0.22)\",\n                boxShadow:\n                  \"inset 0 0 0 1.5px rgba(255,255,255,0.9), 0 2px 8px rgba(0,0,0,0.2)\",\n                opacity: f >= FLICK_START ? dotIn * dotOut : 0,\n                transform: `scale(${dotScale})`,\n                zIndex: 6,\n              }}\n            />\n          </PhoneBody>\n        </div>\n        {hasLabel ? (\n          <div\n            style={{\n              position: \"absolute\",\n              ...(tall\n                ? {\n                    left: width * 0.08,\n                    right: width * 0.08,\n                    top: height * 0.07,\n                    textAlign: \"center\",\n                  }\n                : {\n                    left: width * 0.56,\n                    width: width * 0.37,\n                    top: 0,\n                    bottom: 0,\n                  }),\n              display: \"flex\",\n              flexDirection: \"column\",\n              justifyContent: \"center\",\n            }}\n          >\n            {eyebrow ? (\n              <div\n                style={{\n                  fontSize: Math.max(height * 0.026, headSize * 0.26),\n                  fontWeight: 400,\n                  color: sub,\n                  marginBottom: headSize * 0.32,\n                  opacity: eyeIn,\n                  transform: `translateY(${(1 - eyeIn) * 10}px)`,\n                }}\n              >\n                {eyebrow}\n              </div>\n            ) : null}\n            <div style={{ overflow: \"hidden\", paddingBottom: headSize * 0.12 }}>\n              <div\n                style={{\n                  fontSize: headSize,\n                  lineHeight: 1.04,\n                  fontWeight: 300,\n                  letterSpacing: \"-0.035em\",\n                  color: ink,\n                  textWrap: \"balance\",\n                  transform: `translateY(${(1 - typeIn(0)) * 105}%)`,\n                }}\n              >\n                {split[0]}\n                {split[1] ? (\n                  <span\n                    style={{\n                      fontFamily: serifFamily,\n                      fontStyle: \"italic\",\n                      fontWeight: 400,\n                      letterSpacing: \"-0.01em\",\n                      fontSize: headSize * 1.08,\n                      display: \"inline-block\",\n                      opacity: typeIn(6),\n                      transform: `translateY(${(1 - typeIn(6)) * 30}%)`,\n                    }}\n                  >\n                    {split[1]}\n                  </span>\n                ) : null}\n              </div>\n            </div>\n          </div>\n        ) : null}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/phone-scroll.tsx"
    }
  ],
  "type": "registry:component"
}
