{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "plan-builder",
  "title": "Plan Builder",
  "author": "@fma-ds/ds",
  "description": "Build what the user is about to buy by choosing quantities, with the price in real time: a custom plan with several resources, or a quantity of one item (extra notes, credits).\n\n- **`PlanBuilder`**: lays out the resources and its `summary` (an `OrderSummary`: a sticky card on large screens, a bar fixed to the bottom on smaller ones). `isCalculating` turns every `PlanPrice` (a price formatted by the app) into a skeleton while the app recalculates.\n- **`PlanResource`**: a card with `icon`, `title`, `description` and a controlled `Slider` named by `label` (`value`/`onChange`, `minValue`/`maxValue`/`step`, `marks`, `formatValue`, `limit` for stock or quota left, `isDisabled`). A `hint` under the slider says why it stops or is disabled, and an optional `included` accordion lists what comes with it.\n- **`PlanResourceAddon`**: an add-on as a child of the resource — a switch with label and description.\n\nThe price, debouncing, limits and notices stay in the app.\n\nBuilt-in text is in English; translate it with `i18n.labels`.",
  "dependencies": [
    "cn@^0.5.0"
  ],
  "registryDependencies": [
    "@fma-ds/accordion",
    "@fma-ds/card",
    "@fma-ds/icon-tile",
    "@fma-ds/skeleton",
    "@fma-ds/slider",
    "@fma-ds/switch"
  ],
  "files": [
    {
      "path": "src/blocks/plan-builder/plan-builder.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport * as React from \"react\";\nimport {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger,\n} from \"@/core/accordion/accordion\";\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@/core/card/card\";\nimport { IconTile } from \"@/core/icon-tile/icon-tile\";\nimport { Skeleton } from \"@/core/skeleton/skeleton\";\nimport { Slider, type SliderProps } from \"@/core/slider/slider\";\nimport { Switch } from \"@/core/switch/switch\";\n\ntype PlanBuilderLabels = {\n  /** The trigger of a resource's \"what's included\" accordion. */\n  included: string;\n};\n\ntype PlanBuilderI18n = { labels?: Partial<PlanBuilderLabels> };\n\nconst defaultPlanBuilderLabels: PlanBuilderLabels = { included: \"What's included\" };\n\nconst PlanBuilderContext = React.createContext<{\n  isCalculating: boolean;\n  labels: PlanBuilderLabels;\n}>({ isCalculating: false, labels: defaultPlanBuilderLabels });\n\ntype PlanBuilderProps = React.ComponentProps<\"div\"> & {\n  /** The side column: an `OrderSummary` with the lines, the total and the action. */\n  summary: React.ReactNode;\n  /** While the app recalculates the price; every `PlanPrice` shows a skeleton. */\n  isCalculating?: boolean;\n  i18n?: PlanBuilderI18n;\n};\n\n/**\n * Builds what the user is about to buy: the resources (`PlanResource`) on the\n * left and the `summary` on the right, which sticks on large screens and\n * becomes the `OrderSummary` bar fixed to the bottom on smaller ones (the\n * bottom padding leaves room for it). The price comes from the app.\n */\nfunction PlanBuilder({\n  summary,\n  isCalculating = false,\n  i18n,\n  className,\n  children,\n  ...props\n}: PlanBuilderProps) {\n  const included = i18n?.labels?.included ?? defaultPlanBuilderLabels.included;\n  const context = React.useMemo(\n    () => ({ isCalculating, labels: { included } }),\n    [isCalculating, included],\n  );\n\n  return (\n    <PlanBuilderContext.Provider value={context}>\n      <div\n        data-slot=\"plan-builder\"\n        className={cn(\n          \"grid gap-6 pb-28 lg:grid-cols-[minmax(0,1fr)_22rem] lg:items-start lg:pb-0\",\n          className,\n        )}\n        {...props}\n      >\n        <div data-slot=\"plan-builder-resources\" className=\"flex min-w-0 flex-col gap-4\">\n          {children}\n        </div>\n        <div data-slot=\"plan-builder-summary\" aria-busy={isCalculating || undefined}>\n          {summary}\n        </div>\n      </div>\n    </PlanBuilderContext.Provider>\n  );\n}\n\n/** A price formatted by the app; a skeleton while `PlanBuilder` is `isCalculating`. */\nfunction PlanPrice({ className, children, ...props }: React.ComponentProps<\"span\">) {\n  const { isCalculating } = React.useContext(PlanBuilderContext);\n  if (isCalculating) {\n    return <Skeleton data-slot=\"plan-price\" className={cn(\"inline-block h-4 w-16\", className)} />;\n  }\n  return (\n    <span data-slot=\"plan-price\" className={cn(\"tabular-nums\", className)} {...props}>\n      {children}\n    </span>\n  );\n}\n\ntype PlanResourceProps = Omit<SliderProps<number>, \"label\" | \"showValue\" | \"description\"> & {\n  icon?: React.ReactNode;\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  /** Names the slider: what is being chosen (\"Notes per month\"). */\n  label: React.ReactNode;\n  /** Under the slider: why it stops at `limit`, or why it's disabled. */\n  hint?: React.ReactNode;\n  /** The body of the \"what's included\" accordion; omitted, there's no accordion. */\n  included?: React.ReactNode;\n  /** Add-ons for this resource (`PlanResourceAddon`), between the slider and the accordion. */\n  children?: React.ReactNode;\n};\n\n/**\n * One resource of the plan: what it is, a slider for the quantity (with\n * `marks` for common amounts and an optional `limit`), add-ons and what's\n * included. Controlled like the `Slider` (`value`, `onChange`).\n */\nfunction PlanResource({\n  icon,\n  title,\n  description,\n  label,\n  hint,\n  included,\n  children,\n  className,\n  ...sliderProps\n}: PlanResourceProps) {\n  const { labels } = React.useContext(PlanBuilderContext);\n\n  return (\n    <Card data-slot=\"plan-resource\" className={className}>\n      <CardHeader className=\"flex items-start gap-3\">\n        {icon && (\n          <IconTile variant=\"soft\" className=\"shrink-0 text-primary\">\n            {icon}\n          </IconTile>\n        )}\n        <div className=\"flex min-w-0 flex-col gap-1\">\n          <CardTitle>{title}</CardTitle>\n          {description && <CardDescription>{description}</CardDescription>}\n        </div>\n      </CardHeader>\n      <CardContent className=\"flex flex-col gap-4\">\n        <Slider\n          {...sliderProps}\n          label={label}\n          showValue\n          description={hint}\n          className=\"**:data-[slot=slider-value]:text-base **:data-[slot=slider-value]:font-semibold **:data-[slot=slider-value]:text-foreground\"\n        />\n        {children}\n        {included && (\n          <Accordion className=\"rounded-lg border px-3\">\n            <AccordionItem>\n              <AccordionTrigger>{labels.included}</AccordionTrigger>\n              <AccordionContent className=\"text-muted-foreground\">{included}</AccordionContent>\n            </AccordionItem>\n          </Accordion>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n\n/** An add-on of a resource (a module, a validator): a switch with its label and description. */\nfunction PlanResourceAddon({\n  label,\n  description,\n  isSelected,\n  defaultSelected,\n  onChange,\n  isDisabled,\n  className,\n}: {\n  label: React.ReactNode;\n  description?: React.ReactNode;\n  isSelected?: boolean;\n  defaultSelected?: boolean;\n  onChange?: (isSelected: boolean) => void;\n  isDisabled?: boolean;\n  className?: string;\n}) {\n  const id = React.useId();\n\n  return (\n    <div\n      data-slot=\"plan-resource-addon\"\n      className={cn(\"flex items-start gap-3 rounded-lg border p-3\", className)}\n    >\n      <Switch\n        id={id}\n        isSelected={isSelected}\n        defaultSelected={defaultSelected}\n        onChange={onChange}\n        isDisabled={isDisabled}\n        aria-describedby={description ? `${id}-description` : undefined}\n        className=\"mt-0.5\"\n      />\n      <span className=\"flex flex-col gap-0.5\">\n        <label htmlFor={id} className=\"font-medium\">\n          {label}\n        </label>\n        {description && (\n          <span id={`${id}-description`} className=\"text-muted-foreground\">\n            {description}\n          </span>\n        )}\n      </span>\n    </div>\n  );\n}\n\nexport type { PlanBuilderI18n, PlanBuilderLabels, PlanBuilderProps, PlanResourceProps };\nexport { PlanBuilder, PlanPrice, PlanResource, PlanResourceAddon };\n",
      "type": "registry:component",
      "target": "components/fma-ds/plan-builder/plan-builder.tsx"
    }
  ],
  "meta": {
    "status": "experimental",
    "source": "original",
    "license": "MIT",
    "tags": [
      "block",
      "checkout",
      "plan",
      "pricing",
      "slider",
      "quantity"
    ],
    "contentHash": "ff4c976d1e45"
  },
  "categories": [
    "blocks"
  ],
  "type": "registry:block"
}