{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-section-v10",
  "title": "Feature Section v10",
  "description": "Two-column SaaS feature section with dashed corner-accent cards and a morphing wireframe preview that updates on card hover.",
  "dependencies": [
    "motion",
    "gsap"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/feature-section-v10/page.tsx",
      "content": "import FeatureSectionV10 from \"./components/feature-section-v10\"\n\nexport default function Page() {\n  return (\n    <div className=\"p-4\">\n      <FeatureSectionV10 />\n    </div>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/feature-section-v10/page.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-section-v10.tsx",
      "content": "import FeatureShowcase from \"./feature-showcase\"\n\nexport default function FeatureSectionV10() {\n  return (\n    <section className=\"py-4 md:py-6 lg:py-8\">\n      <div className=\"mx-auto max-w-7xl px-4 sm:px-6\">\n        <FeatureShowcase />\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-section-v10.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-showcase.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\n\nimport {\n  featureSectionItems,\n  featureSectionTitle,\n  type FeatureIconId,\n} from \"../lib/features\"\nimport { useHoverIntent } from \"../hooks/use-hover-intent\"\nimport FeatureCard from \"./feature-card\"\nimport FeaturePanel from \"./feature-panel\"\n\nexport default function FeatureShowcase() {\n  const [activeIcon, setActiveIcon] = useState<FeatureIconId>(\n    featureSectionItems[0]?.icon ?? \"flow\"\n  )\n  const { schedule, commit } = useHoverIntent(setActiveIcon)\n\n  return (\n    <div className=\"flex flex-col gap-5 lg:gap-6\">\n      <header className=\"max-w-2xl\">\n        <h2 className=\"m-0 text-2xl font-medium leading-snug tracking-[-0.02em] text-balance sm:text-3xl sm:leading-[1.2] lg:text-[2rem] lg:leading-[1.2]\">\n          {featureSectionTitle}\n        </h2>\n      </header>\n\n      <div className=\"flex flex-col gap-5 lg:grid lg:grid-cols-2 lg:items-stretch lg:gap-8 xl:gap-10\">\n        <ul className=\"m-0 flex min-w-0 list-none flex-col gap-2.5 p-0 sm:gap-3\">\n          {featureSectionItems.map((item) => (\n            <li key={item.id} className=\"m-0 p-0\">\n              <FeatureCard\n                item={item}\n                active={activeIcon === item.icon}\n                onHoverIntent={() => schedule(item.icon)}\n                onActivate={() => commit(item.icon)}\n              />\n            </li>\n          ))}\n        </ul>\n\n        <div className=\"min-w-0 w-full\">\n          <FeaturePanel activeIcon={activeIcon} />\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-showcase.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-card.tsx",
      "content": "import type { ComponentType } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { FeatureItem } from \"../lib/features\"\nimport { FeatureCardCorners } from \"./feature-card-corners\"\n\nexport default function FeatureCard({\n  item,\n  active,\n  onHoverIntent,\n  onActivate,\n}: {\n  item: FeatureItem\n  active: boolean\n  /** Debounced hover — used so sweeping cards does not thrash the morph. */\n  onHoverIntent: () => void\n  /** Immediate commit — focus / click for a11y. */\n  onActivate: () => void\n}) {\n  return (\n    <button\n      type=\"button\"\n      onMouseEnter={onHoverIntent}\n      onFocus={onActivate}\n      onClick={onActivate}\n      className={cn(\n        \"relative w-full cursor-pointer border border-dashed bg-transparent p-4 text-left motion-safe:transition-colors duration-200 sm:p-5\",\n        \"border-border/80 hover:border-primary/45 focus-visible:border-primary/55 focus-visible:outline-none\",\n        active && \"border-primary/50 bg-primary/3\"\n      )}\n      aria-pressed={active}\n    >\n      <FeatureCardCorners active={active} />\n      <div className=\"flex flex-col gap-1\">\n        <h3\n          className={cn(\n            \"m-0 text-[0.9375rem] font-medium leading-snug tracking-[-0.01em] text-foreground sm:text-base\",\n            active && \"text-primary\"\n          )}\n        >\n          {item.title}\n        </h3>\n        <p className=\"m-0 text-[0.8125rem] leading-relaxed text-muted-foreground sm:text-sm\">\n          {item.description}\n        </p>\n      </div>\n    </button>\n  )\n}\n\nexport type FeatureIllustrationComponent = ComponentType<{\n  active?: boolean\n  className?: string\n}>\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-card.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-card-corners.tsx",
      "content": "import { cn } from \"@/lib/utils\"\n\nconst arm =\n  \"absolute size-3 border-primary/40 motion-safe:transition-colors duration-200\"\n\nexport function FeatureCardCorners({ active }: { active?: boolean }) {\n  return (\n    <>\n      <span\n        className={cn(arm, \"top-0 left-0 -translate-x-px -translate-y-px border-t-[1.5px] border-l-[1.5px]\", active && \"border-primary\")}\n        aria-hidden\n      />\n      <span\n        className={cn(arm, \"top-0 right-0 translate-x-px -translate-y-px border-t-[1.5px] border-r-[1.5px]\", active && \"border-primary\")}\n        aria-hidden\n      />\n      <span\n        className={cn(arm, \"bottom-0 left-0 -translate-x-px translate-y-px border-b-[1.5px] border-l-[1.5px]\", active && \"border-primary\")}\n        aria-hidden\n      />\n      <span\n        className={cn(arm, \"right-0 bottom-0 translate-x-px translate-y-px border-r-[1.5px] border-b-[1.5px]\", active && \"border-primary\")}\n        aria-hidden\n      />\n    </>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-card-corners.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-panel.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { FeatureIconId } from \"../lib/features\"\nimport GeoWireframe from \"./geo-wireframe\"\n\nexport default function FeaturePanel({\n  activeIcon,\n  className,\n}: {\n  activeIcon: FeatureIconId\n  className?: string\n}) {\n  return (\n    <div\n      className={cn(\n        \"relative h-80 w-full min-w-0 overflow-hidden rounded-lg sm:h-96 lg:h-full\",\n        className\n      )}\n      aria-live=\"polite\"\n    >\n      <GeoWireframe icon={activeIcon} active className=\"size-full min-h-full\" />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-panel.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/geo-wireframe.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useSyncExternalStore, type RefObject } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { FeatureIconId } from \"../lib/features\"\nimport { featureIllustrationViewBoxes } from \"../lib/features\"\nimport { useGeoMorph } from \"../hooks/use-geo-morph\"\nimport {\n  projectVerticesInto,\n  unifiedMesh,\n  type ProjectedPoint,\n  type Vec3,\n} from \"../lib/geo-mesh\"\n\nimport \"../styles/feature-section-v10.css\"\n\ntype GeoWireframeProps = {\n  icon: FeatureIconId\n  active?: boolean\n  className?: string\n}\n\nconst NODE_COUNT = unifiedMesh.vertices.length\nconst EDGE_COUNT = unifiedMesh.edges.length\n\nfunction MeshScene({\n  active,\n  morphingRef,\n  filterId,\n  verticesRef,\n}: {\n  active: boolean\n  morphingRef: RefObject<boolean>\n  filterId: string\n  verticesRef: RefObject<Vec3[]>\n}) {\n  const reduceMotion = useReducedMotion()\n  const groupRef = useRef<SVGGElement | null>(null)\n  const lineRefs = useRef<(SVGLineElement | null)[]>([])\n  const bokehRefs = useRef<(SVGCircleElement | null)[]>([])\n  const nodeRefs = useRef<(SVGCircleElement | null)[]>([])\n\n  useEffect(() => {\n    const projected: ProjectedPoint[] = []\n\n    let raf = 0\n    let rotation = 0\n    let lastStrokeWidth = -1\n    let lastOpacityScale = -1\n\n    const render = (rotation: number) => {\n      projectVerticesInto(verticesRef.current, rotation, projected)\n      const morphingNow = morphingRef.current\n\n      // Soften edges while morphing — depth opacity alone is enough for\n      // layering; avoid reordering ~1k SVG nodes (that was the main hitch).\n      const strokeWidth = morphingNow ? 1.25 : 1.35\n      const opacityScale = morphingNow ? 0.82 : 1\n      const strokeChanged =\n        strokeWidth !== lastStrokeWidth || opacityScale !== lastOpacityScale\n      lastStrokeWidth = strokeWidth\n      lastOpacityScale = opacityScale\n\n      for (let index = 0; index < EDGE_COUNT; index++) {\n        const [start, end] = unifiedMesh.edges[index]!\n        const a = projected[start]!\n        const b = projected[end]!\n        const el = lineRefs.current[index]\n        if (!el) continue\n\n        const depth = (a.z + b.z) / 2\n        const opacity = (0.48 + ((depth + 120) / 240) * 0.42) * opacityScale\n\n        el.setAttribute(\"x1\", String(a.x))\n        el.setAttribute(\"y1\", String(a.y))\n        el.setAttribute(\"x2\", String(b.x))\n        el.setAttribute(\"y2\", String(b.y))\n        el.setAttribute(\"stroke-opacity\", String(opacity))\n        if (strokeChanged) {\n          el.setAttribute(\"stroke-width\", String(strokeWidth))\n        }\n      }\n\n      for (let index = 0; index < projected.length; index++) {\n        const node = projected[index]!\n        const depthNorm = (node.z + 110) / 220\n\n        const bokeh = bokehRefs.current[index]\n        if (bokeh) {\n          bokeh.setAttribute(\"cx\", String(node.x))\n          bokeh.setAttribute(\"cy\", String(node.y))\n          bokeh.setAttribute(\"r\", String(2.5 + depthNorm * 5))\n          bokeh.setAttribute(\"fill-opacity\", String(0.02 + depthNorm * 0.06))\n        }\n\n        const core = nodeRefs.current[index]\n        if (core) {\n          core.setAttribute(\"cx\", String(node.x))\n          core.setAttribute(\"cy\", String(node.y))\n          core.setAttribute(\"r\", String(1.5 + depthNorm * 1.8))\n          core.setAttribute(\"fill-opacity\", String(0.78 + depthNorm * 0.22))\n        }\n      }\n    }\n\n    if (!active || reduceMotion) {\n      render(reduceMotion ? 0.55 : 0)\n      return\n    }\n\n    let lastTime = 0\n\n    const tick = (time: number) => {\n      if (!lastTime) lastTime = time\n      // Clamp so a background-tab resume does not jump a huge rotation step.\n      const delta = Math.min((time - lastTime) / 1000, 0.064)\n      lastTime = time\n      // Keep spin nearly constant during morph — a big speed jump reads as\n      // a glitch when cards change quickly.\n      const spinBoost = morphingRef.current ? 0.42 : 0.35\n      rotation += delta * spinBoost\n      render(rotation)\n      raf = requestAnimationFrame(tick)\n    }\n\n    raf = requestAnimationFrame(tick)\n    return () => cancelAnimationFrame(raf)\n  }, [active, reduceMotion, verticesRef, morphingRef])\n\n  return (\n    <g ref={groupRef} className=\"fsv10-geo__scene\">\n      {unifiedMesh.edges.map((_, index) => (\n        <line\n          key={`edge-${index}`}\n          ref={(el) => {\n            lineRefs.current[index] = el\n          }}\n          className=\"fsv10-geo__edge\"\n          stroke=\"currentColor\"\n          vectorEffect=\"non-scaling-stroke\"\n        />\n      ))}\n\n      {Array.from({ length: NODE_COUNT }, (_, index) => (\n        <circle\n          key={`bokeh-${index}`}\n          ref={(el) => {\n            bokehRefs.current[index] = el\n          }}\n          fill=\"currentColor\"\n          filter={`url(#${filterId}-blur)`}\n        />\n      ))}\n\n      {Array.from({ length: NODE_COUNT }, (_, index) => (\n        <circle\n          key={`node-${index}`}\n          ref={(el) => {\n            nodeRefs.current[index] = el\n          }}\n          className=\"fsv10-geo__node\"\n          fill=\"currentColor\"\n          style={{ animationDelay: `${(index % 12) * 45}ms` }}\n        />\n      ))}\n    </g>\n  )\n}\n\nexport default function GeoWireframe({ icon, active = false, className }: GeoWireframeProps) {\n  const filterId = \"fsv10-geo-morph\"\n  const svgRef = useRef<SVGSVGElement | null>(null)\n\n  const onMorphingChange = (morphing: boolean) => {\n    svgRef.current?.classList.toggle(\"fsv10-geo--morphing\", morphing)\n  }\n\n  const { verticesRef, morphingRef } = useGeoMorph(icon, { onMorphingChange })\n\n  const mounted = useSyncExternalStore(\n    () => () => {},\n    () => true,\n    () => false\n  )\n\n  return (\n    <div\n      className={cn(\n        \"relative h-full min-h-full w-full overflow-hidden text-foreground\",\n        className\n      )}\n    >\n      <svg\n        ref={svgRef}\n        aria-hidden\n        viewBox={featureIllustrationViewBoxes[icon]}\n        preserveAspectRatio=\"xMidYMid meet\"\n        className={cn(\"block h-full min-h-full w-full\", active && \"fsv10-geo--active\")}\n        fill=\"none\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        <defs>\n          <filter id={`${filterId}-blur`} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n            <feGaussianBlur stdDeviation=\"1.25\" />\n          </filter>\n        </defs>\n\n        {mounted && (\n          <MeshScene\n            active={active}\n            morphingRef={morphingRef}\n            filterId={filterId}\n            verticesRef={verticesRef}\n          />\n        )}\n      </svg>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/geo-wireframe.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/components/feature-illustrations.tsx",
      "content": "\"use client\"\n\nimport type { FeatureIconId } from \"../lib/features\"\nimport GeoWireframe from \"./geo-wireframe\"\n\ntype IllustrationProps = {\n  active?: boolean\n  className?: string\n}\n\nfunction createIllustration(icon: FeatureIconId) {\n  return function FeatureGeoIllustration({ active, className }: IllustrationProps) {\n    return <GeoWireframe icon={icon} active={active} className={className} />\n  }\n}\n\nexport const FlowIllustration = createIllustration(\"flow\")\nexport const BranchIllustration = createIllustration(\"branch\")\nexport const SignalIllustration = createIllustration(\"signal\")\nexport const SecureIllustration = createIllustration(\"secure\")\n\nexport type { IllustrationProps }\n",
      "type": "registry:component",
      "target": "app/feature-section-v10/components/feature-illustrations.tsx"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/hooks/use-geo-morph.ts",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport gsap from \"gsap\"\n\nimport type { FeatureIconId } from \"../lib/features\"\nimport { getShapeVertices, lerpVec3Into, type Vec3 } from \"../lib/geo-mesh\"\n\n/** Snappy enough to interrupt, long enough to read as a morph. */\nconst MORPH_DURATION = 0.65\n\nfunction cloneVertices(vertices: readonly Vec3[]): Vec3[] {\n  return vertices.map((vertex) => [vertex[0], vertex[1], vertex[2]] as Vec3)\n}\n\nfunction copyVerticesInto(target: Vec3[], source: readonly Vec3[]) {\n  for (let i = 0; i < source.length; i++) {\n    const src = source[i]!\n    const dst = target[i]\n    if (dst) {\n      ;(dst as [number, number, number])[0] = src[0]\n      ;(dst as [number, number, number])[1] = src[1]\n      ;(dst as [number, number, number])[2] = src[2]\n    } else {\n      target[i] = [src[0], src[1], src[2]]\n    }\n  }\n  target.length = source.length\n}\n\ntype UseGeoMorphOptions = {\n  /** Imperative signal — avoid React state so morph start/end does not reconcile the SVG tree. */\n  onMorphingChange?: (morphing: boolean) => void\n}\n\n/**\n * Interpolates vertices between shapes without putting the per-frame tween\n * value in React state — `verticesRef` is mutated in place on every GSAP\n * tick so callers can read it inside their own rAF loop.\n *\n * Retargeting always starts from the *current* vertex positions (even mid-\n * morph), so rapid card switches never snap or get stuck mid-lerp.\n */\nexport function useGeoMorph(\n  targetIcon: FeatureIconId,\n  { onMorphingChange }: UseGeoMorphOptions = {}\n) {\n  const reduceMotion = useReducedMotion()\n  const verticesRef = useRef<Vec3[]>(cloneVertices(getShapeVertices(targetIcon)))\n  const morphingRef = useRef(false)\n  const tweenRef = useRef<gsap.core.Tween | null>(null)\n  /** Last shape that fully completed a morph. */\n  const settledIconRef = useRef(targetIcon)\n  /** Shape the active tween is heading toward (may differ from settled). */\n  const aimIconRef = useRef(targetIcon)\n  const onMorphingChangeRef = useRef(onMorphingChange)\n\n  useEffect(() => {\n    onMorphingChangeRef.current = onMorphingChange\n  }, [onMorphingChange])\n\n  const setMorphing = (next: boolean) => {\n    if (morphingRef.current === next) return\n    morphingRef.current = next\n    onMorphingChangeRef.current?.(next)\n  }\n\n  useEffect(() => {\n    const targetVertices = getShapeVertices(targetIcon)\n\n    if (reduceMotion) {\n      tweenRef.current?.kill()\n      tweenRef.current = null\n      copyVerticesInto(verticesRef.current, targetVertices)\n      settledIconRef.current = targetIcon\n      aimIconRef.current = targetIcon\n      setMorphing(false)\n      return\n    }\n\n    // Already tweening toward this target — do not restart.\n    if (targetIcon === aimIconRef.current && tweenRef.current) return\n\n    // Fully settled on this target.\n    if (targetIcon === settledIconRef.current && !tweenRef.current) return\n\n    // Interrupt in-flight morph and retarget from wherever vertices are now.\n    tweenRef.current?.kill()\n    aimIconRef.current = targetIcon\n\n    const startVertices = cloneVertices(verticesRef.current)\n    const proxy = { t: 0 }\n    setMorphing(true)\n\n    tweenRef.current = gsap.to(proxy, {\n      t: 1,\n      duration: MORPH_DURATION,\n      ease: \"power2.inOut\",\n      onUpdate: () => {\n        const current = verticesRef.current\n        for (let i = 0; i < current.length; i++) {\n          lerpVec3Into(current[i]!, startVertices[i]!, targetVertices[i]!, proxy.t)\n        }\n      },\n      onComplete: () => {\n        copyVerticesInto(verticesRef.current, targetVertices)\n        settledIconRef.current = targetIcon\n        tweenRef.current = null\n        setMorphing(false)\n      },\n    })\n  }, [targetIcon, reduceMotion])\n\n  useEffect(() => {\n    return () => {\n      tweenRef.current?.kill()\n      tweenRef.current = null\n    }\n  }, [])\n\n  return { verticesRef, morphingRef }\n}\n\nexport { MORPH_DURATION }\n",
      "type": "registry:hook",
      "target": "app/feature-section-v10/hooks/use-geo-morph.ts"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/hooks/use-hover-intent.ts",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\n/**\n * Delays hover-driven activation so sweeping across cards does not thrash\n * the morph. Focus / click should call the returned activate immediately\n * (pass `immediate: true`) for keyboard a11y.\n */\nexport function useHoverIntent<T>(\n  onCommit: (value: T) => void,\n  delayMs = 110\n) {\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const onCommitRef = useRef(onCommit)\n\n  useEffect(() => {\n    onCommitRef.current = onCommit\n  }, [onCommit])\n\n  useEffect(() => {\n    return () => {\n      if (timeoutRef.current !== null) clearTimeout(timeoutRef.current)\n    }\n  }, [])\n\n  const schedule = (value: T) => {\n    if (timeoutRef.current !== null) clearTimeout(timeoutRef.current)\n    timeoutRef.current = setTimeout(() => {\n      timeoutRef.current = null\n      onCommitRef.current(value)\n    }, delayMs)\n  }\n\n  const commit = (value: T) => {\n    if (timeoutRef.current !== null) clearTimeout(timeoutRef.current)\n    timeoutRef.current = null\n    onCommitRef.current(value)\n  }\n\n  const cancel = () => {\n    if (timeoutRef.current !== null) clearTimeout(timeoutRef.current)\n    timeoutRef.current = null\n  }\n\n  return { schedule, commit, cancel }\n}\n",
      "type": "registry:hook",
      "target": "app/feature-section-v10/hooks/use-hover-intent.ts"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/lib/geo-mesh.ts",
      "content": "import type { FeatureIconId } from \"./features\"\n\nexport type Vec3 = readonly [number, number, number]\n\nexport type GeoMesh = {\n  vertices: readonly Vec3[]\n  edges: readonly (readonly [number, number])[]\n}\n\nexport type ProjectedPoint = {\n  x: number\n  y: number\n  z: number\n  index: number\n}\n\nconst VIEW_CX = 240\nconst VIEW_CY = 180\nconst VIEW_SCALE = 1.48\n\n/** Shared lattice — identical topology lets vertices morph between shapes. */\nexport const MESH_RINGS = 15\nexport const MESH_SEGMENTS = 18\n\nexport function projectVertices(\n  vertices: readonly Vec3[],\n  rotateY = 0\n): ProjectedPoint[] {\n  const cos = Math.cos(rotateY)\n  const sin = Math.sin(rotateY)\n\n  return vertices.map((vertex, index) => {\n    const [x, y, z] = vertex\n    const rx = x * cos - z * sin\n    const rz = x * sin + z * cos\n    const depth = 1 + rz * 0.0028\n    const tilt = y * 0.08\n\n    return {\n      x: VIEW_CX + (rx / depth) * VIEW_SCALE,\n      y: VIEW_CY + ((y + tilt) / depth) * VIEW_SCALE,\n      z: rz,\n      index,\n    }\n  })\n}\n\n/** Same projection as {@link projectVertices}, writing into `out` in place to avoid per-frame allocation. */\nexport function projectVerticesInto(\n  vertices: readonly Vec3[],\n  rotateY: number,\n  out: ProjectedPoint[]\n): ProjectedPoint[] {\n  const cos = Math.cos(rotateY)\n  const sin = Math.sin(rotateY)\n\n  for (let index = 0; index < vertices.length; index++) {\n    const [x, y, z] = vertices[index]!\n    const rx = x * cos - z * sin\n    const rz = x * sin + z * cos\n    const depth = 1 + rz * 0.0028\n    const tilt = y * 0.08\n\n    const point = out[index]\n    if (point) {\n      point.x = VIEW_CX + (rx / depth) * VIEW_SCALE\n      point.y = VIEW_CY + ((y + tilt) / depth) * VIEW_SCALE\n      point.z = rz\n      point.index = index\n    } else {\n      out[index] = {\n        x: VIEW_CX + (rx / depth) * VIEW_SCALE,\n        y: VIEW_CY + ((y + tilt) / depth) * VIEW_SCALE,\n        z: rz,\n        index,\n      }\n    }\n  }\n\n  out.length = vertices.length\n  return out\n}\n\nfunction gridEdges(ringCount: number, segmentCount: number): (readonly [number, number])[] {\n  const edges: (readonly [number, number])[] = []\n  const indexAt = (ring: number, segment: number) =>\n    ring * segmentCount + (((segment % segmentCount) + segmentCount) % segmentCount)\n\n  for (let ring = 0; ring < ringCount; ring++) {\n    for (let segment = 0; segment < segmentCount; segment++) {\n      edges.push([indexAt(ring, segment), indexAt(ring, segment + 1)])\n      edges.push([indexAt(ring, segment), indexAt(ring + 1, segment)])\n    }\n  }\n\n  return edges\n}\n\ntype ShapeSample = {\n  ring: number\n  segment: number\n  tRing: number\n  tSeg: number\n  angle: number\n}\n\nfunction sampleGrid(ring: number, segment: number): ShapeSample {\n  return {\n    ring,\n    segment,\n    tRing: ring / MESH_RINGS,\n    tSeg: segment / MESH_SEGMENTS,\n    angle: (segment / MESH_SEGMENTS) * Math.PI * 2,\n  }\n}\n\n/** Wireframe sphere — security / trust boundary. */\nfunction sphereVertex({ tRing, angle }: ShapeSample): Vec3 {\n  const phi = tRing * Math.PI\n  const radius = 88\n  const y = Math.cos(phi) * radius\n  const ringRadius = Math.sin(phi) * radius\n\n  return [\n    Math.cos(angle) * ringRadius,\n    y,\n    Math.sin(angle) * ringRadius,\n  ]\n}\n\n/**\n * Soft cube (superellipsoid) — one shared workspace / source of truth.\n * Same spherical parameterization as the secure sphere, then L^p-normalized\n * toward a cube so morphs stay continuous.\n */\nfunction cubeVertex({ tRing, angle }: ShapeSample): Vec3 {\n  const phi = tRing * Math.PI\n  const sx = Math.sin(phi) * Math.cos(angle)\n  const sy = Math.cos(phi)\n  const sz = Math.sin(phi) * Math.sin(angle)\n  const p = 5\n  const norm = Math.pow(\n    Math.abs(sx) ** p + Math.abs(sy) ** p + Math.abs(sz) ** p,\n    1 / p\n  )\n  const radius = 88\n  const scale = norm > 0 ? radius / norm : radius\n\n  return [sx * scale, sy * scale, sz * scale]\n}\n\n/**\n * Dual helix — two parallel lanes that wind together without colliding.\n * A peanut cross-section twists up the Y axis so both strands stay one\n * continuous lattice (no hard strand picks that break morphs).\n */\nfunction helixVertex({ tRing, angle }: ShapeSample): Vec3 {\n  const turns = 2.1\n  const twist = tRing * Math.PI * 2 * turns\n  const height = (tRing - 0.5) * 172\n\n  const sharpness = 3.2\n  const lobe = Math.tanh(sharpness * Math.cos(angle))\n  const separation = 30\n  const baseRadius = 15\n  const pinch = 0.58\n  const localRadius = baseRadius * (1 - pinch * (1 - lobe * lobe))\n\n  const localX = lobe * separation + Math.cos(angle) * localRadius\n  const localZ = Math.sin(angle) * localRadius * 0.72\n\n  const cos = Math.cos(twist)\n  const sin = Math.sin(twist)\n\n  return [\n    localX * cos - localZ * sin,\n    height,\n    localX * sin + localZ * cos,\n  ]\n}\n\n/** Torus lattice — signals / notifications. */\nfunction torusVertex({ tRing, angle }: ShapeSample): Vec3 {\n  const majorRadius = 72\n  const minorRadius = 26\n  const majorAngle = tRing * Math.PI * 2\n  const cx = Math.cos(majorAngle) * majorRadius\n  const cz = Math.sin(majorAngle) * majorRadius\n  const nx = Math.cos(majorAngle) * Math.cos(angle)\n  const ny = Math.sin(angle)\n  const nz = Math.sin(majorAngle) * Math.cos(angle)\n\n  return [\n    cx + nx * minorRadius,\n    ny * minorRadius,\n    cz + nz * minorRadius,\n  ]\n}\n\nconst shapeBuilders: Record<FeatureIconId, (sample: ShapeSample) => Vec3> = {\n  flow: cubeVertex,\n  branch: helixVertex,\n  signal: torusVertex,\n  secure: sphereVertex,\n}\n\nfunction buildShapeVertices(icon: FeatureIconId): Vec3[] {\n  const build = shapeBuilders[icon]\n  const vertices: Vec3[] = []\n\n  for (let ring = 0; ring <= MESH_RINGS; ring++) {\n    for (let segment = 0; segment < MESH_SEGMENTS; segment++) {\n      vertices.push(build(sampleGrid(ring, segment)))\n    }\n  }\n\n  return vertices\n}\n\nfunction centerVertices3D(vertices: readonly Vec3[]): Vec3[] {\n  let ox = 0\n  let oy = 0\n  let oz = 0\n\n  for (const [x, y, z] of vertices) {\n    ox += x\n    oy += y\n    oz += z\n  }\n\n  const count = vertices.length\n  ox /= count\n  oy /= count\n  oz /= count\n\n  return vertices.map(([x, y, z]) => [x - ox, y - oy, z - oz] as const)\n}\n\nfunction getProjectedMaxDim(vertices: readonly Vec3[]): number {\n  const projected = projectVertices(vertices, 0)\n  let minX = Number.POSITIVE_INFINITY\n  let maxX = Number.NEGATIVE_INFINITY\n  let minY = Number.POSITIVE_INFINITY\n  let maxY = Number.NEGATIVE_INFINITY\n\n  for (const point of projected) {\n    minX = Math.min(minX, point.x)\n    maxX = Math.max(maxX, point.x)\n    minY = Math.min(minY, point.y)\n    maxY = Math.max(maxY, point.y)\n  }\n\n  return Math.max(maxX - minX, maxY - minY)\n}\n\n/** Scale every shape to match the secure sphere footprint in the panel. */\nfunction matchShapeToReference(vertices: readonly Vec3[], referenceMaxDim: number): Vec3[] {\n  const centered = centerVertices3D(vertices)\n  const maxDim = getProjectedMaxDim(centered)\n  const scale = maxDim > 0 ? referenceMaxDim / maxDim : 1\n\n  return centered.map(([x, y, z]) => [x * scale, y * scale, z * scale] as const)\n}\n\nfunction buildNormalizedShapeCache(): Record<FeatureIconId, readonly Vec3[]> {\n  const referenceVertices = centerVertices3D(buildShapeVertices(\"secure\"))\n  const referenceMaxDim = getProjectedMaxDim(referenceVertices)\n\n  return {\n    flow: matchShapeToReference(buildShapeVertices(\"flow\"), referenceMaxDim),\n    branch: matchShapeToReference(buildShapeVertices(\"branch\"), referenceMaxDim),\n    signal: matchShapeToReference(buildShapeVertices(\"signal\"), referenceMaxDim),\n    secure: referenceVertices,\n  }\n}\n\nconst normalizedShapeCache = buildNormalizedShapeCache()\n\nexport function getShapeVertices(icon: FeatureIconId): readonly Vec3[] {\n  return normalizedShapeCache[icon]\n}\n\nexport function lerpVec3(a: Vec3, b: Vec3, t: number): Vec3 {\n  return [\n    a[0] + (b[0] - a[0]) * t,\n    a[1] + (b[1] - a[1]) * t,\n    a[2] + (b[2] - a[2]) * t,\n  ]\n}\n\n/** In-place lerp — avoids allocating a new tuple every GSAP tick. */\nexport function lerpVec3Into(out: Vec3, a: Vec3, b: Vec3, t: number): void {\n  ;(out as [number, number, number])[0] = a[0] + (b[0] - a[0]) * t\n  ;(out as [number, number, number])[1] = a[1] + (b[1] - a[1]) * t\n  ;(out as [number, number, number])[2] = a[2] + (b[2] - a[2]) * t\n}\n\nconst unifiedEdges = gridEdges(MESH_RINGS, MESH_SEGMENTS)\n\nexport const unifiedMesh: GeoMesh = {\n  vertices: normalizedShapeCache.secure,\n  edges: unifiedEdges,\n}\n\n/** @deprecated Use getShapeVertices + unifiedMesh.edges */\nexport const geoMeshes = {\n  flow: { vertices: normalizedShapeCache.flow, edges: unifiedEdges },\n  branch: { vertices: normalizedShapeCache.branch, edges: unifiedEdges },\n  signal: { vertices: normalizedShapeCache.signal, edges: unifiedEdges },\n  secure: { vertices: normalizedShapeCache.secure, edges: unifiedEdges },\n} as const\n",
      "type": "registry:lib",
      "target": "app/feature-section-v10/lib/geo-mesh.ts"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/styles/feature-section-v10.css",
      "content": "@keyframes fsv10-geo-node-pulse {\n  0%,\n  100% {\n    opacity: 0.8;\n    transform: scale(1);\n  }\n  50% {\n    opacity: 0.95;\n    transform: scale(1.15);\n  }\n}\n\n.fsv10-geo__node {\n  transform-box: fill-box;\n  transform-origin: center;\n}\n\n.fsv10-geo--active .fsv10-geo__node {\n  animation: fsv10-geo-node-pulse 2.8s ease-in-out infinite;\n}\n\n/* Pause node pulse while morphing — CSS opacity fights the JS depth pass. */\n.fsv10-geo--morphing .fsv10-geo__node {\n  animation: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .fsv10-geo__node {\n    animation: none;\n    transform: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "app/feature-section-v10/styles/feature-section-v10.css"
    },
    {
      "path": "registry/new-york/blocks/feature-section-v10/lib/features.ts",
      "content": "export type FeatureIconId = \"flow\" | \"branch\" | \"signal\" | \"secure\"\n\nexport const featureSectionTitle = \"Get work done without the clutter\"\n\nexport const featureIllustrationViewBoxes: Record<FeatureIconId, string> = {\n  flow: \"0 0 480 360\",\n  branch: \"0 0 480 360\",\n  signal: \"0 0 480 360\",\n  secure: \"0 0 480 360\",\n}\n\nexport type FeatureItem = {\n  id: string\n  title: string\n  description: string\n  icon: FeatureIconId\n}\n\nexport const featureSectionItems: FeatureItem[] = [\n  {\n    id: \"one-source-of-truth\",\n    title: \"Everything in one place\",\n    description:\n      \"Bring related work into a single shared space so you stop hunting across tabs and tools.\",\n    icon: \"flow\",\n  },\n  {\n    id: \"work-in-parallel\",\n    title: \"Try more than one path\",\n    description:\n      \"Explore options side by side, then keep what works and leave the rest behind.\",\n    icon: \"branch\",\n  },\n  {\n    id: \"stay-in-the-loop\",\n    title: \"Only the updates that matter\",\n    description:\n      \"Hear from the product when something needs you, and stay quiet when nothing does.\",\n    icon: \"signal\",\n  },\n  {\n    id: \"secure-by-default\",\n    title: \"Clear control over access\",\n    description:\n      \"Decide who can see and change what, with permissions that are easy to set and easy to explain.\",\n    icon: \"secure\",\n  },\n]\n",
      "type": "registry:lib",
      "target": "app/feature-section-v10/lib/features.ts"
    }
  ],
  "type": "registry:block"
}