{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bezel","type":"registry:component","title":"Bezel","description":"A double-bezel frame: an outer shell with a hairline, a small gap, and an inset panel with its own edge, so a preview, chart or media reads as set into the surface.","categories":["layout"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/bezel/bezel.module.css","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* the shell is a surface and the inner panel a well, so the double bezel takes the look's paint */\n  .shell {\n    --pui-gap: var(--pui-bezel-gap);\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    gap: var(--pui-gap);\n    min-width: 0;\n    padding: var(--pui-gap);\n    border-radius: var(--pui-bezel-radius);\n  }\n  .shell[data-size=\"sm\"] {\n    --pui-gap: var(--pui-space-1-5);\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  /* The inner radius is the outer minus the gap, plus a little: parallel curves read as one object. */\n  .inner {\n    position: relative;\n    min-width: 0;\n    min-height: 0;\n    /* grow into a shell given a height, but keep a size set on the panel (flex-basis auto, not 0) */\n    flex: 1 1 auto;\n    overflow: hidden;\n    border-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    border-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .inner > :is(img, video, canvas) {\n    display: block;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n  /* interactive: the picture set into the panel leans toward the pointer (a small grow, clipped by the panel, so the\n     frame never moves) and settles back when it leaves; focus inside does the same, so the keyboard sees it too.\n     Transform only; reduced motion keeps the frame still. */\n  .shell[data-interactive] {\n    cursor: pointer;\n  }\n  .shell[data-interactive] > .inner > * {\n    transition: scale var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .shell[data-interactive]:is(:hover, :focus-within) > .inner > * {\n    scale: calc(1 + 0.04 * var(--pui-motion-travel, 1));\n    transition-duration: var(--pui-motion-in);\n  }\n  /* Nesting input (O1 §14.1): the footer (in the shell's lower corners) and the inner panel tell the controls they hold\n     their radius and the padding next to the corner; the control clamps its own concentric radius.\n     --pui-bezel-inner-inset says how far the panel's content is padded (default the card padding). */\n  .footer {\n    --pui-nest-radius: var(--pui-bezel-radius);\n    --pui-nest-inset: calc(var(--pui-gap) + var(--pui-space-1));\n  }\n  .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap) + var(--pui-space-0-5));\n    --pui-nest-inset: var(--pui-bezel-inner-inset, var(--pui-pad-card));\n  }\n  .shell[data-size=\"sm\"] > .footer {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - var(--pui-gap));\n  }\n  .shell[data-size=\"sm\"] > .inner {\n    --pui-nest-radius: calc(var(--pui-bezel-radius) - 2 * var(--pui-gap) + var(--pui-space-0-5));\n  }\n  .footer {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-3);\n    min-width: 0;\n    padding: var(--pui-space-0-5) var(--pui-space-1) var(--pui-space-1);\n  }\n  .icon {\n    display: grid;\n    place-items: center;\n    flex: none;\n    width: 40px;\n    height: 40px;\n    /* nested in the shell's corner: shell radius minus the gap and the footer's inline padding */\n    border-radius: max(0px, calc(var(--pui-bezel-radius) - var(--pui-gap) - var(--pui-space-1)));\n    color: var(--pui-ink-2);\n  }\n  .icon > svg {\n    width: 18px;\n    height: 18px;\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.shell {\n  composes: m-surface from global;\n}\n.inner {\n  composes: m-inset from global;\n}\n.icon {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/bezel/bezel.tsx","type":"registry:file","target":"src/ui/beamline/components/bezel/bezel.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BezelDefaults } from \"./defaults\";\nexport type { BezelDefaults } from \"./defaults\";\n\nimport { type ComponentProps, type ComponentPropsWithoutRef, type ElementType, type ReactNode, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./bezel.module.css\";\n\nexport interface BezelClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the inner part; no override by default. */\n  inner?: string;\n  /** Classes for the footer part; no override by default. */\n  footer?: string;\n}\n\nexport interface BezelProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n  /** Sits in the shell under the panel: an icon tile with a title and a line, or actions. */\n  footer?: ReactNode;\n  /** `sm` for tiles and small previews (6 px gap), `md` for cards (10 px). */\n  size?: \"sm\" | \"md\";\n  /** Extra classes for the inner panel. */\n  innerClassName?: string;\n  /** HTML element used for the text or container. Defaults to \"div\". */\n  as?: \"div\" | \"section\" | \"article\" | \"li\";\n  /** The bezel is something to open (a template, a file, a preview): the picture set into it leans toward the pointer\n   *  (or focus inside) and the frame is a pointer target. Put the link or button inside. */\n  interactive?: boolean;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: BezelClassNames;\n}\n\n/**\n * A double-bezel frame: an outer shell with a hairline, a small gap, and an inset panel with its own edge, radii nested\n * so the two curves run parallel. It makes a preview, chart or media read as an object set into the surface. Static\n * CSS paint with scoped presentation defaults. Card, MetricCard and the gallery use it; wrap other content directly.\n */\nexport function Bezel({\n  footer,\n  size: sizeProp,\n  innerClassName,\n  as = \"div\",\n  interactive = false,\n  className,\n  classNames,\n  children,\n  ...props\n}: BezelProps) {\n  const scopeDefaults = useComponentDefaults<BezelDefaults>(\"bezel\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const Tag = as as ElementType;\n  return (\n    <Tag\n      data-slot=\"bezel\"\n      data-size={size}\n      data-interactive={interactive ? \"\" : undefined}\n      className={cn(styles.shell, interactive && \"reveal-host\", classNames?.root, className)}\n      {...props}\n    >\n      <div data-slot=\"bezel-inner\" className={cn(styles.inner, classNames?.inner, innerClassName)}>\n        {children}\n      </div>\n      {footer && (\n        <div data-slot=\"bezel-footer\" className={cn(styles.footer, classNames?.footer)}>\n          {footer}\n        </div>\n      )}\n    </Tag>\n  );\n}\n\n/** An icon tile for a Bezel footer: a small bezel of its own. */\nexport function BezelIcon({ className, ...props }: ComponentProps<\"span\">) {\n  return <span data-slot=\"bezel-icon\" aria-hidden className={cn(styles.icon, className)} {...props} />;\n}\n\nexport default Bezel;\n"},{"path":"beamline/components/bezel/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/bezel/defaults.ts","content":"/** Pure allowed presentation defaults for Bezel; no component or runtime imports. */\nexport interface BezelDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Bezel is in src/ui/beamline/components/bezel/bezel.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/bezel"}}