{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "features-5",
  "title": "Features 5: scrolling product tour",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Numbered steps on the left; as each scrolls into view, a sticky panel on the right crossfades to its picture. On phones every step carries its own picture.",
  "dependencies": [
    "lucide-react",
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/avatar.json",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/features-5/features-5.tsx",
      "content": "// Ballmac UI: Features 5. https://ui.ballmac.com/blocks/features-5\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Inbox, Link2, Lock, Sparkles, Tag, Workflow, Zap } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { Avatar, AvatarFallback } from \"@/components/ballmac/avatar\"\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { cn } from \"@/lib/utils\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype Features5Step = {\n  /** Step title. */\n  title: string\n  /** One or two sentences. */\n  description: string\n  /** Short checked points under the description. */\n  points?: string[]\n  /** The sample picture shown beside the steps while this step is in view. Decorative: describe it in the text. Replaced by `image` when you set one. */\n  visual?: React.ReactNode\n  /** Your own picture for this step: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. */\n  image?: MediaSource\n  /** Describes `image` when it is a plain URL. */\n  imageAlt?: string\n}\n\ntype Features5Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Label above the heading. */\n  eyebrow?: string\n  /** Section heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** The steps, in order. */\n  steps?: Features5Step[]\n}\n\n/* ----------------------------------------------------------------------------------------------\n * Sample visuals: small product panels drawn with theme tokens.\n * --------------------------------------------------------------------------------------------*/\n\nconst panel = \"bg-card w-full max-w-sm rounded-2xl border p-5 shadow-[0_30px_70px_-35px_rgb(0_0_0/0.4)]\"\n\nfunction CaptureVisual() {\n  const items = [\n    { t: \"Pricing page feedback from Maya\", tag: \"Customer\", tone: \"bg-chart-1\" },\n    { t: \"Ship the invoice export before Friday\", tag: \"Task\", tone: \"bg-chart-3\" },\n    { t: \"Idea: weekly digest for admins\", tag: \"Idea\", tone: \"bg-chart-5\" },\n  ]\n  return (\n    <div className={panel}>\n      <div className=\"flex items-center gap-2 text-sm font-medium\">\n        <Inbox className=\"size-4\" /> Inbox <Badge variant=\"secondary\" className=\"ms-auto\">3 new</Badge>\n      </div>\n      <ul className=\"mt-4 space-y-2.5\">\n        {items.map((i) => (\n          <li key={i.t} className=\"flex items-center gap-3 rounded-xl border p-3\">\n            <span className={cn(\"size-2 shrink-0 rounded-full\", i.tone)} />\n            <span className=\"min-w-0 flex-1 truncate text-sm\">{i.t}</span>\n            <span className=\"text-muted-foreground text-xs\">{i.tag}</span>\n          </li>\n        ))}\n      </ul>\n      <div className=\"text-muted-foreground mt-4 flex items-center gap-2 rounded-xl border border-dashed px-3 py-2.5 text-sm\">\n        <Sparkles className=\"size-4\" /> Paste a link or just start typing\n      </div>\n    </div>\n  )\n}\n\nfunction OrganizeVisual() {\n  const cols = [\n    { name: \"Backlog\", cards: [\"Export to CSV\", \"Dark mode polish\"] },\n    { name: \"In progress\", cards: [\"Invoice export\", \"Team roles\", \"Audit log\"] },\n    { name: \"Done\", cards: [\"SSO setup\"] },\n  ]\n  return (\n    <div className={cn(panel, \"max-w-md\")}>\n      <div className=\"flex items-center gap-2 text-sm font-medium\">\n        <Tag className=\"size-4\" /> Q4 roadmap\n      </div>\n      <div className=\"mt-4 grid grid-cols-3 gap-3\">\n        {cols.map((c) => (\n          <div key={c.name} className=\"bg-muted/50 rounded-xl p-2\">\n            <p className=\"text-muted-foreground px-1 pb-2 text-[11px] font-medium\">{c.name}</p>\n            <div className=\"space-y-2\">\n              {c.cards.map((card) => (\n                <div key={card} className=\"bg-card rounded-lg border p-2 text-xs leading-snug shadow-xs\">{card}</div>\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\nfunction AutomateVisual() {\n  const steps = [\n    { icon: Zap, label: \"When\", value: \"An invoice is overdue 7 days\" },\n    { icon: Workflow, label: \"Then\", value: \"Send a friendly reminder email\" },\n    { icon: Link2, label: \"And\", value: \"Post to #finance in Slack\" },\n  ]\n  return (\n    <div className={panel}>\n      <div className=\"flex items-center justify-between text-sm font-medium\">\n        Overdue invoices <Badge status=\"success\">Active</Badge>\n      </div>\n      <ol className=\"relative mt-5 space-y-3\">\n        <span aria-hidden=\"true\" className=\"bg-border absolute top-6 bottom-6 start-[1.1rem] w-px\" />\n        {steps.map(({ icon: Icon, label, value }) => (\n          <li key={label} className=\"relative flex items-center gap-3\">\n            <span className=\"bg-background relative flex size-9 shrink-0 items-center justify-center rounded-full border\"><Icon className=\"size-4\" /></span>\n            <div className=\"min-w-0 flex-1 rounded-xl border px-3 py-2\">\n              <p className=\"text-muted-foreground text-[11px] font-medium uppercase tracking-wide\">{label}</p>\n              <p className=\"truncate text-sm\">{value}</p>\n            </div>\n          </li>\n        ))}\n      </ol>\n    </div>\n  )\n}\n\nfunction ShareVisual() {\n  const people = [\n    [\"MK\", \"Maya Kim\", \"Can edit\", \"bg-chart-1/25\"],\n    [\"JO\", \"Jonas Ortiz\", \"Can comment\", \"bg-chart-3/25\"],\n    [\"AS\", \"Amara Singh\", \"Can view\", \"bg-chart-5/25\"],\n  ]\n  return (\n    <div className={panel}>\n      <p className=\"text-sm font-medium\">Share “Q4 roadmap”</p>\n      <div className=\"mt-4 space-y-3\">\n        {people.map(([i, n, r, c]) => (\n          <div key={n} className=\"flex items-center gap-3\">\n            <Avatar size=\"sm\"><AvatarFallback className={cn(\"text-foreground text-[10px] font-semibold\", c)}>{i}</AvatarFallback></Avatar>\n            <span className=\"flex-1 text-sm\">{n}</span>\n            <span className=\"text-muted-foreground text-xs\">{r}</span>\n          </div>\n        ))}\n      </div>\n      <div className=\"text-muted-foreground mt-5 flex items-center gap-2 rounded-xl border px-3 py-2.5 text-sm\">\n        <Lock className=\"size-4\" /> Only people with the link\n        <Check className=\"text-chart-2 ms-auto size-4\" />\n      </div>\n    </div>\n  )\n}\n\nconst defaultSteps: Features5Step[] = [\n  {\n    title: \"Capture anything in seconds\",\n    description: \"Notes, links and ideas land in one inbox the moment you have them, from the web, your phone or an email forward.\",\n    points: [\"Paste a link to unfurl it\", \"Forward email to your inbox address\"],\n    visual: <CaptureVisual />,\n  },\n  {\n    title: \"Turn it into a plan\",\n    description: \"Drag items onto a board, set an owner and a date, and watch the roadmap assemble itself.\",\n    points: [\"Board, list and timeline views\", \"Owners and due dates on everything\"],\n    visual: <OrganizeVisual />,\n  },\n  {\n    title: \"Automate the busywork\",\n    description: \"Describe what should happen and when. Reminders, handoffs and status updates run on their own.\",\n    points: [\"No code, no scripts\", \"Works with the tools you already use\"],\n    visual: <AutomateVisual />,\n  },\n  {\n    title: \"Share with exactly who needs it\",\n    description: \"Invite teammates or send a link. Permissions are per item, and you can see who looked at what.\",\n    points: [\"View, comment or edit\", \"Revoke access in one click\"],\n    visual: <ShareVisual />,\n  },\n]\n\nfunction Features5({\n  eyebrow = \"How it works\",\n  title = \"From a loose idea to a shipped plan.\",\n  description = \"Four steps, one workspace. Scroll to see each one.\",\n  steps = defaultSteps,\n  className,\n  ...props\n}: Features5Props) {\n  const [active, setActive] = React.useState(0)\n  const refs = React.useRef<(HTMLLIElement | null)[]>([])\n  const reduce = useReducedMotion()\n\n  React.useEffect(() => {\n    if (typeof IntersectionObserver === \"undefined\") return\n    const nodes = refs.current.filter((n): n is HTMLLIElement => !!n)\n    // A step becomes active when it crosses the middle band of the viewport.\n    const observer = new IntersectionObserver(\n      (entries) => {\n        for (const entry of entries) {\n          if (entry.isIntersecting) setActive(Number((entry.target as HTMLElement).dataset.index))\n        }\n      },\n      { rootMargin: \"-45% 0px -45% 0px\" }\n    )\n    nodes.forEach((n) => observer.observe(n))\n    return () => observer.disconnect()\n  }, [steps.length])\n\n  return (\n    <section data-slot=\"features-5\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"max-w-2xl\">\n        <p className=\"text-muted-foreground text-sm font-medium\">{eyebrow}</p>\n        <h2 className=\"mt-3 text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n        <p className=\"text-muted-foreground mt-4 text-lg text-pretty\">{description}</p>\n      </div>\n\n      <div className=\"mt-14 grid gap-x-16 lg:mt-20 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.05fr)]\">\n        <ol className=\"min-w-0 space-y-14 lg:space-y-0\">\n          {steps.map((step, i) => (\n            <li\n              key={step.title}\n              ref={(n) => {\n                refs.current[i] = n\n              }}\n              data-index={i}\n              data-active={active === i}\n              className=\"group/step relative lg:flex lg:min-h-[26rem] lg:items-center\"\n            >\n              <div className=\"lg:border-s lg:ps-10\">\n                <span\n                  aria-hidden=\"true\"\n                  className=\"bg-foreground absolute top-[18%] bottom-[18%] start-0 hidden w-0.5 origin-top scale-y-0 rounded-full transition-transform duration-500 ease-out group-data-[active=true]/step:scale-y-100 motion-reduce:transition-none lg:block\"\n                />\n                <p className=\"text-muted-foreground font-mono text-sm tabular-nums\">{String(i + 1).padStart(2, \"0\")}</p>\n                <h3 className=\"mt-2 text-2xl font-semibold tracking-[-0.025em] text-balance lg:text-3xl lg:text-muted-foreground lg:transition-colors lg:duration-300 lg:group-data-[active=true]/step:text-foreground\">\n                  {step.title}\n                </h3>\n                <p className=\"text-muted-foreground mt-3 max-w-md text-base leading-relaxed text-pretty\">{step.description}</p>\n                {step.points && (\n                  <ul className=\"mt-5 space-y-2 text-sm\">\n                    {step.points.map((p) => (\n                      <li key={p} className=\"flex items-start gap-2.5\">\n                        <Check className=\"mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n                        {p}\n                      </li>\n                    ))}\n                  </ul>\n                )}\n              </div>\n              {/* Phones and tablets: the picture sits right under its step. */}\n              <div aria-hidden={step.image ? undefined : true} className=\"bg-muted/40 mt-8 flex justify-center rounded-3xl border p-5 sm:p-8 lg:hidden\">{step.image ? <Media media={step.image} alt={step.imageAlt} fit=\"contain\" className=\"w-full rounded-xl\" /> : step.visual}</div>\n            </li>\n          ))}\n        </ol>\n\n        <div className=\"hidden lg:block\" aria-hidden={steps[active]?.image ? undefined : true}>\n          <div className=\"sticky top-24 flex h-[28rem] items-center justify-center overflow-hidden rounded-3xl border bg-muted/40 p-10\">\n            <div className=\"absolute inset-0 bg-[radial-gradient(color-mix(in_oklch,var(--foreground)_14%,transparent)_1px,transparent_1px)] [background-size:18px_18px] [mask-image:radial-gradient(70%_70%_at_50%_50%,black,transparent)]\" />\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <motion.div\n                key={active}\n                className=\"relative flex w-full justify-center\"\n                initial={reduce ? false : { opacity: 0, y: 14, scale: 0.97, filter: \"blur(6px)\" }}\n                animate={{ opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" }}\n                exit={reduce ? undefined : { opacity: 0, y: -10, scale: 0.98, filter: \"blur(6px)\" }}\n                transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1] }}\n              >\n                {steps[active]?.image ? <Media media={steps[active].image} alt={steps[active].imageAlt} fit=\"contain\" className=\"w-full rounded-xl\" /> : steps[active]?.visual}\n              </motion.div>\n            </AnimatePresence>\n          </div>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Features5, type Features5Props, type Features5Step }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/features-5/features-5.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "features",
      "tour",
      "sticky",
      "scroll",
      "steps",
      "how it works"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A how-it-works section that shows the product as you read. Pass steps=[{ title, description, points?, visual }] where visual is any ReactNode.",
      "whenToUse": [
        "Explaining a workflow in three to five steps",
        "Products where each step has a distinct screen"
      ],
      "whenNotToUse": [
        "Long feature lists (use features-2)",
        "Pages without room to scroll"
      ],
      "composesWith": [
        "hero-7",
        "logo-cloud-1",
        "testimonials-1",
        "cta-1"
      ],
      "a11y": [
        {
          "keys": "Scroll",
          "action": "The picture is decorative and hidden from assistive technology, so every step must state what it shows in text"
        }
      ],
      "customization": [
        "steps: { title, description, points?, visual }[]",
        "eyebrow, title, description props"
      ]
    },
    "examples": [
      "features-5-demo",
      "features-5-custom"
    ],
    "url": "https://ui.ballmac.com/components/features-5"
  },
  "categories": [
    "blocks",
    "features"
  ],
  "type": "registry:block"
}