{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "choice-cards",
  "type": "registry:component",
  "title": "Choice Cards",
  "description": "One option chosen from a few, each shown as a picture of what it does.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/choice-cards.tsx",
      "type": "registry:component",
      "target": "@components/panelui/choice-cards.tsx",
      "content": "'use client';\n\n/**\n * ChoiceCards — one option chosen from a few, each shown as a picture of\n * what it does.\n *\n * ```tsx\n * <ChoiceCards defaultValue=\"rounded\" aria-label=\"Pointer\">\n *   <ChoiceCards.Item value=\"classic\">\n *     <ChoiceCards.Media><ClassicPointer /></ChoiceCards.Media>\n *     <ChoiceCards.Label>Classic</ChoiceCards.Label>\n *   </ChoiceCards.Item>\n *   …\n * </ChoiceCards>\n * ```\n *\n * ## Show the option, not its name\n *\n * A pointer style, an aspect ratio, a background: options that are seen\n * before they are understood. Each card leads with a picture of the option,\n * so the choice is made by looking, and the name under it confirms it.\n *\n * ## The chosen one comes forward\n *\n * The chosen card is ringed in the theme colour, a check settles into its\n * corner, and its picture grows a little, as if lifted towards the reader.\n * The others stay flat, so a row of cards still reads as one choice and one\n * set of alternatives.\n */\nimport { Radio } from '@base-ui/react/radio';\nimport { RadioGroup } from '@base-ui/react/radio-group';\nimport { createContext, useContext, useId, type ComponentProps, type CSSProperties, type ReactNode } from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type ChoiceCardsColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';\n\nconst SPRING = 'ease-[cubic-bezier(0.3,1.35,0.45,1)]';\n\nconst choiceCardsVariants = tv({\n  slots: {\n    root: 'grid w-full grid-cols-(--choice-columns) gap-2 data-disabled:opacity-50',\n    item: [\n      'group/choice relative flex min-w-0 cursor-pointer flex-col gap-1.5 rounded-[18px] p-1.5 text-start outline-none select-none',\n      'bg-muted/60 transition-[background-color,box-shadow] duration-200 hover:bg-muted',\n      // The ring stands clear of the card, so it never sits on the picture. It\n      // is an outline, whose gap is see-through on whatever surface holds it.\n      'outline-offset-2 data-checked:bg-muted data-checked:outline-2 data-checked:outline-solid data-checked:outline-(--choice)',\n      'focus-visible:ring-2 focus-visible:ring-ring',\n      'data-disabled:cursor-not-allowed data-disabled:opacity-50',\n    ],\n    media: [\n      'relative flex aspect-(--choice-aspect) w-full items-center justify-center overflow-hidden rounded-xl',\n      // A frame a step lighter than the card in either theme, with a hairline edge.\n      'bg-background/80 ring-1 ring-inset ring-foreground/[0.05] dark:bg-foreground/[0.06]',\n      'text-foreground/80 [&>*]:transition-transform [&>*]:duration-[420ms] [&>*]:ease-[cubic-bezier(0.3,1.35,0.45,1)]',\n      'group-data-checked/choice:text-foreground group-data-checked/choice:[&>*]:scale-[1.06] motion-reduce:[&>*]:transition-none',\n      '[&>img]:size-full [&>img]:object-cover',\n    ],\n    check: [\n      'pointer-events-none absolute end-1.5 top-1.5 z-10 flex size-5 items-center justify-center rounded-full bg-(--choice) text-(--choice-foreground)',\n      `scale-0 opacity-0 shadow-sm transition-[scale,opacity] duration-300 ${SPRING}`,\n      'group-data-checked/choice:scale-100 group-data-checked/choice:opacity-100 motion-reduce:transition-none',\n    ],\n    text: 'flex flex-col gap-0.5 px-1.5 pb-1',\n    label: 'truncate text-[13px] font-medium text-foreground/80 group-data-checked/choice:text-foreground',\n    description: 'text-xs leading-4 text-muted-foreground',\n  },\n  variants: {\n    color: {\n      primary: { root: '[--choice:var(--primary)] [--choice-foreground:var(--primary-foreground)]' },\n      info: { root: '[--choice:var(--info)] [--choice-foreground:white]' },\n      success: { root: '[--choice:var(--success)] [--choice-foreground:white]' },\n      warning: { root: '[--choice:var(--warning)] [--choice-foreground:black]' },\n      destructive: { root: '[--choice:var(--destructive)] [--choice-foreground:white]' },\n    },\n  },\n  defaultVariants: { color: 'primary' },\n});\n\ninterface ItemContextValue {\n  labelId: string;\n  descriptionId: string;\n}\n\nconst ItemContext = createContext<ItemContextValue | null>(null);\n\nexport interface ChoiceCardsProps extends Omit<RadioGroup.Props<string>, 'className' | 'style' | 'children'> {\n  /** How many cards stand in a row. Default: one per item, up to four. */\n  columns?: number;\n  /** The shape of every card's picture, as a CSS aspect ratio. Default `4 / 3`. */\n  aspect?: string;\n  /** The colour of the ring and the check on the chosen card. Default `primary`. */\n  color?: ChoiceCardsColor;\n  children?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n}\n\n/**\n * The cards, of which one is chosen. `value` and `onValueChange`, or\n * `defaultValue`, hold which; `name` submits it with a form.\n */\nfunction ChoiceCardsRoot({ columns, aspect = '4 / 3', color, className, style, children, ...props }: ChoiceCardsProps) {\n  const slots = choiceCardsVariants({ color });\n  const count = Array.isArray(children) ? children.filter(Boolean).length : 1;\n  const across = columns ?? Math.min(Math.max(count, 1), 4);\n  return (\n    <RadioGroup\n      data-slot=\"choice-cards\"\n      className={slots.root({ className })}\n      style={\n        {\n          ...style,\n          '--choice-columns': `repeat(${across}, minmax(0, 1fr))`,\n          '--choice-aspect': aspect,\n        } as CSSProperties\n      }\n      {...props}\n    >\n      {children}\n    </RadioGroup>\n  );\n}\nChoiceCardsRoot.displayName = 'ChoiceCards';\n\nexport interface ChoiceCardsItemProps extends Omit<Radio.Root.Props, 'className' | 'value'> {\n  /** What choosing this card sets the group's value to. */\n  value: string;\n  className?: string;\n}\n\n/** One option: a `ChoiceCards.Media` picture, a `Label`, and maybe a `Description`. */\nfunction ChoiceCardsItem({ className, children, ...props }: ChoiceCardsItemProps) {\n  const slots = choiceCardsVariants();\n  const id = useId();\n  const ids = { labelId: `${id}-label`, descriptionId: `${id}-description` };\n  return (\n    <ItemContext.Provider value={ids}>\n      <Radio.Root\n        data-slot=\"choice-cards-item\"\n        aria-labelledby={ids.labelId}\n        aria-describedby={ids.descriptionId}\n        className={slots.item({ className })}\n        {...props}\n      >\n        <span aria-hidden data-slot=\"choice-cards-check\" className={slots.check()}>\n          <svg\n            viewBox=\"0 0 12 12\"\n            className=\"size-3\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          >\n            <path d=\"M2.75 6.25 5 8.5l4.25-5\" />\n          </svg>\n        </span>\n        {children}\n      </Radio.Root>\n    </ItemContext.Provider>\n  );\n}\nChoiceCardsItem.displayName = 'ChoiceCards.Item';\n\nexport interface ChoiceCardsMediaProps extends ComponentProps<'span'> {}\n\n/** The picture of the option: a drawing, an icon or an image, centred in a rounded frame. */\nfunction ChoiceCardsMedia({ className, render, ...props }: ChoiceCardsMediaProps & RenderProp<'span'>) {\n  const slots = choiceCardsVariants();\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      { 'aria-hidden': true, 'data-slot': 'choice-cards-media', className: slots.media({ className }) },\n      props\n    ),\n  });\n}\nChoiceCardsMedia.displayName = 'ChoiceCards.Media';\n\n/** Gathers the label and description under the picture, with the card's padding. */\nfunction useItem(part: string) {\n  const item = useContext(ItemContext);\n  if (!item) throw new Error(`${part} must be used inside a <ChoiceCards.Item>.`);\n  return item;\n}\n\nexport interface ChoiceCardsLabelProps extends ComponentProps<'span'> {}\n\n/** The option's name, under its picture. It is what a screen reader calls the card. */\nfunction ChoiceCardsLabel({ className, render, ...props }: ChoiceCardsLabelProps & RenderProp<'span'>) {\n  const { labelId } = useItem('ChoiceCards.Label');\n  const slots = choiceCardsVariants();\n  const element = useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps({ id: labelId, 'data-slot': 'choice-cards-label', className: slots.label({ className }) }, props),\n  });\n  return (\n    <span data-slot=\"choice-cards-text\" className={slots.text()}>\n      {element}\n    </span>\n  );\n}\nChoiceCardsLabel.displayName = 'ChoiceCards.Label';\n\nexport interface ChoiceCardsDescriptionProps extends ComponentProps<'span'> {}\n\n/** A line under the label: what the option costs or does. */\nfunction ChoiceCardsDescription({ className, render, ...props }: ChoiceCardsDescriptionProps & RenderProp<'span'>) {\n  const { descriptionId } = useItem('ChoiceCards.Description');\n  const slots = choiceCardsVariants();\n  return useRender({\n    defaultTagName: 'span',\n    render,\n    props: mergeProps(\n      {\n        id: descriptionId,\n        'data-slot': 'choice-cards-description',\n        className: slots.description({ className: ['-mt-1.5 px-1.5 pb-1', className] }),\n      },\n      props\n    ),\n  });\n}\nChoiceCardsDescription.displayName = 'ChoiceCards.Description';\n\nexport const ChoiceCards = Object.assign(ChoiceCardsRoot, {\n  Item: ChoiceCardsItem,\n  Media: ChoiceCardsMedia,\n  Label: ChoiceCardsLabel,\n  Description: ChoiceCardsDescription,\n});\n\nexport { ChoiceCardsItem, ChoiceCardsMedia, ChoiceCardsLabel, ChoiceCardsDescription, choiceCardsVariants };\n"
    }
  ]
}
