{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"hover-card","type":"registry:component","title":"Hover card","description":"A preview of a person, link or record on hover or keyboard focus, without leaving the page; HoverCardProfile is the standard person preview.","categories":["overlays"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-hover-card@1.1.23","clsx@^2.1.1","motion@^13.4.4","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/_shared/overlay.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/overlay.module.css","content":"@layer properties, theme, base, components, utilities;\n/* the source morph's moving parts: the clip's four sides, the radius and the offset back to the trigger */\n@property --ci-t {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-r {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-b {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-l {\n  syntax: \"<length-percentage>\";\n  inherits: false;\n  initial-value: -48px;\n}\n@property --ci-x {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@property --ci-y {\n  syntax: \"<length>\";\n  inherits: false;\n  initial-value: 0px;\n}\n@layer components {\n  /* Every floating layer in the system: one surface (painted by the overlay role of the look in force), one menu-item\n   look, one scrim, one enter and exit. A layer placed against a trigger opens out of it (the source morph below); one\n   without a placement arrives in --pui-motion-in scaling from its origin. Each leaves in --pui-motion-out; Radix keeps\n   it mounted until the exit animation ends. Reduced motion makes both instant (the duration tokens become 1ms). */\n  .surface {\n    z-index: var(--pui-z-popover);\n    min-width: 8rem;\n    /* never past the screen: as wide and as tall as the room beside the trigger (Radix measures it) */\n    max-width: var(--radix-popper-available-width, none);\n    max-height: var(--radix-popper-available-height, 70vh);\n    overflow: auto;\n    border-radius: var(--pui-overlay-radius, var(--pui-radius-card));\n    color: var(--pui-ink);\n    transform-origin: var(--radix-popper-transform-origin, var(--radix-dropdown-menu-content-transform-origin, center));\n    outline: none;\n  }\n  .surface[data-state=\"open\"],\n  .surface[data-state=\"delayed-open\"],\n  .surface[data-state=\"instant-open\"] {\n    animation: pop-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .surface[data-state=\"closed\"] {\n    animation: pop-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n\n  /* The source morph (research/morphing-interfaces-2026-10-04, M3): a layer placed against a trigger does not fade in\n     beside it, it opens out of it. It starts as the trigger's own box (Radix gives its size and the side and alignment\n     it was placed on), offset back onto the trigger, and grows: the side nearest the trigger is there at once, the\n     sides across follow, the far side arrives last (~200 ms), so the rows by the trigger are readable within the\n     ruled 90 ms while the outline settles. Content fades in over those 90 ms. It closes back into the trigger in the\n     ruled 180 ms. The clip ends 48px outside the box so the layer's shadow is never cut. Reduced motion: instant. */\n  .surface[data-side] {\n    --_aw: var(--radix-popper-anchor-width, 0px);\n    --_ah: var(--radix-popper-anchor-height, 0px);\n    --_gap: 6px;\n    --from-t: -48px;\n    --from-r: -48px;\n    --from-b: -48px;\n    --from-l: -48px;\n    --from-x: 0px;\n    --from-y: 0px;\n    --d-near: calc(var(--pui-duration-standard) * 0.55);\n    --d-cross: calc(var(--pui-duration-standard) * 0.85);\n    --d-far: var(--pui-duration-standard);\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-overlay-radius, var(--pui-radius-card))\n    );\n    translate: var(--ci-x) var(--ci-y);\n  }\n  .surface[data-side=\"bottom\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n    --from-y: calc(-1 * (var(--_ah) + var(--_gap)));\n    --d-t: var(--d-near);\n    --d-b: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"top\"] {\n    --from-b: 0px;\n    --from-t: calc(100% - var(--_ah));\n    --from-y: calc(var(--_ah) + var(--_gap));\n    --d-b: var(--d-near);\n    --d-t: var(--d-far);\n    --d-l: var(--d-cross);\n    --d-r: var(--d-cross);\n  }\n  .surface[data-side=\"right\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n    --from-x: calc(-1 * (var(--_aw) + var(--_gap)));\n    --d-l: var(--d-near);\n    --d-r: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  .surface[data-side=\"left\"] {\n    --from-r: 0px;\n    --from-l: calc(100% - var(--_aw));\n    --from-x: calc(var(--_aw) + var(--_gap));\n    --d-r: var(--d-near);\n    --d-l: var(--d-far);\n    --d-t: var(--d-cross);\n    --d-b: var(--d-cross);\n  }\n  /* alignment along the trigger: which part of the layer the trigger sits over */\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"] {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"] {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"start\"]:dir(rtl) {\n    --from-l: calc(100% - var(--_aw));\n    --from-r: 0px;\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"end\"]:dir(rtl) {\n    --from-l: 0px;\n    --from-r: calc(100% - var(--_aw));\n  }\n  .surface:is([data-side=\"bottom\"], [data-side=\"top\"])[data-align=\"center\"] {\n    --from-l: calc((100% - var(--_aw)) / 2);\n    --from-r: calc((100% - var(--_aw)) / 2);\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"start\"] {\n    --from-t: 0px;\n    --from-b: calc(100% - var(--_ah));\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"end\"] {\n    --from-t: calc(100% - var(--_ah));\n    --from-b: 0px;\n  }\n  .surface:is([data-side=\"left\"], [data-side=\"right\"])[data-align=\"center\"] {\n    --from-t: calc((100% - var(--_ah)) / 2);\n    --from-b: calc((100% - var(--_ah)) / 2);\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) {\n    animation:\n      grow-t var(--d-t) var(--pui-ease-out-strong),\n      grow-r var(--d-r) var(--pui-ease-out-strong),\n      grow-b var(--d-b) var(--pui-ease-out-strong),\n      grow-l var(--d-l) var(--pui-ease-out-strong),\n      grow-xy var(--d-cross) var(--pui-ease-out-strong),\n      settling var(--d-far) step-end;\n  }\n  .surface[data-side]:is([data-state=\"open\"], [data-state=\"delayed-open\"], [data-state=\"instant-open\"]) > * {\n    animation: content-in var(--pui-motion-in) var(--pui-ease-default) both;\n  }\n  /* one duration for every part of the exit: the layer unmounts when the first of them ends */\n  .surface[data-side][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .surface[data-side][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  @keyframes grow-t {\n    from {\n      --ci-t: var(--from-t);\n    }\n  }\n  @keyframes grow-r {\n    from {\n      --ci-r: var(--from-r);\n    }\n  }\n  @keyframes grow-b {\n    from {\n      --ci-b: var(--from-b);\n    }\n  }\n  @keyframes grow-l {\n    from {\n      --ci-l: var(--from-l);\n    }\n  }\n  @keyframes grow-xy {\n    from {\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n    }\n  }\n  /* it starts over its own trigger: until it has grown, the press that opened it must not land on a row */\n  @keyframes settling {\n    0%,\n    100% {\n      pointer-events: none;\n    }\n  }\n  @keyframes shrink {\n    to {\n      --ci-t: var(--from-t);\n      --ci-r: var(--from-r);\n      --ci-b: var(--from-b);\n      --ci-l: var(--from-l);\n      --ci-x: var(--from-x);\n      --ci-y: var(--from-y);\n      opacity: 0;\n    }\n  }\n  @keyframes content-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes content-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .padded {\n    padding: var(--pui-pad-popover);\n  }\n  .menu {\n    padding: var(--pui-space-1);\n  }\n  @keyframes pop-in {\n    from {\n      opacity: 0;\n      transform: scale(0.97);\n    }\n  }\n  @keyframes pop-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n\n  /* rows (and the gliding plate) nest in the list: its radius minus its padding, never rounder than a chip; on the\n     row itself, so any list that uses .item gets it */\n  .item,\n  .glide {\n    --_item-radius: min(\n      var(--pui-radius-chip),\n      calc(var(--pui-overlay-radius, var(--pui-radius-card)) - var(--pui-space-1))\n    );\n  }\n\n  /* menu rows: shared by dropdown, context menu, menubar, select, combobox lists */\n  .item {\n    position: relative;\n    z-index: 1;\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    min-height: var(--pui-control-height-sm);\n    padding: 0 var(--pui-space-2);\n    /* nested in the list: the overlay's radius minus its padding, never rounder than a chip */\n    border-radius: var(--_item-radius);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    cursor: default;\n    user-select: none;\n    outline: none;\n  }\n  /* on a touch screen a row is a finger's target: the touch height (44px) from the tokens; a mouse keeps the dense row */\n  @media (pointer: coarse) {\n    .item {\n      min-height: var(--pui-control-height-touch);\n    }\n  }\n  /* a destructive row speaks in the danger ink only while it can be used: disabled wins (muted ink, no alarm for an\n     action that cannot happen) */\n  .item[data-tone=\"danger\"]:not([data-disabled]) {\n    color: var(--pui-red);\n  }\n  .item[data-disabled] {\n    color: var(--pui-ink-3);\n    pointer-events: none;\n  }\n  .item :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n    flex-shrink: 0;\n    color: var(--pui-ink-3);\n  }\n  .item[data-tone=\"danger\"]:not([data-disabled]) :where(svg) {\n    color: currentColor;\n  }\n  .item[data-highlighted]:not([data-glide]) {\n    background: var(--pui-hover-2);\n  }\n  /* a press is acknowledged at once, over the plate, before the menu closes or the row toggles */\n  .item:active:not([data-disabled]) {\n    background: var(--pui-hover);\n  }\n  .shortcut {\n    margin-inline-start: auto;\n    padding-inline-start: var(--pui-space-4);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    letter-spacing: var(--pui-tracking-mono-caps);\n  }\n  .heading {\n    padding: var(--pui-space-1-5) var(--pui-space-2) var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n  }\n  .separator {\n    height: 1px;\n    margin: var(--pui-space-1) calc(var(--pui-space-1) * -1);\n    background: var(--pui-line);\n  }\n  .check {\n    margin-inline-start: auto;\n    color: var(--pui-accent) !important;\n  }\n  /* a check arrives in its row as the value changes (at once): it grows from the middle over the arrival */\n  .check :where(svg) {\n    animation: check-in var(--pui-motion-in) var(--pui-ease-out-strong);\n  }\n  @keyframes check-in {\n    from {\n      opacity: 0;\n      scale: calc(1 - 0.5 * var(--pui-motion-travel, 1));\n    }\n  }\n  /* the gliding highlight: one plate moves between rows for the pointer and the arrow keys; it appears in place */\n  .glide {\n    position: absolute;\n    z-index: 0;\n    inset-inline: var(--pui-space-1);\n    top: 0;\n    border-radius: var(--_item-radius);\n    background: var(--pui-hover-2);\n    pointer-events: none;\n    opacity: 0;\n    transition: opacity var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .glide[data-on] {\n    opacity: 1;\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n  .glide[data-jump] {\n    transition: opacity 0ms;\n  }\n  /* a plate over a row or grid of cells takes each cell's size and travels both ways */\n  .glide[data-axis=\"xy\"] {\n    inset-inline: auto;\n    left: 0;\n  }\n  .glide[data-axis=\"xy\"][data-on] {\n    transition:\n      transform var(--pui-duration-fast) var(--pui-ease-standard),\n      width var(--pui-duration-fast) var(--pui-ease-standard),\n      height var(--pui-duration-fast) var(--pui-ease-standard),\n      opacity 0ms;\n  }\n\n  /* a layer that has begun to close is out of the way at once: for the length of its exit it takes no pointer, so a tap\n     meant for the page behind (the next row, the button the panel covered) lands there, never on the leaving ghost.\n     Focus has already gone back (Radix) and it unmounts when the exit ends. */\n  .scrim[data-state=\"closed\"],\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    pointer-events: none;\n  }\n\n  /* modal layers */\n  .scrim {\n    position: fixed;\n    inset: 0;\n    z-index: var(--pui-z-overlay);\n    background: var(--pui-scrim);\n    backdrop-filter: blur(var(--pui-blur-overlay));\n  }\n  .scrim[data-state=\"open\"] {\n    animation: fade-in var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .scrim[data-state=\"closed\"] {\n    animation: fade-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n  }\n  @keyframes fade-out {\n    to {\n      opacity: 0;\n    }\n  }\n  .dialog {\n    /* the content answers to the dialog's own width (a footer's buttons share the row on a phone) */\n    container-type: inline-size;\n    position: fixed;\n    z-index: var(--pui-z-modal);\n    left: 50%;\n    top: 50%;\n    width: min(calc(100vw - var(--pui-space-8)), var(--pui-dialog-width, var(--_dialog-width, 440px)));\n    max-height: calc(100dvh - var(--pui-space-12));\n    overflow: hidden;\n    translate: -50% -50%;\n    border-radius: var(--pui-dialog-radius, var(--pui-radius-window));\n    color: var(--pui-ink);\n    outline: none;\n    /* a size chosen while open (a step that needs more room) eases the width in place, centred */\n    transition: width var(--pui-duration-standard) var(--pui-ease-standard);\n  }\n  .dialog[data-state=\"open\"] {\n    animation: dialog-in var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .dialog[data-state=\"closed\"] {\n    animation: dialog-out var(--pui-motion-out) var(--pui-ease-default) forwards;\n  }\n  /* A dialog opened from something (use-open-from.ts) grows out of it: it starts as the opener's box, offset back onto\n     it, and grows to its own place over the considered duration while its content fades in; it closes back into the\n     opener in the ruled 180 ms. Shares the source morph's moving parts and guard with .surface. */\n  .dialog[data-open-from] {\n    clip-path: inset(\n      var(--ci-t) var(--ci-r) var(--ci-b) var(--ci-l) round var(--pui-dialog-radius, var(--pui-radius-window))\n    );\n    transform: translate(var(--ci-x), var(--ci-y));\n  }\n  .dialog[data-open-from][data-state=\"open\"] {\n    animation:\n      grow-t var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-r var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-b var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-l var(--pui-duration-considered) var(--pui-ease-out-strong),\n      grow-xy calc(var(--pui-duration-considered) * 0.9) var(--pui-ease-out-strong),\n      settling var(--pui-duration-considered) step-end;\n  }\n  .dialog[data-open-from][data-state=\"open\"] > * {\n    animation: content-in var(--pui-duration-standard) var(--pui-ease-default) calc(var(--pui-duration-standard) * 0.35)\n      both;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] {\n    animation: shrink var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  .dialog[data-open-from][data-state=\"closed\"] > * {\n    animation: content-out var(--pui-motion-in) var(--pui-ease-default) forwards;\n  }\n  /* A sheet: a panel from an edge. It slides in from its edge over the considered duration on the drawer curve and\n     goes back out to that edge in the ruled departure (a layer closes back into where it came from). The edge is\n     logical: an end (\"right\") sheet comes from the left in a right-to-left page. Its content answers to its width. */\n  .sheet {\n    --_sx: 0px;\n    --_sy: 0px;\n    container-type: inline-size;\n    /* where a finger has swiped it toward its edge (sheet.tsx); it settles back over the departure */\n    translate: var(--_swipe, 0px) 0;\n  }\n  /* a side panel leaves sideways pans to the swipe and keeps vertical scrolling native, in its body too */\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]),\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) [data-slot=\"sheet-body\"] {\n    touch-action: pan-y;\n  }\n  .sheet:is([data-side=\"left\"], [data-side=\"right\"]) {\n    transition:\n      var(--m-transition),\n      translate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .sheet[data-swiping] {\n    transition: none;\n  }\n  .sheet[data-side=\"right\"] {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"left\"] {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"right\"]:dir(rtl) {\n    --_sx: -100%;\n  }\n  .sheet[data-side=\"left\"]:dir(rtl) {\n    --_sx: 100%;\n  }\n  .sheet[data-side=\"top\"] {\n    --_sy: -100%;\n  }\n  .sheet[data-side=\"bottom\"] {\n    --_sy: 100%;\n  }\n  .sheet[data-state=\"open\"] {\n    animation: sheet-in var(--pui-duration-considered) var(--pui-ease-drawer) both;\n  }\n  .sheet[data-state=\"closed\"] {\n    animation: sheet-out var(--pui-motion-out) var(--pui-ease-exit) forwards;\n  }\n  @keyframes sheet-in {\n    from {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes sheet-out {\n    to {\n      translate: var(--_sx) var(--_sy);\n    }\n  }\n  @keyframes dialog-in {\n    from {\n      opacity: 0;\n      transform: scale(0.96);\n    }\n  }\n  @keyframes dialog-out {\n    to {\n      opacity: 0;\n      transform: scale(0.98);\n    }\n  }\n}\n\n/* Reduced motion: surfaces and dialogs fade without scaling or growing, over the fade tokens (restrained under reduced,\n   1ms under none; APPEARANCE A06). */\n@media (prefers-reduced-motion: reduce) {\n  .surface,\n  .dialog,\n  .sheet {\n    animation-name: fade-in !important;\n    animation-duration: var(--pui-fade-in) !important;\n  }\n  .surface[data-state=\"closed\"],\n  .dialog[data-state=\"closed\"],\n  .sheet[data-state=\"closed\"] {\n    animation-name: fade-out !important;\n    animation-duration: var(--pui-fade-out) !important;\n  }\n}\n@layer components {\n  :global(.pui[data-motion=\"reduced\"]) .surface,\n  :global(.pui[data-motion=\"none\"]) .surface,\n  :global(.pui[data-motion=\"reduced\"]) .dialog,\n  :global(.pui[data-motion=\"none\"]) .dialog,\n  :global(.pui[data-motion=\"reduced\"]) .sheet,\n  :global(.pui[data-motion=\"none\"]) .sheet {\n    animation-name: fade-in;\n    animation-duration: var(--pui-fade-in);\n  }\n  :global(.pui[data-motion=\"reduced\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .surface[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .dialog[data-state=\"closed\"],\n  :global(.pui[data-motion=\"reduced\"]) .sheet[data-state=\"closed\"],\n  :global(.pui[data-motion=\"none\"]) .sheet[data-state=\"closed\"] {\n    animation-name: fade-out;\n    animation-duration: var(--pui-fade-out);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.surface {\n  composes: m-overlay from global;\n}\n.dialog {\n  composes: m-overlay from global;\n}\n"},{"path":"beamline/components/avatar/avatar.module.css","type":"registry:file","target":"src/ui/beamline/components/avatar/avatar.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Avatar: a face (initials on the name's hue, or a photo) and an optional presence mark. The mark is cut out of the\n     face with a mask, so the gap around it is truly empty and reads on any ground: a card, a row hover, glass, a photo. */\n  .root {\n    --_s: 36px;\n    --_d: 10px;\n    --_g: 2px;\n    position: relative;\n    display: inline-grid;\n    flex: none;\n    width: var(--_s);\n    height: var(--_s);\n    place-items: center;\n    /* initials scale with the face (geometry), never below 80% of the smallest type step */\n    font-size: max(calc(var(--pui-text-2xs) * 0.8), calc(var(--_s) * 0.36));\n    font-weight: var(--pui-weight-medium);\n    line-height: 1;\n    user-select: none;\n  }\n  .xs {\n    --_s: 20px;\n    --_d: 6px;\n    --_g: 1.5px;\n  }\n  .sm {\n    --_s: 28px;\n    --_d: 8px;\n  }\n  .lg {\n    --_s: 44px;\n    --_d: 12px;\n  }\n  .xl {\n    --_s: 56px;\n    --_d: 14px;\n    --_g: 2.5px;\n  }\n  .face {\n    position: absolute;\n    inset: 0;\n    display: grid;\n    place-items: center;\n    overflow: hidden;\n    border-radius: 50%;\n    background: color-mix(in oklab, var(--pui-avatar-hue) 24%, var(--pui-surface-2));\n    color: color-mix(in oklab, var(--pui-avatar-hue) 55%, var(--pui-ink));\n  }\n  /* the initials keep their tint but mix it into black or white, whichever the face's fill reads better against (its\n     luminance at WCAG's 0.179 crossover): mixed into the scope's ink they fell to 3.5–4.5 on mid fills, a brand-toned\n     sidebar's above all; this keeps every face at 5.5:1 or more in every look */\n  @supports (color: color(from red xyz-d65 x y z)) {\n    .face {\n      --_ink: color(\n        from color-mix(in oklab, var(--pui-avatar-hue) 24%, var(--pui-surface-2)) xyz-d65\n          calc(0.9505 * round(0.679 - y)) round(0.679 - y) calc(1.089 * round(0.679 - y))\n      );\n      color: color-mix(in oklab, var(--pui-avatar-hue) 55%, var(--_ink));\n    }\n  }\n  /* a company tile takes the look's card corner, never more than a third of the face, so it stays a square at xs */\n  .root[data-shape=\"square\"] .face {\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-avatar-radius wins over both. */\n    --_avatar-nest: clamp(\n      var(--pui-radius-chip),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, min(var(--pui-radius-card), calc(var(--_s) / 3)))\n    );\n    --_r: var(--pui-avatar-radius, var(--_avatar-nest, min(var(--pui-radius-card), calc(var(--_s) / 3))));\n    border-radius: var(--_r);\n  }\n  .photo {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: opacity var(--pui-fade-in) var(--pui-ease-default);\n  }\n  /* still on its way: the initials hold the place, and the photo fades in over them when it lands */\n  .photo[data-pending] {\n    opacity: 0;\n  }\n  /* The mark sits on the bottom inline-end corner; the face loses a disc one gap wider than the mark under it. */\n  .root[data-status] .face {\n    --_at: calc(var(--_d) / 2 - 1px);\n    mask: radial-gradient(\n      circle at calc(100% - var(--_at)) calc(100% - var(--_at)),\n      transparent calc(var(--_d) / 2 + var(--_g)),\n      black calc(var(--_d) / 2 + var(--_g) + 0.5px)\n    );\n  }\n  .root[data-status]:dir(rtl) .face {\n    mask: radial-gradient(\n      circle at var(--_at) calc(100% - var(--_at)),\n      transparent calc(var(--_d) / 2 + var(--_g)),\n      black calc(var(--_d) / 2 + var(--_g) + 0.5px)\n    );\n  }\n  .mark {\n    position: absolute;\n    inset-inline-end: -1px;\n    bottom: -1px;\n  }\n  /* a presence that changed settles into its new mark: the colour and shape arrive at once, the mark grows from a\n     little smaller (reduced motion keeps only the fade) */\n  .mark[data-changed] {\n    animation: presence-settle var(--pui-duration-feedback) var(--pui-ease-standard) both;\n  }\n\n  /* Presence: colour and shape together, so it reads in greyscale (DESIGN-LANGUAGE: colour is never the only signal).\n     online a full disc · away a crescent · busy a disc with a bar through it · offline a hollow ring. */\n  .presence {\n    --_d: 10px;\n    display: inline-block;\n    flex: none;\n    width: var(--_d);\n    height: var(--_d);\n    border-radius: 50%;\n    background: var(--_c);\n  }\n  .presence[data-size=\"xs\"] {\n    --_d: 6px;\n  }\n  .presence[data-size=\"sm\"] {\n    --_d: 8px;\n  }\n  .presence[data-size=\"lg\"] {\n    --_d: 12px;\n  }\n  .presence[data-size=\"xl\"] {\n    --_d: 14px;\n  }\n  .presence[data-status=\"online\"] {\n    --_c: var(--pui-green);\n  }\n  .presence[data-status=\"away\"] {\n    --_c: var(--pui-orange);\n    mask: radial-gradient(circle at 18% 18%, transparent calc(var(--_d) * 0.36), black calc(var(--_d) * 0.36 + 0.5px));\n  }\n  .presence[data-status=\"away\"]:dir(rtl) {\n    mask: radial-gradient(circle at 82% 18%, transparent calc(var(--_d) * 0.36), black calc(var(--_d) * 0.36 + 0.5px));\n  }\n  .presence[data-status=\"busy\"] {\n    --_c: var(--pui-red);\n    mask:\n      linear-gradient(black 0 0) center / 56% max(1.5px, 22%) no-repeat,\n      linear-gradient(black 0 0);\n    mask-composite: exclude;\n  }\n  .presence[data-status=\"offline\"] {\n    --_c: var(--pui-ink-3);\n    mask: radial-gradient(circle, transparent calc(var(--_d) * 0.24), black calc(var(--_d) * 0.24 + 0.5px));\n  }\n}\n@keyframes presence-settle {\n  from {\n    opacity: 0.4;\n    scale: calc(1 - 0.45 * var(--pui-motion-travel, 1));\n  }\n}\n"},{"path":"beamline/components/avatar/avatar.tsx","type":"registry:file","target":"src/ui/beamline/components/avatar/avatar.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { AvatarDefaults } from \"./defaults\";\nexport type { AvatarDefaults } from \"./defaults\";\n\nimport { useCallback, useState, type CSSProperties, type HTMLAttributes, type Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./avatar.module.css\";\n\nexport type AvatarSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\nexport type PresenceStatus = \"online\" | \"away\" | \"busy\" | \"offline\";\n\nconst SIZE: Record<AvatarSize, string | undefined> = {\n  xs: styles.xs,\n  sm: styles.sm,\n  md: undefined,\n  lg: styles.lg,\n  xl: styles.xl,\n};\n\n/** A person's (or account's) series hue, the same everywhere their name appears. */\nexport function hueOf(name: string) {\n  let h = 0;\n  for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) | 0;\n  return `var(--pui-series-${(Math.abs(h) % 8) + 1})`;\n}\nexport const initialsOf = (name: string) =>\n  name\n    .trim()\n    .split(/\\s+/)\n    .filter((w) => /\\p{L}|\\d/u.test(w[0] ?? \"\"))\n    .slice(0, 2)\n    .map((w) => w[0].toUpperCase())\n    .join(\"\") || \"?\";\n\n/** A presence mark for avatars and lists. Colour and shape together: online a disc, away a crescent, busy a disc with\n *  a bar, offline a hollow ring, so the status survives greyscale and colour-blindness. */\nexport function PresenceDot({\n  status,\n  size = \"md\",\n  className,\n  style,\n  ref,\n  ...props\n}: {\n  status: PresenceStatus;\n  size?: AvatarSize;\n  className?: string;\n  style?: CSSProperties;\n  ref?: Ref<HTMLSpanElement>;\n} & Omit<HTMLAttributes<HTMLSpanElement>, \"children\">) {\n  const i18n = useMessages(\"avatar\", englishMessages);\n  return (\n    <span\n      {...props}\n      style={style}\n      ref={ref}\n      role=\"img\"\n      aria-label={props[\"aria-label\"] ?? i18n(status)}\n      data-slot=\"presence-dot\"\n      data-status={status}\n      data-size={size}\n      className={cn(styles.presence, className)}\n    />\n  );\n}\n\nexport interface AvatarClassNames {\n  root?: string;\n  face?: string;\n  photo?: string;\n  presence?: string;\n}\n\nexport interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {\n  name: string;\n  /** A photo; initials show while it loads and if it fails. */\n  src?: string;\n  size?: AvatarSize;\n  /** circle for people, square for companies and workspaces. */\n  shape?: \"circle\" | \"square\";\n  status?: PresenceStatus;\n  ref?: Ref<HTMLSpanElement>;\n  classNames?: AvatarClassNames;\n}\n\n/** A person or account in a small space: a photo, or initials on the colour their name always gets. A photo still on\n *  its way fades in over the initials instead of popping; a presence that changes settles into its new mark. */\nexport function Avatar({\n  name,\n  src,\n  size: sizeProp,\n  shape = \"circle\",\n  status,\n  className,\n  classNames,\n  style,\n  ref,\n  ...props\n}: AvatarProps) {\n  const i18n = useMessages(\"avatar\", englishMessages);\n  const scopeDefaults = useComponentDefaults<AvatarDefaults>(\"avatar\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const [failed, setFailed] = useState<string | null>(null);\n  const photo = src && failed !== src;\n  // a photo that is not decoded yet when it mounts waits unseen and fades in over the initials when it lands; one that\n  // is already there (cached, or loaded before the page hydrated) shows at once, so a server render never hides it\n  const [pending, setPending] = useState<string | null>(null);\n  const watch = useCallback((img: HTMLImageElement | null) => {\n    if (!img) return;\n    if (!img.complete) setPending(img.getAttribute(\"src\"));\n    // A cached/decode failure can finish before React attaches onError during hydration.\n    else if (img.naturalWidth === 0) setFailed(img.getAttribute(\"src\"));\n  }, []);\n  // a presence that changes (online → away) settles into its new mark; the first render does not play it\n  const [seen, setSeen] = useState(status);\n  const [changes, setChanges] = useState(0);\n  if (seen !== status) {\n    setSeen(status);\n    setChanges((n) => n + 1);\n  }\n  return (\n    <span\n      ref={ref}\n      data-slot=\"avatar\"\n      data-shape={shape}\n      data-status={status}\n      role=\"img\"\n      aria-label={status ? i18n(\"presence\", { name, status: i18n(status) }) : name}\n      {...props}\n      className={cn(styles.root, SIZE[size], classNames?.root, className)}\n      style={{ \"--pui-avatar-hue\": hueOf(name), ...style } as CSSProperties}\n    >\n      <span aria-hidden data-slot=\"avatar-face\" className={cn(styles.face, classNames?.face)}>\n        {initialsOf(name)}\n        {photo && (\n          <img\n            key={src}\n            ref={watch}\n            data-slot=\"avatar-photo\"\n            data-pending={pending === src || undefined}\n            src={src}\n            alt=\"\"\n            draggable={false}\n            onLoad={() => setPending(null)}\n            onError={() => setFailed(src)}\n            className={cn(styles.photo, classNames?.photo)}\n          />\n        )}\n      </span>\n      {status && (\n        <PresenceDot\n          key={changes}\n          status={status}\n          size={size}\n          data-changed={changes > 0 || undefined}\n          className={cn(styles.mark, classNames?.presence)}\n        />\n      )}\n    </span>\n  );\n}\n\nexport default Avatar;\n"},{"path":"beamline/components/avatar/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/avatar/defaults.ts","content":"/** Pure allowed presentation defaults for Avatar; no component or runtime imports. */\nexport interface AvatarDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"xs\" | \"lg\" | \"xl\";\n}\n"},{"path":"beamline/components/avatar/messages.ts","type":"registry:file","target":"src/ui/beamline/components/avatar/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  online: \"Online\",\n  away: \"Away\",\n  busy: \"Do not disturb\",\n  offline: \"Offline\",\n  presence: \"{name}, {status}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/hover-card/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/hover-card/defaults.ts","content":"/** Pure allowed presentation defaults for HoverCard; no component or runtime imports. */\nexport interface HoverCardDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"center\" | \"end\";\n}\n"},{"path":"beamline/components/hover-card/hover-card.tsx","type":"registry:file","target":"src/ui/beamline/components/hover-card/hover-card.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { HoverCardDefaults } from \"./defaults\";\nexport type { HoverCardDefaults } from \"./defaults\";\n\nimport type { CSSProperties, Ref, ReactElement, ReactNode } from \"react\";\nimport * as RadixHoverCard from \"@radix-ui/react-hover-card\";\nimport { Avatar } from \"../avatar/avatar\";\nimport { SkeletonBlock } from \"../skeleton/skeleton\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport overlay from \"../_shared/overlay.module.css\";\n\nexport interface HoverCardClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n}\n\nexport interface HoverCardProps {\n  /** What is previewed: a link or button that can take a ref. It keeps its own click. */\n  children: ReactElement;\n  /** Content displayed inside the region. Required. */\n  content: ReactNode;\n  /** Edge from which the panel or anchored content opens. Defaults to \"bottom\". */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Alignment within the available space. Defaults to \"center\". */\n  align?: \"start\" | \"center\" | \"end\";\n  /** ms before it opens on hover; keyboard focus opens it at once. Default 400. */\n  openDelay?: number;\n  /** ms it stays after the pointer leaves, so the pointer can reach it. Default 200. */\n  closeDelay?: number;\n  /** Controlled open state; omit to use defaultOpen. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: HoverCardClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * A preview of a person, link or record on hover or keyboard focus, without leaving the page. It adds to a link that\n * already works; touch screens never see it, so nothing essential lives only here.\n */\nexport function HoverCard({\n  children,\n  content,\n  side = \"bottom\",\n  align: alignProp,\n  openDelay = 400,\n  closeDelay = 200,\n  open,\n  defaultOpen,\n  onOpenChange,\n  className,\n  classNames,\n  style,\n  ref,\n}: HoverCardProps) {\n  const scopeDefaults = useComponentDefaults<HoverCardDefaults>(\"hover-card\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"center\";\n\n  const container = usePortalContainer();\n  return (\n    <RadixHoverCard.Root\n      openDelay={openDelay}\n      closeDelay={closeDelay}\n      open={open}\n      defaultOpen={defaultOpen}\n      onOpenChange={onOpenChange}\n    >\n      <RadixHoverCard.Trigger asChild data-slot=\"hover-card-trigger\" className={classNames?.trigger}>\n        {children}\n      </RadixHoverCard.Trigger>\n      {container !== null && (\n        <RadixHoverCard.Portal container={container}>\n          <RadixHoverCard.Content\n            data-slot=\"hover-card\"\n            style={style}\n            ref={ref}\n            side={side}\n            align={align}\n            sideOffset={8}\n            collisionPadding={8}\n            className={cn(\n              overlay.surface,\n              overlay.padded,\n              \"max-w-[min(360px,calc(100vw-16px))]\",\n              classNames?.root,\n              className,\n            )}\n          >\n            {content}\n          </RadixHoverCard.Content>\n        </RadixHoverCard.Portal>\n      )}\n    </RadixHoverCard.Root>\n  );\n}\n\nexport interface HoverCardProfileClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the header part; no override by default. */\n  header?: string;\n  /** Classes for the avatar part; no override by default. */\n  avatar?: string;\n  /** Classes for the identity part; no override by default. */\n  identity?: string;\n  /** Classes for the name part; no override by default. */\n  name?: string;\n  /** Classes for the role part; no override by default. */\n  role?: string;\n  /** Classes for the about part; no override by default. */\n  about?: string;\n  /** Classes for the facts part; no override by default. */\n  facts?: string;\n  /** Classes for the fact part; no override by default. */\n  fact?: string;\n  /** Classes for the fact label part; no override by default. */\n  factLabel?: string;\n  /** Classes for the fact value part; no override by default. */\n  factValue?: string;\n}\n\nexport interface HoverCardProfileProps {\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: HoverCardProfileClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Person name shown in the profile. Required. */\n  name: string;\n  /** Role and team, e.g. \"Head of RevOps\". */\n  role?: ReactNode;\n  /** URL of the avatar image. Defaults to initials instead of a photo. */\n  avatarSrc?: string;\n  /** One or two lines: what they own, where they are. */\n  about?: ReactNode;\n  /** Small facts in a row, e.g. [[\"Deals\", \"14\"], [\"Local time\", \"16:20\"]]. */\n  facts?: [label: ReactNode, value: ReactNode][];\n  /** Presence indicator for the profile; omitted by default to show no presence dot. */\n  status?: \"online\" | \"offline\";\n  /** The person is still being fetched: the card shows the shape of the profile (avatar, name, role, a line, the\n   *  facts) instead of a spinner, so nothing moves when it arrives; the name, which is known, shows at once. */\n  loading?: boolean;\n}\n\n/** The standard person preview for a HoverCard's content. */\nexport function HoverCardProfile({\n  name,\n  role,\n  avatarSrc,\n  about,\n  facts,\n  status,\n  loading = false,\n  className,\n  classNames,\n  style,\n  ref,\n}: HoverCardProfileProps) {\n  if (loading)\n    return (\n      <div\n        data-slot=\"hover-card-profile\"\n        data-state=\"loading\"\n        aria-busy\n        className={cn(\"grid w-64 max-w-full gap-3\", classNames?.root, className)}\n        style={style}\n        ref={ref}\n      >\n        <div data-slot=\"hover-card-header\" className={cn(\"flex items-center gap-3\", classNames?.header)}>\n          <SkeletonBlock className=\"size-10 shrink-0 rounded-full\" />\n          <div data-slot=\"hover-card-identity\" className={cn(\"grid min-w-0 flex-1 gap-1.5\", classNames?.identity)}>\n            <div\n              data-slot=\"hover-card-name\"\n              className={cn(\"truncate text-base font-medium text-ink\", classNames?.name)}\n            >\n              {name}\n            </div>\n            <SkeletonBlock className=\"h-3 w-2/3\" />\n          </div>\n        </div>\n        <div className=\"grid gap-2.5\">\n          <SkeletonBlock className=\"h-4\" />\n          <SkeletonBlock className=\"h-4 w-4/5\" />\n        </div>\n        <div className=\"flex gap-5 border-t border-line-soft pt-2.5\">\n          <SkeletonBlock className=\"h-8 w-14\" />\n          <SkeletonBlock className=\"h-8 w-14\" />\n        </div>\n      </div>\n    );\n  return (\n    <div\n      data-slot=\"hover-card-profile\"\n      className={cn(\"grid w-64 max-w-full animate-enter-fade gap-3\", classNames?.root, className)}\n      style={style}\n      ref={ref}\n    >\n      <div data-slot=\"hover-card-header\" className={cn(\"flex items-center gap-3\", classNames?.header)}>\n        <Avatar name={name} src={avatarSrc} size=\"lg\" status={status} className={classNames?.avatar} />\n        <div data-slot=\"hover-card-identity\" className={cn(\"min-w-0\", classNames?.identity)}>\n          <div data-slot=\"hover-card-name\" className={cn(\"truncate text-base font-medium text-ink\", classNames?.name)}>\n            {name}\n          </div>\n          {role && (\n            <div data-slot=\"hover-card-role\" className={cn(\"truncate text-xs text-ink-3\", classNames?.role)}>\n              {role}\n            </div>\n          )}\n        </div>\n      </div>\n      {about && (\n        <p data-slot=\"hover-card-about\" className={cn(\"text-sm leading-relaxed text-ink-2\", classNames?.about)}>\n          {about}\n        </p>\n      )}\n      {facts?.length ? (\n        <dl\n          data-slot=\"hover-card-facts\"\n          className={cn(\"flex flex-wrap gap-x-5 gap-y-2 border-t border-line-soft pt-2.5\", classNames?.facts)}\n        >\n          {facts.map(([label, value], i) => (\n            <div key={i} data-slot=\"hover-card-fact\" className={cn(\"grid gap-0.5\", classNames?.fact)}>\n              <dt data-slot=\"hover-card-fact-label\" className={cn(\"text-2xs text-ink-3\", classNames?.factLabel)}>\n                {label}\n              </dt>\n              <dd\n                data-slot=\"hover-card-fact-value\"\n                className={cn(\"font-numeric text-sm text-ink\", classNames?.factValue)}\n              >\n                {value}\n              </dd>\n            </div>\n          ))}\n        </dl>\n      ) : null}\n    </div>\n  );\n}\n\nexport default HoverCard;\n"},{"path":"beamline/components/skeleton/messages.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  label: \"Loading content\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/skeleton/motion-features.ts","type":"registry:file","target":"src/ui/beamline/components/skeleton/motion-features.ts","content":"// REASON: Motion's DOM animation features (tween and presence) load after the first paint, so a Skeleton costs a\n// small part of Motion on first load; LazyMotion in skeleton.tsx renders until they arrive.\nimport { domAnimation } from \"motion/react\";\n\nexport default domAnimation;\n"},{"path":"beamline/components/skeleton/skeleton.tsx","type":"registry:file","target":"src/ui/beamline/components/skeleton/skeleton.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useRef, type ComponentProps, type CSSProperties, type ReactNode, type Ref } from \"react\";\nimport { AnimatePresence, LazyMotion, m } from \"motion/react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useHeightMorph } from \"../../hooks/use-height-morph\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { cn } from \"../../lib/utils\";\n\n// Motion's animation features arrive in a deferred chunk; the swap is instant until they land.\nconst features = () => import(\"./motion-features\").then((module) => module.default);\n\nexport interface SkeletonClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the placeholder part; no override by default. */\n  placeholder?: string;\n  /** Classes for the avatar part; no override by default. */\n  avatar?: string;\n  /** Classes for the lines part; no override by default. */\n  lines?: string;\n  /** Classes for the line part; no override by default. */\n  line?: string;\n  /** Classes for the content part; no override by default. */\n  content?: string;\n}\n\nexport interface SkeletonProps {\n  /** Announced to assistive tech while loading. */\n  label?: string;\n  /** Text lines in the placeholder; the last is shorter. */\n  lines?: number;\n  /** A round avatar before the lines. */\n  avatar?: boolean;\n  /** With children: the placeholder shows while true, the children after. */\n  loading?: boolean;\n  /** The real content. */\n  children?: ReactNode;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SkeletonClassNames;\n}\n\n// The sheen (utilities.css .sheen) slides a gradient layer with transform, so the compositor moves it without repainting\n// the placeholder on every frame; it pauses with the block (play state) and stops for reduced motion (data-still). A\n// scope's own motion choice (data-motion) and the OS preference also still it in CSS, so a block or group needs no\n// reader of the look's motion values: a part that only shows a placeholder stays small.\nconst SHEEN = \"sheen\";\n\n/** One placeholder shape: give it the width, height and radius of what will be there. */\nexport function SkeletonBlock({\n  className,\n  style,\n  ref,\n}: {\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n}) {\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <span\n      ref={attach}\n      aria-hidden\n      data-slot=\"skeleton-block\"\n      data-still={reduced ? \"\" : undefined}\n      className={cn(\"block h-3 rounded-chip\", SHEEN, className)}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n    />\n  );\n}\n\n/**\n * Many placeholder shapes under one visibility and motion check: a heatmap's cells, a chart's bars. Each shape is a\n * plain element with the `sheen` class anywhere inside the group, which pauses them off screen and stills them under\n * reduced motion together, so a 500-cell placeholder is one component instead of 500, and a list's loading rows can\n * keep the real rows' structure (and so their height).\n */\nexport function SkeletonGroup({ className, style, ref, ...props }: ComponentProps<\"div\">) {\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const reduced = useReducedMotionSafe();\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton-group\"\n      className={className}\n      style={{ \"--pui-sheen-play\": active ? \"running\" : \"paused\", ...style } as CSSProperties}\n      {...props}\n      // the shapes' styles key on these, so a caller's own data-slot (a chart's loading area) keeps its name\n      data-skeleton-group=\"\"\n      data-still={reduced ? \"\" : undefined}\n    />\n  );\n}\n\n/**\n * One line of text that has not arrived, inside a SkeletonGroup: an inline box exactly one line of the surrounding text\n * style tall (a zero-width character holds the line), with the shape centred in it. Put the real text's classes on it\n * (`text-sm`, `font-figure text-2xl leading-none`) and a width, and the loaded line takes its place without a shift.\n */\nexport function SkeletonLine({\n  className,\n  style,\n}: {\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n}) {\n  return (\n    <span\n      aria-hidden\n      data-slot=\"skeleton-line\"\n      className={cn(\"relative inline-block w-24 align-top\", className)}\n      style={style}\n    >\n      {\"\\u200b\"}\n      <span className=\"sheen absolute inset-x-0 top-1/2 block h-[0.7em] -translate-y-1/2 rounded-chip\" />\n    </span>\n  );\n}\n\n/** Holds the shape of content while it loads, then crossfades into it; if the content is taller or shorter than its\n *  placeholder, the height eases from one to the other, so what sits below glides instead of jumping. */\nexport function Skeleton({\n  label: labelProp,\n  lines = 3,\n  avatar = false,\n  loading = true,\n  children,\n  className,\n  classNames,\n  style,\n  ref,\n}: SkeletonProps) {\n  const i18n = useMessages(\"skeleton\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const host = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(host, ref);\n  const motionTokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = motionTokens.reduced || managedReduced;\n  const showPlaceholder = children === undefined || loading;\n  // the swap keeps its path: the box eases between the placeholder's height and the content's\n  useHeightMorph(host, showPlaceholder, { clip: true, duration: \"standard\", reduced });\n  // A crossfade: reduced motion keeps it restrained, none removes it (APPEARANCE A06).\n  const fade = {\n    duration: motionTokens.mode === \"none\" ? 0 : reduced ? motionTokens.fade.in : motionTokens.duration.standard,\n    ease: motionTokens.ease.default,\n  };\n  return (\n    <div\n      ref={attach}\n      data-slot=\"skeleton\"\n      data-state={showPlaceholder ? \"loading\" : \"loaded\"}\n      aria-busy={showPlaceholder || undefined}\n      className={cn(\"relative\", classNames?.root, className)}\n      style={style}\n    >\n      <LazyMotion features={features}>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {showPlaceholder ? (\n            <m.div\n              key=\"placeholder\"\n              role=\"status\"\n              aria-label={label}\n              data-slot=\"skeleton-placeholder\"\n              className={cn(\"flex items-start gap-3\", classNames?.placeholder)}\n              exit={{ opacity: 0 }}\n              transition={fade}\n            >\n              {avatar && <SkeletonBlock className={cn(\"size-9 shrink-0 rounded-full\", classNames?.avatar)} />}\n              <div\n                data-slot=\"skeleton-lines\"\n                className={cn(\"flex min-w-0 flex-1 flex-col gap-2.5 py-1\", classNames?.lines)}\n              >\n                {Array.from({ length: Math.max(1, lines) }, (_, i) => (\n                  <SkeletonBlock\n                    key={i}\n                    className={classNames?.line}\n                    style={{ width: i === lines - 1 && lines > 1 ? \"62%\" : i === 0 ? \"88%\" : \"100%\" }}\n                  />\n                ))}\n              </div>\n            </m.div>\n          ) : (\n            <m.div\n              data-slot=\"skeleton-content\"\n              className={classNames?.content}\n              key=\"content\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              transition={fade}\n            >\n              {children}\n            </m.div>\n          )}\n        </AnimatePresence>\n      </LazyMotion>\n    </div>\n  );\n}\n\nexport default Skeleton;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-height-morph.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-height-morph.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\n/**\n * The step tray: a card whose content is replaced (a sign-in step, the sign-up card, a form step) keeps its path. When\n * `key` changes, the element's height eases from what it was to what the new content needs, so the card's edge and\n * everything under it glide instead of jumping (the morph grammar's \"modes change size in place\"). The element clips\n * while it eases, with room for focus rings, unless `clip` is false (a slot whose ease only moves what is around it, like\n * the auth page re-centring its card, shows its content whole). Nothing runs at rest; reduced motion, motion \"none\" and a card off screen\n * switch at once; a change mid-way starts from where the card is. The height twin of hooks/use-width-morph; any element whose\n * content is replaced in place uses it (the auth cards, the step form, a dialog's stepped tray, the command menu's pages).\n */\nexport function useHeightMorph(\n  ref: RefObject<HTMLElement | null>,\n  key: unknown,\n  {\n    clip = true,\n    duration = \"morph\",\n    reduced: reducedOverride = false,\n  }: {\n    clip?: boolean;\n    /** The semantic duration the ease takes: a mode change morphs, a content swap (a skeleton) takes the standard. */\n    duration?: \"morph\" | \"standard\";\n    /** Also place at once when the caller knows motion is reduced by its own policy. */\n    reduced?: boolean;\n  } = {},\n) {\n  const motion = useMotionTokens(ref);\n  const last = useRef<number | null>(null);\n  const easing = useRef<Animation | null>(null);\n  // the height it last rested at, kept current when it changes on its own (an error line, a bot check) so the next ease\n  // starts where the card really is; the ease's own frames are not a resting height\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (!easing.current) last.current = el.getBoundingClientRect().height;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const from = easing.current ? el.getBoundingClientRect().height : last.current;\n    easing.current?.cancel();\n    easing.current = null;\n    const box = el.getBoundingClientRect();\n    const to = box.height;\n    last.current = to;\n    const ms = motion.duration[duration] * 1000;\n    if (from === null || Math.abs(from - to) < 0.5 || motion.reduced || reducedOverride || ms < 2) return;\n    // off screen nobody sees the glide: the card takes its height at once and no frame is spent on it\n    if (box.bottom < 0 || box.top > innerHeight) return;\n    // the ease runs between measured heights: a keyframe to \"auto\" (even with interpolate-size, or calc-size) does not\n    // interpolate in Chromium's Web Animations, it jumps at once; at the end the element is back on its own height\n    // the clip below leaves room for a focus ring or a control's shadow at the content's edge (inert at rest); a plain\n    // length, because Chromium computes any calc() here (and so the space tokens) as 0\n    el.style.setProperty(\"overflow-clip-margin\", \"8px\");\n    const ease = motion.ease.default;\n    const overflow = clip ? \"clip\" : \"visible\";\n    const animation = el.animate(\n      [\n        { height: `${from}px`, overflow },\n        { height: `${to}px`, overflow },\n      ],\n      { duration: ms, easing: cssEase(ease, \"ease-in-out\") },\n    );\n    easing.current = animation;\n    void animation.finished.then(\n      () => {\n        if (easing.current !== animation) return;\n        easing.current = null;\n        last.current = el.getBoundingClientRect().height;\n      },\n      () => {},\n    );\n  }, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- eases on a change of the step it stands for\n}\n"},{"path":"beamline/hooks/use-media-query.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-media-query.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// one MediaQueryList per query: React reads the snapshot on every render, and a part that renders thirty times a\n// second (a playing audio player's clock) would otherwise ask the browser for a new list each time\nconst lists = new Map<string, MediaQueryList>();\nconst list = (query: string) => {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return undefined;\n  let m = lists.get(query);\n  if (!m) lists.set(query, (m = window.matchMedia(query)));\n  return m;\n};\n\n/** True while the media query matches; false on the server and in the first client render of SSR output. */\nexport function useMediaQuery(query: string): boolean {\n  return useSyncExternalStore(\n    (notify) => {\n      const m = list(query);\n      m?.addEventListener(\"change\", notify);\n      return () => m?.removeEventListener(\"change\", notify);\n    },\n    () => list(query)?.matches ?? false,\n    () => false,\n  );\n}\n\n/** Phone-width layouts: below 768px. */\nexport const usePhone = () => useMediaQuery(\"(max-width: 767px)\");\n"},{"path":"beamline/hooks/use-motion-tokens.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-motion-tokens.ts","content":"\"use client\";\n\nimport { useMemo, type RefObject } from \"react\";\nimport { useAppearanceRoot } from \"../runtime/appearance\";\nimport { useResolvedAppearance } from \"./use-resolved-appearance\";\nimport { motionAt, motionEase, motionTokens, type MotionTokens } from \"../lib/motion-tokens\";\nimport { semanticMotion } from \"../lib/motion-source\";\n\nconst durationToken = (name: string) =>\n  name === \"exit\" ? \"motion-out\" : name === \"in\" || name === \"out\" ? `motion-${name}` : `duration-${name}`;\nconst easeToken = (name: string) => `ease-${name === \"inOut\" ? \"in-out\" : name}`;\nconst REQUEST = {\n  times: [\n    ...Object.keys(semanticMotion.duration).map(durationToken),\n    ...Object.keys(semanticMotion.spring).map((name) => `spring-${name}`),\n    ...Object.keys(semanticMotion.stagger).map((name) => `stagger-${name}`),\n    \"fade-in\",\n    \"fade-out\",\n  ],\n  easings: [...Object.keys(semanticMotion.ease).map(easeToken), \"ease-spring\", \"ease-spring-bounce\", \"ease-overshoot\"],\n  numbers: [\"motion-scale\", \"motion-look\", \"press-scale\"],\n  // timing only: the host's own type would make every control resolve differently from the page around it\n  typography: false,\n};\n\n/**\n * Resolve each timing at the actual host; omitted host uses the nearest managed root. The first/server snapshot is\n * the pure foundation default. Local CSS values, scope changes, font readiness, OS preference and explicit refresh\n * update the same consumer; no document-wide first-root fallback or persistent global listener.\n */\nexport function useMotionTokens(hostRef?: RefObject<HTMLElement | null>): MotionTokens & {\n  ready: boolean;\n  /** Reduced or none: movement, springs and staggers are instant. */\n  reduced: boolean;\n  mode: \"full\" | \"reduced\" | \"none\";\n  /** Opacity-only arrival and departure (seconds): kept under reduced, instant under none (APPEARANCE A06). */\n  fade: { in: number; out: number };\n} {\n  const root = useAppearanceRoot();\n  const scopeRef = useMemo(() => ({ current: root }), [root]);\n  const resolved = useResolvedAppearance(hostRef ?? scopeRef, REQUEST);\n  return useMemo(() => {\n    if (!resolved.ready)\n      return {\n        ...motionTokens,\n        ready: false,\n        reduced: false,\n        mode: \"full\" as const,\n        fade: { in: motionTokens.duration.in, out: motionTokens.duration.out },\n      };\n    const reduced = resolved.reducedMotion;\n    const speed = (resolved.numbers[\"motion-scale\"] ?? 1) * (resolved.numbers[\"motion-look\"] ?? 1);\n    const fade =\n      resolved.motionMode === \"none\"\n        ? { in: 0.001, out: 0.001 }\n        : {\n            in: resolved.times[\"fade-in\"] ?? semanticMotion.duration.in * speed,\n            out: resolved.times[\"fade-out\"] ?? semanticMotion.duration.out * speed,\n          };\n    const factor = reduced ? 0.001 / semanticMotion.duration.standard : speed;\n    const base = motionAt(factor);\n    const duration = Object.fromEntries(\n      Object.entries(base.duration).map(([name, fallback]) => [\n        name,\n        reduced ? 0.001 : (resolved.times[durationToken(name)] ?? fallback),\n      ]),\n    ) as MotionTokens[\"duration\"];\n    const stagger = Object.fromEntries(\n      Object.entries(base.stagger).map(([name, fallback]) => [\n        name,\n        reduced ? 0 : (resolved.times[`stagger-${name}`] ?? fallback),\n      ]),\n    ) as MotionTokens[\"stagger\"];\n    const ease = Object.fromEntries(\n      Object.entries(base.ease).map(([name, fallback]) => [\n        name,\n        motionEase(resolved.easings[easeToken(name)], fallback),\n      ]),\n    ) as MotionTokens[\"ease\"];\n    const spring = { ...base.spring };\n    for (const name of Object.keys(semanticMotion.spring) as (keyof typeof semanticMotion.spring)[])\n      spring[name] = {\n        ...base.spring[name],\n        visualDuration: reduced ? 0.001 : (resolved.times[`spring-${name}`] ?? base.spring[name].visualDuration),\n        bounce: reduced ? 0 : base.spring[name].bounce,\n      };\n    return {\n      ...base,\n      ready: true,\n      reduced,\n      mode: resolved.motionMode,\n      fade,\n      duration,\n      stagger,\n      ease,\n      spring,\n      pressScale: resolved.numbers[\"press-scale\"] ?? base.pressScale,\n      css: {\n        spring: resolved.easings[\"ease-spring\"] ?? base.css.spring,\n        springBounce: resolved.easings[\"ease-spring-bounce\"] ?? base.css.springBounce,\n        overshoot: resolved.easings[\"ease-overshoot\"] ?? base.css.overshoot,\n      },\n    };\n  }, [resolved]);\n}\n"},{"path":"beamline/hooks/use-reduced-motion-safe.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-reduced-motion-safe.ts","content":"\"use client\";\n\nimport { useMediaQuery } from \"./use-media-query\";\nimport { useScopeMotion } from \"../runtime/appearance\";\n\n/** Managed motion policy plus the OS preference, with a stable server snapshot. */\nexport function useReducedMotionSafe() {\n  const requested = useScopeMotion();\n  const os = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return requested !== \"auto\" || os;\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":"Hover card is in src/ui/beamline/components/hover-card/hover-card.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/hover-card"}}