{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "services-preview-list-01",
  "title": "Services Preview List",
  "description": "A large numbered list of services beside a panel that stays in view and wipes in the image and description of the one you point at",
  "dependencies": [
    "motion@13"
  ],
  "registryDependencies": [
    "https://ui.cortexcn.dev/r/utils.json",
    "button"
  ],
  "files": [
    {
      "path": "registry/blocks/services-preview-list-01/components/services-preview-list.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion, type Variants } from \"motion/react\";\nimport { useId, useState } from \"react\";\n\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\n// Cortexcn item \"services-preview-list-01\", revision 9a7b97d16c03, MIT license.\n// https://ui.cortexcn.dev/r/v/services-preview-list-01/9a7b97d16c03.json\n\ninterface ServicesAction {\n  label: string;\n  href: string;\n}\n\nexport interface PreviewService {\n  title: string;\n  description: string;\n  image: { src: string; alt: string };\n}\n\nexport interface ServicesPreviewListProps {\n  /** Pointing at or focusing a service shows its image and description in the side panel. */\n  services?: PreviewService[];\n  action?: ServicesAction;\n  className?: string;\n}\n\n/** The flower paintings from the Cortexcn site. */\nconst ART_BASE = \"https://www.cortexcn.dev/images/ui-home\";\n\nconst DEFAULT_SERVICES: PreviewService[] = [\n  {\n    title: \"Landing pages\",\n    description:\n      \"One custom page for a launch, a campaign, or a product, with scroll and hover animations, a contact or waitlist form, and SEO basics. Most ship in one to two weeks.\",\n    image: {\n      src: `${ART_BASE}/neutral-florals.webp`,\n      alt: \"A painting of white flowers on tall stems\",\n    },\n  },\n  {\n    title: \"Websites\",\n    description:\n      \"Five custom pages and a CMS your team can edit on its own, with SEO basics, sitemaps, a contact form, and analytics. Most take three to four weeks.\",\n    image: {\n      src: `${ART_BASE}/pastel-wildflowers.webp`,\n      alt: \"A painting of golden wildflowers in soft blue light\",\n    },\n  },\n  {\n    title: \"Web applications\",\n    description:\n      \"A product with accounts, data, and an admin: sign-in and roles, a database and API, a dashboard, and automated tests and deploys.\",\n    image: {\n      src: `${ART_BASE}/white-blooms.webp`,\n      alt: \"A painting of white blooms drifting in the wind\",\n    },\n  },\n  {\n    title: \"Backend and APIs\",\n    description:\n      \"APIs designed and documented, with the database, auth, and background jobs behind them, ready for your web or mobile app.\",\n    image: {\n      src: `${ART_BASE}/blue-cream-florals.webp`,\n      alt: \"A painting of cream flowers in a blue field\",\n    },\n  },\n  {\n    title: \"AI pipelines\",\n    description:\n      \"Retrieval over your own data, agents that call your tools, and evals that show how well it works before it reaches your users.\",\n    image: {\n      src: `${ART_BASE}/soft-meadow-grass.webp`,\n      alt: \"A painting of meadow grass and small flowers\",\n    },\n  },\n];\n\n/** Wipes the new image up over the one before it. */\nconst reveal: Variants = {\n  hidden: { clipPath: \"inset(100% 0% 0% 0%)\" },\n  visible: {\n    clipPath: \"inset(0% 0% 0% 0%)\",\n    transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },\n  },\n};\n\nfunction ArrowUpRightIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"size-4\", className)}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth=\"2\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path d=\"M7 7h10v10M7 17 17 7\" />\n    </svg>\n  );\n}\n\n/** An L-shaped mark for the bottom-left corner; flip it for the others. */\nfunction CornerMark({ className }: { className: string }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"pointer-events-none absolute size-3\", className)}\n      fill=\"none\"\n      viewBox=\"0 0 12 12\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M0 0V12M0 12H12\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"square\"\n        strokeWidth=\"1\"\n      />\n    </svg>\n  );\n}\n\nexport function ServicesPreviewList({\n  services = DEFAULT_SERVICES,\n  action = { label: \"Get a quote\", href: \"#\" },\n  className,\n}: ServicesPreviewListProps) {\n  const [active, setActive] = useState(0);\n  const [previous, setPrevious] = useState(0);\n  const reduce = useReducedMotion();\n  const panelId = useId();\n\n  const current = services[active] ?? services[0];\n  if (!current) {\n    return null;\n  }\n\n  function select(index: number) {\n    if (index === active) {\n      return;\n    }\n    setPrevious(active);\n    setActive(index);\n  }\n\n  return (\n    <section className={cn(\"px-6 py-24 sm:py-32\", className)}>\n      <div className=\"mx-auto flex max-w-6xl flex-col gap-12 lg:flex-row lg:gap-20\">\n        <div\n          className=\"flex h-fit w-full flex-col gap-6 lg:sticky lg:top-10 lg:max-w-xs\"\n          id={panelId}\n        >\n          <div className=\"relative h-90 overflow-hidden bg-muted\">\n            {/* Every image loads up front, so the wipe never shows a blank. The\n                one underneath is the image being covered. */}\n            {services.map((service, index) => (\n              // biome-ignore lint/performance/noImgElement: framework-agnostic registry block\n              <img\n                alt=\"\"\n                className={cn(\n                  \"absolute inset-0 size-full object-cover\",\n                  index !== previous && \"invisible\",\n                )}\n                decoding=\"async\"\n                height={720}\n                key={service.title}\n                src={service.image.src}\n                width={640}\n              />\n            ))}\n            <motion.div\n              animate=\"visible\"\n              className=\"absolute inset-0\"\n              initial={reduce ? false : \"hidden\"}\n              key={active}\n              variants={reveal}\n            >\n              {/* biome-ignore lint/performance/noImgElement: framework-agnostic registry block */}\n              <img\n                alt={current.image.alt}\n                className=\"size-full object-cover\"\n                decoding=\"async\"\n                height={720}\n                src={current.image.src}\n                width={640}\n              />\n            </motion.div>\n          </div>\n          <div aria-live=\"polite\" className=\"flex flex-col gap-3\">\n            <p className=\"font-semibold text-muted-foreground text-sm uppercase tracking-widest\">\n              {current.title}\n            </p>\n            <p className=\"text-muted-foreground leading-relaxed\">\n              {current.description}\n            </p>\n          </div>\n        </div>\n\n        <div className=\"flex w-full flex-col items-start gap-10 xl:pl-20\">\n          <ul className=\"w-full\">\n            {services.map((service, index) => {\n              const isActive = index === active;\n              return (\n                <li className=\"border-border border-b\" key={service.title}>\n                  <button\n                    aria-controls={panelId}\n                    aria-pressed={isActive}\n                    className={cn(\n                      \"w-full py-8 text-left font-semibold text-4xl tracking-tight outline-none transition-opacity duration-300 focus-visible:ring-3 focus-visible:ring-ring/50 sm:text-5xl lg:text-6xl\",\n                      isActive ? \"opacity-100\" : \"opacity-20\",\n                    )}\n                    onClick={() => select(index)}\n                    onFocus={() => select(index)}\n                    onPointerEnter={() => select(index)}\n                    type=\"button\"\n                  >\n                    {service.title}\n                    <sup className=\"ml-1 font-normal text-muted-foreground text-sm tabular-nums lg:text-2xl\">\n                      {`{${String(index + 1).padStart(2, \"0\")}}`}\n                    </sup>\n                  </button>\n                </li>\n              );\n            })}\n          </ul>\n          <a\n            className={cn(\n              buttonVariants({ variant: \"outline\" }),\n              \"relative rounded-none px-4 shadow-none\",\n            )}\n            href={action.href}\n          >\n            {action.label}\n            <ArrowUpRightIcon className=\"transition-transform duration-300 motion-safe:group-hover/button:rotate-45\" />\n            <CornerMark className=\"-bottom-px -left-px\" />\n            <CornerMark className=\"-right-px -bottom-px scale-x-[-1]\" />\n            <CornerMark className=\"-top-px -left-px scale-y-[-1]\" />\n            <CornerMark className=\"-top-px -right-px scale-[-1]\" />\n          </a>\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/services-preview-list.tsx"
    }
  ],
  "type": "registry:block",
  "author": "Cortexcn (https://ui.cortexcn.dev)",
  "meta": {
    "license": "MIT",
    "revision": "9a7b97d16c03"
  }
}