{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ecommerce46",
  "title": "Product Finder Quiz",
  "description": "A finder that replaces a filter sidebar: three questions score every product against the traits the answers ask for, and the winner takes over the plate beside them with its photograph, its price, and the reasons it came out on top.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "lucide-react",
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "questionnaire"
  ],
  "files": [
    {
      "path": "registry/ecommerce46/ecommerce46.tsx",
      "content": "\"use client\";\n\nimport { RotateCcw } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { type FormEvent, useState } from \"react\";\nimport { Badge7 } from \"@/components/beste/component/badge7\";\nimport { Button12 } from \"@/components/beste/component/button12\";\nimport {\n  Questionnaire,\n  QuestionnaireActions,\n  QuestionnaireChoice,\n  QuestionnaireChoiceDescription,\n  QuestionnaireChoices,\n  QuestionnaireDescription,\n  QuestionnaireError,\n  QuestionnaireItem,\n  QuestionnaireNext,\n  QuestionnairePrevious,\n  QuestionnaireProgress,\n  QuestionnaireSubmit,\n  QuestionnaireTitle,\n} from \"@/components/ui/questionnaire\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Badge {\n  label: string;\n}\n\ninterface ActionButton {\n  label: string;\n  href: string;\n}\n\ninterface ProductImage {\n  src: string;\n  alt: string;\n}\n\ninterface FinderChoice {\n  value: string;\n  label: string;\n  description?: string;\n  /** Traits this answer asks for. A product scores one point per trait it has. */\n  traits?: string[];\n  /** Line printed in the result's reasons list when this answer is picked */\n  reason?: string;\n}\n\ninterface FinderQuestion {\n  /** Form field name. Must be unique across the finder. */\n  name: string;\n  title: string;\n  description?: string;\n  choices?: FinderChoice[];\n  /** Renders the choices as checkboxes instead of radios */\n  multiple?: boolean;\n}\n\ninterface Product {\n  name: string;\n  price: string;\n  summary: string;\n  image: ProductImage;\n  /** Matched against the traits the answers ask for */\n  traits?: string[];\n  button: ActionButton;\n}\n\ninterface Ecommerce46Labels {\n  previous?: string;\n  next?: string;\n  submit?: string;\n  restart?: string;\n  resultTitle?: string;\n  reasonsTitle?: string;\n}\n\ninterface Ecommerce46Props {\n  badge?: Badge;\n  heading?: string;\n  description?: string;\n  questions?: FinderQuestion[];\n  /** Prints a scoped shortcut on every choice and binds it while the question is active */\n  shortcuts?: \"letters\" | \"numbers\";\n  /** Scored against the answers; the highest score wins, ties go to the first. */\n  products?: Product[];\n  /** Shown beside the questions, before anything has been answered */\n  image?: ProductImage;\n  caption?: string;\n  labels?: Ecommerce46Labels;\n  className?: string;\n}\n\nexport const ecommerce46Demo: Ecommerce46Props = {\n  badge: { label: \"Find your bag\" },\n  heading: \"Three questions, one bag.\",\n  description:\n    \"Rather than filter twelve bags by six attributes, tell us how you travel. We name one bag and say exactly why it came out on top.\",\n  shortcuts: \"numbers\",\n  image: {\n    src: \"https://images.unsplash.com/photo-1760565030401-c37ce3432ad6?q=80&w=3869&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n    alt: \"A black backpack held up by its handle against a plain background\",\n  },\n  caption:\n    \"Twelve bags in the range, each one carried loaded before it was signed off. Only one of them is the answer to your particular week.\",\n  labels: {\n    submit: \"See my match\",\n    restart: \"Answer again\",\n    resultTitle: \"Your match\",\n    reasonsTitle: \"Why this one\",\n  },\n  questions: [\n    {\n      name: \"trip\",\n      title: \"How long are you usually away?\",\n      description: \"The most common trip, not the longest one you have ever taken.\",\n      choices: [\n        {\n          value: \"day\",\n          label: \"Out and back in a day\",\n          description: \"Laptop, notebook, and lunch.\",\n          traits: [\"compact\", \"laptop\"],\n          reason: \"Sized for a day out rather than a week away\",\n        },\n        {\n          value: \"weekend\",\n          label: \"Two or three nights\",\n          description: \"Enough clothes to not do laundry.\",\n          traits: [\"weekend\", \"expandable\"],\n          reason: \"Holds two or three nights without a second bag\",\n        },\n        {\n          value: \"week\",\n          label: \"A week or more\",\n          description: \"Packing cubes are involved.\",\n          traits: [\"large\", \"expandable\"],\n          reason: \"Deep main compartment for a week of packing\",\n        },\n      ],\n    },\n    {\n      name: \"carry\",\n      title: \"How do you want to carry it?\",\n      choices: [\n        {\n          value: \"back\",\n          label: \"On my back\",\n          traits: [\"straps\"],\n          reason: \"Padded harness that takes the weight off one shoulder\",\n        },\n        {\n          value: \"hand\",\n          label: \"In one hand\",\n          traits: [\"handle\", \"compact\"],\n          reason: \"Balanced handle, so it hangs level when it is full\",\n        },\n        {\n          value: \"wheels\",\n          label: \"On wheels\",\n          traits: [\"wheels\", \"large\"],\n          reason: \"Wheels and a locking handle for long terminals\",\n        },\n      ],\n    },\n    {\n      name: \"needs\",\n      title: \"Anything it has to survive?\",\n      description: \"Pick as many as apply.\",\n      multiple: true,\n      choices: [\n        {\n          value: \"rain\",\n          label: \"Rain, regularly\",\n          traits: [\"waterproof\"],\n          reason: \"Coated shell and covered zips for wet platforms\",\n        },\n        {\n          value: \"overhead\",\n          label: \"Overhead lockers\",\n          traits: [\"cabin\"],\n          reason: \"Within cabin size on every airline we checked\",\n        },\n        {\n          value: \"kit\",\n          label: \"A camera or tools\",\n          traits: [\"padded\"],\n          reason: \"Padded divider that stays put when the bag is half empty\",\n        },\n      ],\n    },\n  ],\n  products: [\n    {\n      name: \"Halden 18 Daypack\",\n      price: \"$180\",\n      summary:\n        \"An eighteen litre pack that keeps its shape empty, with a laptop sleeve that sits off the floor.\",\n      image: {\n        src: \"https://images.unsplash.com/photo-1760564877882-9b7c9f4a3e9b?q=80&w=3869&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n        alt: \"Charcoal daypack standing upright against a plain wall\",\n      },\n      traits: [\"compact\", \"laptop\", \"straps\", \"waterproof\", \"padded\", \"cabin\"],\n      button: { label: \"See the Halden 18\", href: \"https://beste.co\" },\n    },\n    {\n      name: \"Norr Weekender\",\n      price: \"$320\",\n      summary:\n        \"A flat-packing weekend bag that expands by six litres when the trip runs long, carried by hand or on the back.\",\n      image: {\n        src: \"https://images.unsplash.com/photo-1760564877791-50863c5773da?q=80&w=3869&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n        alt: \"Black weekend bag with tan leather straps resting on a rock\",\n      },\n      traits: [\"weekend\", \"expandable\", \"handle\", \"straps\", \"cabin\", \"padded\"],\n      button: { label: \"See the Norr Weekender\", href: \"https://beste.co\" },\n    },\n    {\n      name: \"Vante 65 Roller\",\n      price: \"$460\",\n      summary:\n        \"A sixty-five litre case on sealed wheels, with a compression panel that keeps a week of clothes from moving.\",\n      image: {\n        src: \"https://images.unsplash.com/photo-1760565031198-00e39da3d9cb?q=80&w=3869&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D\",\n        alt: \"Hard-shell rolling suitcase photographed from the side\",\n      },\n      traits: [\"large\", \"wheels\", \"expandable\", \"waterproof\"],\n      button: { label: \"See the Vante 65\", href: \"https://beste.co\" },\n    },\n  ],\n};\n\nexport function Ecommerce46({\n  badge,\n  heading,\n  description,\n  questions = [],\n  shortcuts,\n  products = [],\n  image,\n  caption,\n  labels = {},\n  className,\n}: Ecommerce46Props) {\n  const {\n    previous: previousLabel,\n    next: nextLabel,\n    submit: submitLabel,\n    restart: restartLabel,\n    resultTitle,\n    reasonsTitle,\n  } = labels;\n\n  const [result, setResult] = useState<{ product: Product; reasons: string[] } | null>(null);\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    const data = new FormData(event.currentTarget);\n\n    const picked: FinderChoice[] = [];\n    for (const question of questions) {\n      const values = data.getAll(question.name).map((entry) => String(entry));\n      for (const value of values) {\n        const choice = question.choices?.find((entry) => entry.value === value);\n        if (choice) picked.push(choice);\n      }\n    }\n\n    const wanted = picked.flatMap((choice) => choice.traits ?? []);\n    let best: Product | undefined;\n    let bestScore = -1;\n    for (const product of products) {\n      const score = wanted.filter((trait) => product.traits?.includes(trait)).length;\n      if (score > bestScore) {\n        best = product;\n        bestScore = score;\n      }\n    }\n\n    if (!best) return;\n    const winner = best;\n\n    // Only the answers the winning product actually satisfies are worth printing.\n    const reasons = picked\n      .filter((choice) => (choice.traits ?? []).some((trait) => winner.traits?.includes(trait)))\n      .map((choice) => choice.reason)\n      .filter((reason): reason is string => Boolean(reason));\n\n    setResult({ product: winner, reasons });\n  };\n\n  const restart = () => setResult(null);\n\n  const shownImage = result?.product.image ?? image;\n\n  return (\n    <section className={cn(\"w-full py-16 md:py-24\", className)}>\n      <div className=\"mx-auto max-w-7xl px-4 md:px-6\">\n        <div className=\"grid gap-8 md:grid-cols-[1.2fr_1fr] md:items-end md:gap-16\">\n          <div>\n            {badge && <Badge7 label={badge.label} />}\n            {heading && (\n              <h2 className=\"mt-6 max-w-2xl text-balance text-3xl font-bold leading-tight tracking-tight text-foreground md:text-4xl lg:text-5xl\">\n                {heading}\n              </h2>\n            )}\n          </div>\n          {description && (\n            <p className=\"max-w-md text-lg leading-relaxed text-muted-foreground md:text-xl\">\n              {description}\n            </p>\n          )}\n        </div>\n\n        <div className=\"mt-12 grid gap-10 md:mt-16 lg:grid-cols-2 lg:gap-16\">\n          <div>\n            <div className=\"relative h-96 overflow-hidden rounded-md bg-muted lg:h-[34rem]\">\n              {shownImage && (\n                <img\n                  className=\"absolute inset-0 size-full object-cover\"\n                  src={shownImage.src}\n                  alt={shownImage.alt}\n                />\n              )}\n            </div>\n            {!result && caption && (\n              <p className=\"mt-6 text-lg leading-relaxed text-muted-foreground\">\n                {caption}\n              </p>\n            )}\n            {result && (\n              <div className=\"mt-6 flex flex-wrap items-baseline justify-between gap-4\">\n                <p className=\"text-xl font-bold tracking-tight text-foreground md:text-2xl\">\n                  {result.product.name}\n                </p>\n                <p className=\"text-xl font-bold tabular-nums text-foreground md:text-2xl\">\n                  {result.product.price}\n                </p>\n              </div>\n            )}\n          </div>\n\n          <div>\n            {result ? (\n              <>\n                {resultTitle && <Badge7 label={resultTitle} />}\n                <p className=\"mt-6 text-xl leading-relaxed text-foreground md:text-2xl\">\n                  {result.product.summary}\n                </p>\n\n                {result.reasons.length > 0 && (\n                  <div className=\"mt-8\">\n                    {reasonsTitle && (\n                      <p className=\"text-lg font-bold tracking-tight text-foreground\">\n                        {reasonsTitle}\n                      </p>\n                    )}\n                    <ul className=\"mt-4 flex flex-col\">\n                      {result.reasons.map((reason, index) => (\n                        <li\n                          key={index}\n                          className=\"border-t border-current/10 py-4 text-lg leading-relaxed text-muted-foreground\"\n                        >\n                          {reason}\n                        </li>\n                      ))}\n                    </ul>\n                  </div>\n                )}\n\n                <div className=\"mt-8 flex flex-wrap items-center gap-4\">\n                  <Button12 asChild label={result.product.button.label}>\n                    <Link href={result.product.button.href} />\n                  </Button12>\n                  <Button12\n                    label={restartLabel ?? \"Answer again\"}\n                    tone=\"outline\"\n                    icon={RotateCcw}\n                    onClick={restart}\n                  />\n                </div>\n              </>\n            ) : (\n              <Questionnaire\n                defaultItem={questions[0]?.name}\n                shortcuts={shortcuts}\n                onSubmit={handleSubmit}\n                className=\"gap-6\"\n              >\n                {questions.length > 0 && <QuestionnaireProgress className=\"text-base\" />}\n\n                {questions.map((question, index) => (\n                  <QuestionnaireItem\n                    key={index}\n                    name={question.name}\n                    required={!question.multiple}\n                    multiple={question.multiple}\n                  >\n                    <QuestionnaireTitle className=\"text-xl font-bold tracking-tight text-foreground md:text-2xl\">\n                      {question.title}\n                    </QuestionnaireTitle>\n                    {question.description && (\n                      <QuestionnaireDescription className=\"text-base text-muted-foreground md:text-lg\">\n                        {question.description}\n                      </QuestionnaireDescription>\n                    )}\n                    <QuestionnaireChoices>\n                      {question.choices?.map((choice, choiceIndex) => (\n                        <QuestionnaireChoice\n                          key={choiceIndex}\n                          value={choice.value}\n                          className=\"rounded-md p-4 text-base data-checked:border-foreground\"\n                        >\n                          <span className=\"font-bold text-foreground\">{choice.label}</span>\n                          {choice.description && (\n                            <QuestionnaireChoiceDescription className=\"text-base\">\n                              {choice.description}\n                            </QuestionnaireChoiceDescription>\n                          )}\n                        </QuestionnaireChoice>\n                      ))}\n                    </QuestionnaireChoices>\n                    <QuestionnaireError className=\"text-base\" />\n                  </QuestionnaireItem>\n                ))}\n\n                <QuestionnaireActions>\n                  <QuestionnairePrevious className=\"cursor-pointer rounded-full text-base\">\n                    {previousLabel}\n                  </QuestionnairePrevious>\n                  <QuestionnaireNext className=\"cursor-pointer rounded-full bg-foreground text-base text-background hover:bg-foreground/90\">\n                    {nextLabel}\n                  </QuestionnaireNext>\n                  <QuestionnaireSubmit className=\"cursor-pointer rounded-full bg-foreground text-base text-background hover:bg-foreground/90\">\n                    {submitLabel}\n                  </QuestionnaireSubmit>\n                </QuestionnaireActions>\n              </Questionnaire>\n            )}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/block/ecommerce46.tsx"
    },
    {
      "path": "registry-components/badge7/badge7.tsx",
      "content": "import { cn } from \"@/lib/utils\";\n\n/** Allowed wrapper styles, a fixed set, not arbitrary characters. */\ntype Badge7Bracket = \"round\" | \"square\" | \"curly\" | \"angle\" | \"none\";\n\nconst BRACKETS: Record<Badge7Bracket, [open: string, close: string]> = {\n  round: [\"(\", \")\"],\n  square: [\"[\", \"]\"],\n  curly: [\"{\", \"}\"],\n  angle: [\"<\", \">\"],\n  none: [\"\", \"\"],\n};\n\ntype Tone = \"muted\" | \"foreground\" | \"primary\";\n\ninterface Badge7Props {\n  /** Eyebrow label (rendered uppercase) */\n  label: string;\n  /** Which characters wrap the label. Limited to the BRACKETS set. */\n  bracket?: Badge7Bracket;\n  /** Label color */\n  tone?: Tone;\n  /** Render rotated for vertical edge placement (e.g. along a hero side) */\n  vertical?: boolean;\n  /** Additional classes merged onto the root */\n  className?: string;\n}\n\nconst toneStyles: Record<Tone, string> = {\n  muted: \"text-muted-foreground\",\n  foreground: \"text-foreground\",\n  primary: \"text-primary\",\n};\n\nexport const badge7Demo: Badge7Props = {\n  label: \"About\",\n};\n\nexport function Badge7({\n  label,\n  bracket = \"round\",\n  tone = \"muted\",\n  vertical = false,\n  className,\n}: Badge7Props) {\n  const [open, close] = BRACKETS[bracket];\n\n  return (\n    <span\n      className={cn(\n        \"font-mono text-base uppercase tracking-[0.25em]\",\n        toneStyles[tone],\n        vertical ? \"rotate-180 [writing-mode:vertical-rl]\" : \"inline-block\",\n        className\n      )}\n    >\n      {`${open}${label}${close}`}\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/component/badge7.tsx"
    },
    {
      "path": "registry-components/button12/button12.tsx",
      "content": "\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ArrowUpRight, type LucideIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tone = \"dark\" | \"primary\" | \"outline\";\n\ninterface Button12Props {\n  /** Button label (rendered uppercase) */\n  label: string;\n  /**\n   * Compose the rendered element shadcn-style (radix asChild): your element\n   * (e.g. a router Link) becomes the root and the button content is injected\n   * as its children. Pass the element without children of its own.\n   */\n  asChild?: boolean;\n  /** The element to render when `asChild` is set (e.g. your framework's Link) */\n  children?: React.ReactElement;\n  /**\n   * Fallback link: renders a plain `<a>`. Prefer `asChild` with your\n   * framework's Link component for client-side navigation.\n   */\n  href?: string;\n  /** Seal icon (defaults to ArrowUpRight) */\n  icon?: LucideIcon;\n  /** Surface tone: solid dark (default), solid primary, or bordered outline */\n  tone?: Tone;\n  /** Click handler (used when there is no href and no asChild) */\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n  /** Additional classes merged onto the button */\n  className?: string;\n}\n\nconst toneStyles: Record<Tone, { pill: string; seal: string }> = {\n  dark: {\n    pill: \"bg-foreground text-background hover:bg-foreground/90\",\n    seal: \"bg-background text-foreground\",\n  },\n  primary: {\n    pill: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n    seal: \"bg-primary-foreground text-primary\",\n  },\n  outline: {\n    pill: \"border bg-background text-foreground hover:bg-muted\",\n    seal: \"bg-foreground text-background\",\n  },\n};\n\nexport const button12Demo: Button12Props = {\n  label: \"Book an intro call\",\n};\n\nexport function Button12({\n  label,\n  asChild = false,\n  children,\n  href,\n  icon: Icon = ArrowUpRight,\n  tone = \"dark\",\n  onClick,\n  className,\n}: Button12Props) {\n  const styles = toneStyles[tone];\n\n  const classes = cn(\n    \"group/button12 inline-flex w-fit cursor-pointer items-center gap-3 rounded-full py-2 pl-8 pr-2 text-base font-bold uppercase tracking-wider transition-colors\",\n    styles.pill,\n    className\n  );\n\n  const inner = (\n    <>\n      {/* Label slides out the bottom while a copy drops in from the top */}\n      <span className=\"relative block overflow-hidden\">\n        <span className=\"block transition-transform duration-300 ease-out motion-safe:group-hover/button12:translate-y-full\">\n          {label}\n        </span>\n        <span\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 block -translate-y-full transition-transform duration-300 ease-out motion-safe:group-hover/button12:translate-y-0\"\n        >\n          {label}\n        </span>\n      </span>\n\n      {/* Seal: icon exits the top-right corner as a copy enters from bottom-left */}\n      <span\n        className={cn(\n          \"relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-full\",\n          styles.seal\n        )}\n      >\n        <Icon className=\"size-4 transition-transform duration-300 ease-out motion-safe:group-hover/button12:translate-x-[160%] motion-safe:group-hover/button12:-translate-y-[160%]\" />\n        <Icon\n          aria-hidden=\"true\"\n          className=\"absolute size-4 -translate-x-[160%] translate-y-[160%] transition-transform duration-300 ease-out motion-safe:group-hover/button12:translate-x-0 motion-safe:group-hover/button12:translate-y-0\"\n        />\n      </span>\n    </>\n  );\n\n  if (asChild && React.isValidElement(children)) {\n    return (\n      <Slot className={classes}>\n        {React.cloneElement(children, undefined, inner)}\n      </Slot>\n    );\n  }\n\n  if (href) {\n    return (\n      <a href={href} className={classes}>\n        {inner}\n      </a>\n    );\n  }\n\n  return (\n    <button type=\"button\" onClick={onClick} className={classes}>\n      {inner}\n    </button>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/component/button12.tsx"
    }
  ],
  "type": "registry:block"
}