{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"empty-state","type":"registry:component","title":"Empty state","description":"What a place will hold and the first step to fill it, instead of a blank; a compact size for panels and filtered tables.","categories":["feedback"],"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/empty-state/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/empty-state/defaults.ts","content":"/** Pure allowed presentation defaults for EmptyState; no component or runtime imports. */\nexport interface EmptyStateDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/components/empty-state/empty-state.module.css","type":"registry:file","target":"src/ui/beamline/components/empty-state/empty-state.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* An empty state answers to its room (data-room, read from its own width once per change: it is often centred in a\n     panel, where a size container would have no width of its own). Its air, icon ring and actions step with that room;\n     a caller's own padding or width classes still win (they sit in the utilities layer above this one). It arrives with\n     a short fade, as it usually replaces a list that just emptied. */\n  .root {\n    display: grid;\n    justify-items: center;\n    text-align: center;\n    animation: arrive var(--pui-fade-in) var(--pui-ease-default) both;\n  }\n  @keyframes arrive {\n    from {\n      opacity: 0;\n    }\n  }\n  .root[data-size=\"md\"] {\n    gap: var(--pui-space-3);\n    padding: var(--pui-space-10) var(--pui-space-6);\n  }\n  .root[data-size=\"sm\"] {\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-6) var(--pui-space-4);\n  }\n  /* a big room (a whole page or panel body) gets more air above and below */\n  .root[data-size=\"md\"][data-room=\"wide\"] {\n    padding-block: var(--pui-space-16);\n  }\n  /* a narrow room (a sidebar, a phone column, a small tile): less air, the dashed ring folds away, and the actions\n     stack full width so each is an easy target */\n  .root[data-room=\"narrow\"] {\n    gap: var(--pui-space-2);\n    padding: var(--pui-space-5) var(--pui-space-3);\n  }\n  .root[data-room=\"narrow\"] .ring {\n    display: none;\n  }\n  .root[data-room=\"narrow\"] > .actions {\n    flex-direction: column;\n    justify-self: stretch;\n  }\n  .root[data-room=\"narrow\"] > .actions > * {\n    width: 100%;\n  }\n\n  /* layout row: a strip for a list body or a table section — icon, words and actions in one line from 32rem, the\n     stack below it */\n  .root[data-row] {\n    grid-template-columns: auto minmax(0, 1fr) auto;\n    align-items: center;\n    justify-items: start;\n    column-gap: var(--pui-space-4);\n    text-align: start;\n  }\n  .root[data-row] > .icon {\n    margin: 0 var(--pui-space-2);\n  }\n  .root[data-row] > .actions {\n    margin-top: 0;\n    justify-content: flex-end;\n  }\n\n  .icon {\n    position: relative;\n    display: grid;\n    place-items: center;\n    border-radius: var(--pui-radius-panel);\n    color: var(--pui-ink-2);\n  }\n  .root[data-size=\"md\"] > .icon {\n    width: var(--pui-space-12);\n    height: var(--pui-space-12);\n    margin-bottom: var(--pui-space-1);\n  }\n  .root[data-size=\"sm\"] > .icon {\n    width: calc(var(--pui-space-1) * 9);\n    height: calc(var(--pui-space-1) * 9);\n  }\n  .icon > svg {\n    width: var(--pui-icon-lg);\n    height: var(--pui-icon-lg);\n  }\n  .root[data-size=\"sm\"] > .icon > svg {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  /* the dashed ring around the icon says \"a place waiting to be filled\"; it firms up while the next step is pointed at\n     or focused, tying the picture to the action */\n  .ring {\n    position: absolute;\n    inset: calc(var(--pui-space-2) * -1);\n    border: 1px dashed var(--pui-line);\n    border-radius: var(--pui-radius-surface);\n    transition: border-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:has(> .actions :is(:hover, :focus-visible)) .ring {\n    border-color: var(--pui-line-strong);\n    transition-duration: 0ms;\n  }\n  .body {\n    display: grid;\n    gap: var(--pui-space-1);\n    max-width: 44ch;\n  }\n  .actions {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: center;\n    gap: var(--pui-space-2);\n    margin-top: var(--pui-space-1);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.icon {\n  composes: m-control from global;\n}\n"},{"path":"beamline/components/empty-state/empty-state.tsx","type":"registry:file","target":"src/ui/beamline/components/empty-state/empty-state.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { EmptyStateDefaults } from \"./defaults\";\nexport type { EmptyStateDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./empty-state.module.css\";\n\nexport interface EmptyStateClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the icon part; no override by default. */\n  icon?: string;\n  /** Classes for the ring part; no override by default. */\n  ring?: string;\n  /** Classes for the body part; no override by default. */\n  body?: string;\n  /** Classes for the title part; no override by default. */\n  title?: string;\n  /** Classes for the description part; no override by default. */\n  description?: string;\n  /** Classes for the actions part; no override by default. */\n  actions?: string;\n}\n\nexport interface EmptyStateProps {\n  /** Say what will be here: \"No reports yet\". */\n  title: ReactNode;\n  /** Why it is empty and what happens next. */\n  description?: ReactNode;\n  /** The next step: create, import, invite. */\n  action?: ReactNode;\n  /** A quieter second step: docs, a sample. */\n  secondary?: ReactNode;\n  /** Icon element shown beside the label. Defaults to no leading icon. */\n  icon?: ReactNode;\n  /** compact for panels and table bodies. */\n  size?: \"md\" | \"sm\";\n  /** stack (default): centred. row: a strip for a list body or table section — icon, words and actions in one line\n   *  from 32rem, the stack below. */\n  layout?: \"stack\" | \"row\";\n  /** The region's name, when the title is not text. */\n  label?: string;\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?: EmptyStateClassNames;\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<HTMLElement>;\n}\n\n/** What a place will hold and the first step to fill it, instead of a blank. For filters with no results, say which\n *  filter to loosen. */\nexport function EmptyState({\n  title,\n  description,\n  action,\n  secondary,\n  icon,\n  size: sizeProp,\n  layout = \"stack\",\n  label,\n  className,\n  classNames,\n  style,\n  ref,\n}: EmptyStateProps) {\n  const scopeDefaults = useComponentDefaults<EmptyStateDefaults>(\"empty-state\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  const host = useRef<HTMLElement>(null);\n  const attach = useComposedRef(host, ref);\n  // the room, read once per change of step: narrow below 16rem, wide from 42rem, a row (for layout row) from 32rem of\n  // content at the default padding. Read on the outer width: the padding steps with the room, so the content width\n  // would flip it back and forth every frame when it sits near a step.\n  const pad = size === \"sm\" ? 32 : 48;\n  const room = useWidthAs(\n    host,\n    (w) => {\n      const s = host.current ? getComputedStyle(host.current) : null;\n      const outer = w + (s ? parseFloat(s.paddingLeft) + parseFloat(s.paddingRight) : pad) - pad;\n      return outer < 256 ? \"narrow\" : outer >= 672 ? \"wide\" : outer >= 512 ? \"row\" : \"default\";\n    },\n    \"default\",\n  );\n\n  return (\n    <section\n      ref={attach}\n      style={style}\n      data-slot=\"empty-state\"\n      data-size={size}\n      data-room={room === \"row\" ? \"default\" : room}\n      data-row={layout === \"row\" && (room === \"row\" || room === \"wide\") ? \"\" : undefined}\n      aria-label={label ?? (typeof title === \"string\" ? title : undefined)}\n      className={cn(styles.root, classNames?.root, className)}\n    >\n      {icon && (\n        <span data-slot=\"empty-state-icon\" aria-hidden className={cn(styles.icon, classNames?.icon)}>\n          <span data-slot=\"empty-state-ring\" className={cn(styles.ring, classNames?.ring)} />\n          {icon}\n        </span>\n      )}\n      <div data-slot=\"empty-state-body\" className={cn(styles.body, classNames?.body)}>\n        <h3\n          data-slot=\"empty-state-title\"\n          className={cn(\"text-ink\", size === \"md\" ? \"text-subheading\" : \"text-label\", classNames?.title)}\n        >\n          {title}\n        </h3>\n        {description && (\n          <p\n            data-slot=\"empty-state-description\"\n            className={cn(\"leading-relaxed text-ink-3\", size === \"md\" ? \"text-sm\" : \"text-xs\", classNames?.description)}\n          >\n            {description}\n          </p>\n        )}\n      </div>\n      {(action || secondary) && (\n        <div data-slot=\"empty-state-actions\" className={cn(styles.actions, classNames?.actions)}>\n          {action}\n          {secondary}\n        </div>\n      )}\n    </section>\n  );\n}\n\nexport default EmptyState;\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-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\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":"Empty state is in src/ui/beamline/components/empty-state/empty-state.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/empty-state"}}