{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "product12",
  "title": "Live Made To Order Configurator",
  "description": "A made-to-order configurator that answers as you go: every option takes over the plate beside it, writes its own line into a running specification, and moves a total that is visible from the first question rather than revealed at the end.",
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "lucide-react",
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "questionnaire"
  ],
  "files": [
    {
      "path": "registry/product12/product12.tsx",
      "content": "\"use client\";\n\nimport { RotateCcw } from \"lucide-react\";\nimport Link from \"next/link\";\nimport { type ChangeEvent, 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 ConfigImage {\n  src: string;\n  alt: string;\n}\n\ninterface ConfigChoice {\n  value: string;\n  label: string;\n  description?: string;\n  /** Takes over the plate while this option is the one selected */\n  image?: ConfigImage;\n  caption?: string;\n  /** How this option reads in the specification */\n  spec: string;\n  /** Added to the base price. Negative is allowed. */\n  price?: number;\n}\n\ninterface ConfigStep {\n  /** Form field name. Must be unique across the configurator. */\n  name: string;\n  title: string;\n  description?: string;\n  /** Row label in the specification */\n  specLabel: string;\n  choices?: ConfigChoice[];\n}\n\ninterface Closing {\n  title: string;\n  description: string;\n}\n\ninterface Product12Labels {\n  previous?: string;\n  next?: string;\n  submit?: string;\n  restart?: string;\n  specTitle?: string;\n  priceLabel?: string;\n  pending?: string;\n}\n\ninterface Product12Props {\n  badge?: Badge;\n  heading?: string;\n  description?: string;\n  steps?: ConfigStep[];\n  /** Prints a scoped shortcut on every choice and binds it while the step is active */\n  shortcuts?: \"letters\" | \"numbers\";\n  /** Every option's price is added to this */\n  basePrice?: number;\n  /** Prefix printed in front of the total */\n  currency?: string;\n  /** Plate shown before anything has been chosen */\n  image?: ConfigImage;\n  caption?: string;\n  /** Replaces the questions once the specification is sent */\n  closing?: Closing;\n  button?: ActionButton;\n  labels?: Product12Labels;\n  className?: string;\n}\n\n/** Thousands separator without toLocaleString, which drifts between server and client. */\nfunction formatPrice(amount: number, currency: string) {\n  return `${currency}${String(amount).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\")}`;\n}\n\nexport const product12Demo: Product12Props = {\n  badge: { label: \"Made to order\" },\n  heading: \"Specify the chair while you look at it.\",\n  description:\n    \"Every answer changes the plate on the left and the line it writes into the specification. Nothing is hidden until the end.\",\n  shortcuts: \"numbers\",\n  basePrice: 480,\n  currency: \"£\",\n  image: {\n    src: \"https://images.unsplash.com/photo-1503602642458-232111445657?w=1200&h=1400&fit=crop\",\n    alt: \"Wooden chair standing alone in a bright room\",\n  },\n  caption: \"The Aro chair, built in the workshop in batches of twelve.\",\n  labels: {\n    submit: \"Send this specification\",\n    restart: \"Specify another\",\n    specTitle: \"Specification\",\n    priceLabel: \"Total, before delivery\",\n    pending: \"Not chosen yet\",\n  },\n  steps: [\n    {\n      name: \"frame\",\n      title: \"Which frame?\",\n      description: \"The part that decides how heavy the chair looks in a room.\",\n      specLabel: \"Frame\",\n      choices: [\n        {\n          value: \"oak\",\n          label: \"Oiled oak\",\n          description: \"Pale, open grain, and it lightens over the years.\",\n          spec: \"Oiled European oak\",\n          price: 0,\n          image: {\n            src: \"https://images.unsplash.com/photo-1503602642458-232111445657?w=1200&h=1400&fit=crop\",\n            alt: \"Pale oak chair against a white wall\",\n          },\n          caption: \"Oak is the standing frame, and the one most people keep.\",\n        },\n        {\n          value: \"ash\",\n          label: \"Blackened ash\",\n          description: \"Closed grain, near black, and it hides a working room.\",\n          spec: \"Blackened ash\",\n          price: 60,\n          image: {\n            src: \"https://images.unsplash.com/photo-1592078615290-033ee584e267?w=1200&h=1400&fit=crop\",\n            alt: \"Dark timber chair beside a window\",\n          },\n          caption: \"Ash is dyed rather than painted, so the grain still reads up close.\",\n        },\n        {\n          value: \"steel\",\n          label: \"Waxed steel\",\n          description: \"Thinner in section, heavier in the hand, and it marks.\",\n          spec: \"Waxed steel, 16mm section\",\n          price: 140,\n          image: {\n            src: \"https://images.unsplash.com/photo-1540574163026-643ea20ade25?w=1200&h=1400&fit=crop\",\n            alt: \"Metal framed chair on a plain floor\",\n          },\n          caption: \"Steel frames are welded here and waxed by hand, so no two age alike.\",\n        },\n      ],\n    },\n    {\n      name: \"seat\",\n      title: \"What sits on it?\",\n      specLabel: \"Seat\",\n      choices: [\n        {\n          value: \"canvas\",\n          label: \"Cotton canvas\",\n          description: \"Washable, replaceable, and it softens in a season.\",\n          spec: \"16oz cotton canvas, natural\",\n          price: 0,\n        },\n        {\n          value: \"leather\",\n          label: \"Vegetable-tanned leather\",\n          description: \"Stiff for a month, then it takes your shape and keeps it.\",\n          spec: \"Vegetable-tanned leather, 3mm\",\n          price: 180,\n        },\n        {\n          value: \"cane\",\n          label: \"Hand-woven cane\",\n          description: \"Lighter to carry, and it lets a warm room breathe.\",\n          spec: \"Hand-woven cane panel\",\n          price: 120,\n        },\n      ],\n    },\n    {\n      name: \"finish\",\n      title: \"How should it be finished?\",\n      description: \"This decides how it is looked after, not how it looks.\",\n      specLabel: \"Finish\",\n      choices: [\n        {\n          value: \"oiled\",\n          label: \"Oiled\",\n          description: \"Re-oil it once a year. Scratches disappear into it.\",\n          spec: \"Hard-wax oil, matte\",\n          price: 0,\n        },\n        {\n          value: \"lacquered\",\n          label: \"Lacquered\",\n          description: \"Wipes clean, survives a kitchen, and cannot be patched.\",\n          spec: \"Water-based lacquer, satin\",\n          price: 45,\n        },\n      ],\n    },\n  ],\n  closing: {\n    title: \"Specification sent\",\n    description:\n      \"The workshop replies inside three working days with a build slot and a delivery window. Nothing is charged until you confirm both.\",\n  },\n  button: { label: \"Book a workshop visit\", href: \"https://beste.co\" },\n};\n\nexport function Product12({\n  badge,\n  heading,\n  description,\n  steps = [],\n  shortcuts,\n  basePrice = 0,\n  currency = \"$\",\n  image,\n  caption,\n  closing,\n  button,\n  labels = {},\n  className,\n}: Product12Props) {\n  const {\n    previous: previousLabel,\n    next: nextLabel,\n    submit: submitLabel,\n    restart: restartLabel,\n    specTitle,\n    priceLabel,\n    pending,\n  } = labels;\n\n  const [picked, setPicked] = useState<Record<string, string>>({});\n  const [sent, setSent] = useState(false);\n\n  const chosen = (step: ConfigStep) =>\n    step.choices?.find((choice) => choice.value === picked[step.name]);\n\n  /** The last chosen option that carries a plate is the one on screen. */\n  const platedStep = [...steps].reverse().find((step) => chosen(step)?.image);\n  const plated = platedStep ? chosen(platedStep) : undefined;\n  const shownImage = plated?.image ?? image;\n  const shownCaption = plated?.caption ?? caption;\n\n  const total = steps.reduce((sum, step) => sum + (chosen(step)?.price ?? 0), basePrice);\n\n  const record = (step: ConfigStep, value: string) => (event: ChangeEvent<HTMLInputElement>) => {\n    if (!event.target.checked) return;\n    setPicked((previous) => ({ ...previous, [step.name]: value }));\n  };\n\n  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    setSent(true);\n  };\n\n  const restart = () => {\n    setPicked({});\n    setSent(false);\n  };\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 md: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            {shownCaption && (\n              <p className=\"mt-6 text-lg leading-relaxed text-muted-foreground\">\n                {shownCaption}\n              </p>\n            )}\n          </div>\n\n          <div>\n            {sent && closing ? (\n              <>\n                <p className=\"text-xl font-bold tracking-tight text-foreground md:text-2xl\">\n                  {closing.title}\n                </p>\n                <p className=\"mt-3 text-lg leading-relaxed text-muted-foreground\">\n                  {closing.description}\n                </p>\n              </>\n            ) : (\n              <Questionnaire\n                defaultItem={steps[0]?.name}\n                shortcuts={shortcuts}\n                onSubmit={handleSubmit}\n                className=\"gap-6\"\n              >\n                {steps.length > 0 && <QuestionnaireProgress className=\"text-base\" />}\n\n                {steps.map((step, index) => (\n                  <QuestionnaireItem key={index} name={step.name} required>\n                    <QuestionnaireTitle className=\"text-xl font-bold tracking-tight text-foreground md:text-2xl\">\n                      {step.title}\n                    </QuestionnaireTitle>\n                    {step.description && (\n                      <QuestionnaireDescription className=\"text-base text-muted-foreground md:text-lg\">\n                        {step.description}\n                      </QuestionnaireDescription>\n                    )}\n                    <QuestionnaireChoices>\n                      {step.choices?.map((choice, choiceIndex) => (\n                        <QuestionnaireChoice\n                          key={choiceIndex}\n                          value={choice.value}\n                          onChange={record(step, 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\n            {steps.length > 0 && (\n              <div className=\"mt-10\">\n                {specTitle && <Badge7 label={specTitle} />}\n\n                <dl className=\"mt-5 flex flex-col\">\n                  {steps.map((step, index) => {\n                    const option = chosen(step);\n                    return (\n                      <div\n                        key={index}\n                        className=\"grid gap-1 border-t border-current/10 py-4 sm:grid-cols-[minmax(0,8rem)_minmax(0,1fr)] sm:gap-6\"\n                      >\n                        <dt className=\"text-base text-muted-foreground md:text-lg\">\n                          {step.specLabel}\n                        </dt>\n                        <dd\n                          className={cn(\n                            \"text-base md:text-lg\",\n                            option\n                              ? \"font-bold text-foreground\"\n                              : \"text-muted-foreground\"\n                          )}\n                        >\n                          {option ? option.spec : (pending ?? \"Not chosen yet\")}\n                        </dd>\n                      </div>\n                    );\n                  })}\n                </dl>\n\n                <div className=\"mt-6 flex flex-wrap items-baseline justify-between gap-4 border-t border-current/10 pt-6\">\n                  <p className=\"text-base text-muted-foreground md:text-lg\">\n                    {priceLabel ?? \"Total\"}\n                  </p>\n                  <p className=\"text-2xl font-bold tabular-nums tracking-tight text-foreground md:text-3xl\">\n                    {formatPrice(total, currency)}\n                  </p>\n                </div>\n\n                {sent && (\n                  <div className=\"mt-8 flex flex-wrap items-center gap-4\">\n                    {button && (\n                      <Button12 asChild label={button.label}>\n                        <Link href={button.href} />\n                      </Button12>\n                    )}\n                    <Button12\n                      label={restartLabel ?? \"Specify another\"}\n                      tone=\"outline\"\n                      icon={RotateCcw}\n                      onClick={restart}\n                    />\n                  </div>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/beste/block/product12.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"
}