{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "frame",
  "type": "registry:component",
  "title": "Frame",
  "description": "A panel of rows set into a tray, with the tray's top strip carrying the title.",
  "dependencies": [
    "@base-ui/react",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/frame.tsx",
      "type": "registry:component",
      "target": "@components/panelui/frame.tsx",
      "content": "'use client';\n\n/**\n * Frame — a widget shell: a panel of rows sitting in a tray, with the tray's\n * one exposed strip along the top carrying the title.\n *\n * ```tsx\n * <Frame>\n *   <Frame.Header>\n *     <Frame.Title>Deploys</Frame.Title>\n *     <Frame.Action>Last 24 hours</Frame.Action>\n *   </Frame.Header>\n *   <Frame.Panel>\n *     <Frame.Row>\n *       <Frame.Media><RocketIcon /></Frame.Media>\n *       <Frame.Content>\n *         <Frame.Title>main · a41c2e9</Frame.Title>\n *         <Frame.Description>Production · 2 minutes ago</Frame.Description>\n *       </Frame.Content>\n *       <Frame.Actions><Badge>Ready</Badge></Frame.Actions>\n *     </Frame.Row>\n *   </Frame.Panel>\n * </Frame>\n * ```\n *\n * ## Two surfaces, one exposed edge\n *\n * The panel sits flush against the shell's left, right and bottom, so the\n * shell reads as something the panel is set into rather than as a border\n * round it — and the strip left at the top is the header, which is why the\n * header needs no rule under it and no background of its own. The panel's\n * bottom corners are not drawn at all: the shell clips them to its own radius.\n *\n * ## Four ways to nest them\n *\n * `default` is the tray. `band` holds a bordered card in a thin muted band,\n * with the band's top strip naming what is inside — the frame for a widget\n * with one thing to show, a figure or a chart, rather than a list of rows.\n * `inset` floats the panel clear of the shell on all four sides, at the radius\n * that keeps the two curves concentric, and the band left round it is a recess\n * — darker than the panel in a light theme and a dark one alike — that\n * `Frame.Footer` puts its actions in. `plain` drops the shell, for a frame\n * inside something that already draws an edge, where a second one just inside\n * it reads as a double line.\n *\n * ## Rows divide themselves\n *\n * The panel draws the hairline between its children, so no row has to know\n * whether it is first. Under `band` it draws none: a figure and its chart are\n * one thing, not two rows. A row with `href` is a link and one with `onClick`\n * is a button, each with the hover and focus a pressable row should have, and\n * `chevron` marks one that leads somewhere.\n *\n * ## It renders on the server\n *\n * The variant reaches the parts through a data attribute and CSS rather than\n * through context, so a frame has no client JavaScript of its own.\n */\nimport type { ComponentProps, MouseEventHandler, 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\n/*\n * `inset` geometry. The panel's radius is the shell's less the shell's padding,\n * so the two curves are concentric — a panel radius that ignored the band\n * would leave a crescent of shell thicker at the corners than along the sides.\n */\nconst frameVariants = tv({\n  slots: {\n    root: 'group/frame flex flex-col text-foreground',\n    header: [\n      'grid grid-cols-[minmax(0,1fr)] items-center gap-x-3 px-4 pt-2.5 pb-3',\n      'has-data-[slot=frame-action]:grid-cols-[minmax(0,1fr)_auto]',\n      // The action spans two rows, so with no description the second is empty\n      // and a gap under the title would only add height.\n      'has-data-[slot=frame-description]:gap-y-0.5',\n      'group-data-[variant=inset]/frame:px-3 group-data-[variant=inset]/frame:pt-1 group-data-[variant=inset]/frame:pb-2.5',\n      'group-data-[variant=plain]/frame:px-1',\n      'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:py-2.5',\n    ],\n    /*\n     * One part in two places. In the header it labels the tray, so it stays\n     * quiet; inside a row's content it is the row's subject, so it takes the\n     * foreground and the weight, and truncates rather than pushing the row's\n     * trailing slot out of view.\n     */\n    title: [\n      'text-sm font-medium text-muted-foreground',\n      'in-data-[slot=frame-content]:truncate in-data-[slot=frame-content]:text-foreground',\n      // A band names one thing rather than captioning a list, so its title reads first.\n      'group-data-[variant=band]/frame:text-foreground',\n    ],\n    description: 'text-sm text-muted-foreground in-data-[slot=frame-content]:truncate',\n    action: 'col-start-2 row-span-2 row-start-1 text-sm text-muted-foreground',\n    // The panel's shape follows the root's variant, read from its data attribute.\n    panel: [\n      'flex flex-col divide-y overflow-hidden bg-card text-card-foreground',\n      // A band holds one figure, not a list of rows, so nothing in it is ruled off.\n      'group-data-[variant=band]/frame:divide-y-0',\n      'group-data-[variant=default]/frame:rounded-t-2xl group-data-[variant=default]/frame:border-t',\n      'group-data-[variant=plain]/frame:rounded-3xl group-data-[variant=plain]/frame:border group-data-[variant=plain]/frame:shadow-xs',\n      'group-data-[variant=inset]/frame:rounded-[30px] group-data-[variant=inset]/frame:bg-popover group-data-[variant=inset]/frame:text-popover-foreground group-data-[variant=inset]/frame:shadow-xs',\n      // 18px is the band's 24 less its 6px of padding: one corner, two curves.\n      'group-data-[variant=band]/frame:rounded-[1.125rem] group-data-[variant=band]/frame:border group-data-[variant=band]/frame:shadow-xs',\n    ],\n    row: 'flex w-full items-center gap-3 px-4 py-3 text-start',\n    rowPressable: [\n      'cursor-pointer outline-none transition-colors hover:bg-muted/60',\n      'focus-visible:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',\n    ],\n    chevron: 'size-4 shrink-0 text-muted-foreground/70 rtl:-scale-x-100',\n    media: 'flex shrink-0 items-center justify-center',\n    content: 'flex min-w-0 flex-1 flex-col gap-0.5',\n    actions: 'flex shrink-0 items-center gap-1.5',\n    section: 'flex flex-col divide-y',\n    sectionTitle: 'bg-muted/40 px-4 pt-2.5 pb-1.5 text-xs font-medium uppercase tracking-wider text-muted-foreground',\n    footer: [\n      'flex items-center gap-2 px-4 pt-3 pb-3.5',\n      // Under `inset` the band is a row of equal decisions: equal widths, full pills.\n      'group-data-[variant=inset]/frame:gap-3.5 group-data-[variant=inset]/frame:px-[18px] group-data-[variant=inset]/frame:pt-4 group-data-[variant=inset]/frame:pb-2.5',\n      'group-data-[variant=inset]/frame:*:h-11 group-data-[variant=inset]/frame:*:flex-1 group-data-[variant=inset]/frame:*:rounded-full',\n      'group-data-[variant=plain]/frame:px-1',\n      'group-data-[variant=band]/frame:px-3.5 group-data-[variant=band]/frame:pt-2.5 group-data-[variant=band]/frame:pb-1',\n    ],\n  },\n  variants: {\n    variant: {\n      default: {\n        // `overflow-hidden` is what gives the panel the shell's bottom corners.\n        root: 'overflow-hidden rounded-3xl border bg-muted',\n      },\n      plain: {},\n      band: {\n        root: 'rounded-3xl bg-muted p-1.5',\n      },\n      inset: {\n        // The band is the popover surface with black mixed in, so it is the\n        // darker of the two in either theme, which a step on a grey scale\n        // cannot promise: those run darker one way and lighter the other.\n        root: [\n          'rounded-[38px] p-2 ring-1 ring-foreground/[0.06]',\n          'bg-[color-mix(in_oklab,var(--popover),black_5%)] dark:bg-[color-mix(in_oklab,var(--popover),black_14%)]',\n        ],\n      },\n    },\n  },\n  defaultVariants: { variant: 'default' },\n});\n\nexport type FrameVariant = 'default' | 'band' | 'inset' | 'plain';\n\nexport interface FrameProps extends ComponentProps<'div'> {\n  /**\n   * `default` sets the panel in a tray. `band` holds a bordered card in a\n   * thin muted band, for a widget showing one figure or chart, and its panel\n   * draws no rule between its children. `inset` floats\n   * the panel in a recessed band and gives `Frame.Footer` somewhere to sit.\n   * `plain` drops the shell, for a frame inside a container that already draws\n   * its own edge.\n   */\n  variant?: FrameVariant;\n}\n\nfunction FrameRoot({ variant = 'default', className, render, ...props }: FrameProps & RenderProp<'div'>) {\n  const slots = frameVariants({ variant });\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame', 'data-variant': variant, className: slots.root({ className }) }, props),\n  });\n}\nFrameRoot.displayName = 'Frame';\n\nexport interface FrameHeaderProps extends ComponentProps<'div'> {}\n\n/**\n * The strip of shell above the panel: a title, an optional description under\n * it, and `Frame.Action` on the trailing side.\n */\nfunction FrameHeader({ className, render, ...props }: FrameHeaderProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-header', className: frameVariants().header({ className }) }, props),\n  });\n}\nFrameHeader.displayName = 'Frame.Header';\n\nexport interface FrameTitleProps extends ComponentProps<'h3'> {\n  /** The heading level in a header. Default `h3`; a row title is always a `p`. */\n  as?: 'h2' | 'h3' | 'h4' | 'p';\n}\n\n/** A caption in the header; the subject line inside `Frame.Content`. */\nfunction FrameTitle({ as: Tag = 'h3', className, ...props }: FrameTitleProps) {\n  return <Tag data-slot=\"frame-title\" className={frameVariants().title({ className })} {...props} />;\n}\nFrameTitle.displayName = 'Frame.Title';\n\nexport interface FrameDescriptionProps extends ComponentProps<'p'> {}\n\n/** A line under a title, in the header or in a row. */\nfunction FrameDescription({ className, render, ...props }: FrameDescriptionProps & RenderProp<'p'>) {\n  return useRender({\n    defaultTagName: 'p',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'frame-description', className: frameVariants().description({ className }) },\n      props\n    ),\n  });\n}\nFrameDescription.displayName = 'Frame.Description';\n\nexport interface FrameActionProps extends ComponentProps<'div'> {}\n\n/** The header's trailing side: a short status, a link, a small button. */\nfunction FrameAction({ className, render, ...props }: FrameActionProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-action', className: frameVariants().action({ className }) }, props),\n  });\n}\nFrameAction.displayName = 'Frame.Action';\n\nexport interface FramePanelProps extends ComponentProps<'div'> {}\n\n/** The card the rows sit on. It draws the hairline between its children, except under `band`. */\nfunction FramePanel({ className, render, ...props }: FramePanelProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-panel', className: frameVariants().panel({ className }) }, props),\n  });\n}\nFramePanel.displayName = 'Frame.Panel';\n\nexport interface FrameRowProps extends Omit<ComponentProps<'div'>, 'onClick'> {\n  /** Make the row a link to this address. */\n  href?: string;\n  /** Where a link row opens, as on an `<a>`. */\n  target?: string;\n  /** The relationship of a link row's target, as on an `<a>`. */\n  rel?: string;\n  /** Make the row a button that calls this. Ignored when `href` is set. */\n  onClick?: MouseEventHandler<HTMLElement>;\n  /** A trailing chevron, for a row that leads somewhere. */\n  chevron?: boolean;\n}\n\nconst Chevron = ({ className }: { className: string }) => (\n  <svg\n    viewBox=\"0 0 24 24\"\n    aria-hidden\n    className={className}\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth={2}\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"m9 6 6 6-6 6\" />\n  </svg>\n);\n\n/**\n * One line of the panel: `Frame.Media`, `Frame.Content` and `Frame.Actions`\n * side by side. With `href` it is a link and with `onClick` a button.\n */\nfunction FrameRow({\n  className,\n  children,\n  chevron = false,\n  href,\n  target,\n  rel,\n  render,\n  ...props\n}: FrameRowProps & RenderProp<'div'>) {\n  const slots = frameVariants();\n  const inside = (\n    <>\n      {children}\n      {chevron ? <Chevron className={slots.chevron()} /> : null}\n    </>\n  );\n  // Called before any branch, so a prop that changes later cannot change the hook count.\n  const rendered = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      { 'data-slot': 'frame-row', className: slots.row({ className }), children: <>{inside}</> },\n      props\n    ),\n  });\n  if (href !== undefined) {\n    return (\n      <a\n        data-slot=\"frame-row\"\n        href={href}\n        target={target}\n        rel={rel}\n        className={slots.row({ className: [slots.rowPressable(), className] })}\n        {...(props as ComponentProps<'a'>)}\n      >\n        {inside}\n      </a>\n    );\n  }\n  // `onClick` stays in `props`, so it reaches whichever element is rendered.\n  if (props.onClick !== undefined) {\n    return (\n      <button\n        type=\"button\"\n        data-slot=\"frame-row\"\n        className={slots.row({ className: [slots.rowPressable(), className] })}\n        {...(props as ComponentProps<'button'>)}\n      >\n        {inside}\n      </button>\n    );\n  }\n  return rendered;\n}\nFrameRow.displayName = 'Frame.Row';\n\nexport interface FrameMediaProps extends ComponentProps<'div'> {}\n\n/** A row's leading icon, avatar or picture. Holds its size. */\nfunction FrameMedia({ className, render, ...props }: FrameMediaProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-media', className: frameVariants().media({ className }) }, props),\n  });\n}\nFrameMedia.displayName = 'Frame.Media';\n\nexport interface FrameContentProps extends ComponentProps<'div'> {}\n\n/**\n * The flexible middle of a row, usually a title over a description. It takes\n * what the media and actions leave and may shrink past its content, which is\n * what keeps a long line from pushing the rest of the row off the edge.\n */\nfunction FrameContent({ className, render, ...props }: FrameContentProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-content', className: frameVariants().content({ className }) }, props),\n  });\n}\nFrameContent.displayName = 'Frame.Content';\n\nexport interface FrameActionsProps extends ComponentProps<'div'> {}\n\n/** A row's trailing side: a badge, a value, a switch, a small button. Holds its size. */\nfunction FrameActions({ className, render, ...props }: FrameActionsProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-actions', className: frameVariants().actions({ className }) }, props),\n  });\n}\nFrameActions.displayName = 'Frame.Actions';\n\nexport interface FrameSectionProps extends Omit<ComponentProps<'section'>, 'title'> {\n  /** The heading over the section's rows. */\n  title?: ReactNode;\n}\n\n/** A labelled group of rows, for a panel holding more than one. */\nfunction FrameSection({ title, className, children, render, ...props }: FrameSectionProps & RenderProp<'section'>) {\n  const slots = frameVariants();\n  return useRender({\n    defaultTagName: 'section',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'frame-section',\n        className: slots.section({ className }),\n        children: (\n          <>\n            {title !== undefined ? <h4 className={slots.sectionTitle()}>{title}</h4> : null}\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nFrameSection.displayName = 'Frame.Section';\n\nexport interface FrameFooterProps extends ComponentProps<'div'> {}\n\n/**\n * The strip below the panel. Under `inset` its children become equal pills in\n * the recessed band, the shape a row of decisions takes.\n */\nfunction FrameFooter({ className, render, ...props }: FrameFooterProps & RenderProp<'div'>) {\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps({ 'data-slot': 'frame-footer', className: frameVariants().footer({ className }) }, props),\n  });\n}\nFrameFooter.displayName = 'Frame.Footer';\n\nexport const Frame = Object.assign(FrameRoot, {\n  Header: FrameHeader,\n  Title: FrameTitle,\n  Description: FrameDescription,\n  Action: FrameAction,\n  Panel: FramePanel,\n  Row: FrameRow,\n  Media: FrameMedia,\n  Content: FrameContent,\n  Actions: FrameActions,\n  Section: FrameSection,\n  Footer: FrameFooter,\n});\n\nexport {\n  FrameHeader,\n  FrameTitle,\n  FrameDescription,\n  FrameAction,\n  FramePanel,\n  FrameRow,\n  FrameMedia,\n  FrameContent,\n  FrameActions,\n  FrameSection,\n  FrameFooter,\n};\n"
    }
  ]
}
