{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"illustration","title":"Illustration","description":"Show a drawing of a cloud, a box, a credit card or a few documents, with an optional glyph, to give an empty state its picture. Search terms: empty state illustration, empty state picture, cloud illustration, no data drawing, zero state.","dependencies":[],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/illustration/illustration.tsx","content":"import * as React from \"react\";\n\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\n\nimport \"./illustration.css\";\n\n/** The drawing: a cloud, a box, a credit card or a few documents. */\nexport type IllustrationType = \"cloud\" | \"box\" | \"credit-card\" | \"documents\";\n\n/** `sm` is 120 px high, `md` 128 px and `lg`, the default, 160 px. */\nexport type IllustrationSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface IllustrationProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\" | \"title\"\n> {\n  /** Which drawing to show. */\n  type: IllustrationType;\n  /**\n   * The size: `sm` is 120 px high, `md` 128 px and `lg` 160 px. The width follows from the drawing's proportions. An\n   * Illustration in the media of an Empty takes the height of the Empty's size\n   * instead. `lg` by default.\n   */\n  size?: IllustrationSize | undefined;\n  /**\n   * The glyph that floats over the drawing in a frosted disc, such as a\n   * magnifier on the cloud or an upload arrow on the box. Pass a Phosphor icon\n   * with `aria-hidden=\"true\"`; its size comes from the disc. Without it the\n   * drawing stands alone.\n   */\n  children?: React.ReactNode;\n}\n\n/** The small discs that scatter around the large one: x, y and radius. */\nconst specks = [\n  [26, 20, 8],\n  [191, 11, 7],\n  [210, 46, 10],\n  [198, 126, 6],\n  [25, 138, 10],\n] as const;\n\n/** A rounded rectangle as a path, so a surface and its shade share one shape. */\nfunction roundedRect(\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  radius: number,\n): string {\n  const r = radius;\n  return `M${x + r} ${y}H${x + width - r}A${r} ${r} 0 0 1 ${x + width} ${y + r}V${y + height - r}A${r} ${r} 0 0 1 ${x + width - r} ${y + height}H${x + r}A${r} ${r} 0 0 1 ${x} ${y + height - r}V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`;\n}\n\n/**\n * A shape that is lit from its top start corner: the opaque surface, then the\n * shade that fades out of the corner, then, when the shape asks for it, its\n * hairline edge.\n */\nfunction Lit({\n  d,\n  edge = true,\n  shade,\n}: {\n  d: string;\n  edge?: boolean;\n  shade: string;\n}) {\n  return (\n    <>\n      <path className=\"sp-illustration-surface\" d={d} />\n      <path d={d} fill={`url(#${shade})`} />\n      {edge ? <path className=\"sp-illustration-edge\" d={d} /> : null}\n    </>\n  );\n}\n\n/** A cloud of three lobes on a flat base, shaded lobe by lobe. */\nfunction Cloud({ shade }: { shade: string }) {\n  const outline =\n    \"M63.77 115.43A39.77 39.77 0 1 1 72.94 36.95A49.71 49.71 0 0 1 159.16 46.06A34.8 34.8 0 1 1 163.2 115.43Z\";\n  return (\n    <>\n      <path className=\"sp-illustration-surface\" d={outline} />\n      <circle cx=\"63.77\" cy=\"75.66\" fill={`url(#${shade})`} r=\"39.77\" />\n      <circle cx=\"113.49\" cy=\"65.71\" fill={`url(#${shade})`} r=\"49.71\" />\n      <circle cx=\"163.2\" cy=\"80.63\" fill={`url(#${shade})`} r=\"34.8\" />\n      <path className=\"sp-illustration-edge\" d={outline} />\n    </>\n  );\n}\n\n/** A closed box seen from above and aside, with a slit on its lid. */\nfunction Box({ shade }: { shade: string }) {\n  const outline = \"M110 12 168 39V109L110 136 52 109V39Z\";\n  return (\n    <>\n      <path className=\"sp-illustration-surface\" d={outline} />\n      <path className=\"sp-illustration-depth\" d=\"M52 39 110 66V136L52 109Z\" />\n      <path d=\"M110 66 168 39V109L110 136Z\" fill={`url(#${shade})`} />\n      <path className=\"sp-illustration-fold\" d=\"M110 29 132 39 110 49 88 39Z\" />\n      <path className=\"sp-illustration-edge\" d={outline} />\n      <path\n        className=\"sp-illustration-edge\"\n        d=\"M52 39 110 66 168 39M110 66V136\"\n      />\n    </>\n  );\n}\n\n/** A card with a chip, two overlapping discs and four groups of digits. */\nfunction CreditCard({ shade }: { shade: string }) {\n  return (\n    <>\n      <Lit d={roundedRect(42, 36, 136, 88, 10)} shade={shade} />\n      <path\n        className=\"sp-illustration-fold\"\n        d={roundedRect(58, 52, 22, 16, 3)}\n      />\n      <circle className=\"sp-illustration-fold\" cx=\"146\" cy=\"62\" r=\"10\" />\n      <circle className=\"sp-illustration-fold\" cx=\"158\" cy=\"62\" r=\"10\" />\n      {[58, 84, 110, 136].map((x) => (\n        <path\n          className=\"sp-illustration-fold\"\n          d={roundedRect(x, 98, 20, 5, 2.5)}\n          key={x}\n        />\n      ))}\n      <path\n        className=\"sp-illustration-fold\"\n        d={roundedRect(58, 108, 44, 5, 2.5)}\n      />\n    </>\n  );\n}\n\n/** One sheet of paper with a folded corner and three lines of text. */\nfunction Sheet({\n  angle = 0,\n  shade,\n  x,\n  y,\n}: {\n  angle?: number;\n  shade: string;\n  x: number;\n  y: number;\n}) {\n  const width = 58;\n  const height = 76;\n  const fold = 14;\n  const r = 6;\n  const d = `M${x + r} ${y}H${x + width - fold}L${x + width} ${y + fold}V${y + height - r}A${r} ${r} 0 0 1 ${x + width - r} ${y + height}H${x + r}A${r} ${r} 0 0 1 ${x} ${y + height - r}V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`;\n  return (\n    <g transform={`rotate(${angle} ${x + width / 2} ${y + height / 2})`}>\n      <Lit d={d} shade={shade} />\n      <path\n        className=\"sp-illustration-fold\"\n        d={`M${x + width - fold} ${y}V${y + fold}H${x + width}Z`}\n      />\n      {[28, 40, 52].map((offset, index) => (\n        <path\n          className=\"sp-illustration-fold\"\n          d={roundedRect(x + 12, y + offset, index === 2 ? 22 : 34, 4, 2)}\n          key={offset}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction Documents({ shade }: { shade: string }) {\n  return (\n    <>\n      <Sheet angle={-8} shade={shade} x={34} y={46} />\n      <Sheet angle={8} shade={shade} x={128} y={46} />\n      <Sheet shade={shade} x={81} y={24} />\n    </>\n  );\n}\n\nconst drawings = {\n  box: Box,\n  \"credit-card\": CreditCard,\n  cloud: Cloud,\n  documents: Documents,\n} as const satisfies Record<\n  IllustrationType,\n  (props: { shade: string }) => React.ReactElement\n>;\n\n/**\n * A drawing for an empty state: a cloud, a box, a credit card or a few\n * documents on a soft disc with a few specks, an optional glyph floating over\n * it in a frosted disc. It is drawn in the kit's surface\n * and edge tokens, so it follows the theme, and it is decoration: it is hidden\n * from assistive technology unless it carries an `aria-label`, which makes it\n * an image. Put it in an EmptyMedia with `variant=\"illustration\"` to give an\n * empty state its picture.\n */\nexport function Illustration({\n  children,\n  className,\n  size = \"lg\",\n  type,\n  ...props\n}: IllustrationProps): React.ReactElement {\n  const id = React.useId();\n  const shade = `${id}-shade`;\n  const shadow = `${id}-shadow`;\n  const Drawing = drawings[type];\n  const labelled = props[\"aria-label\"] !== undefined;\n  return (\n    <div\n      aria-hidden={labelled ? undefined : true}\n      className={cn(slotClass(\"illustration\", props), className)}\n      data-size={size}\n      data-slot=\"illustration\"\n      data-type={type}\n      role={labelled ? \"img\" : undefined}\n      {...props}\n    >\n      <svg\n        className=\"sp-illustration-svg\"\n        data-slot=\"illustration-svg\"\n        fill=\"none\"\n        focusable=\"false\"\n        viewBox=\"0 0 220 160\"\n      >\n        <defs>\n          <linearGradient id={shade} x1=\"0.1\" x2=\"1\" y1=\"0\" y2=\"1\">\n            <stop className=\"sp-illustration-shade-start\" offset=\"0\" />\n            <stop className=\"sp-illustration-shade-end\" offset=\"0.4\" />\n          </linearGradient>\n          <filter height=\"150%\" id={shadow} width=\"130%\" x=\"-15%\" y=\"-15%\">\n            <feDropShadow\n              className=\"sp-illustration-contact\"\n              dx=\"0\"\n              dy=\"2\"\n              stdDeviation=\"1.5\"\n            />\n            <feDropShadow\n              className=\"sp-illustration-ambient\"\n              dx=\"0\"\n              dy=\"14\"\n              stdDeviation=\"10\"\n            />\n          </filter>\n        </defs>\n        <circle className=\"sp-illustration-disc\" cx=\"110\" cy=\"80\" r=\"80\" />\n        {specks.map(([cx, cy, r]) => (\n          <circle\n            className=\"sp-illustration-disc\"\n            cx={cx}\n            cy={cy}\n            key={`${cx}-${cy}`}\n            r={r}\n          />\n        ))}\n        <g className=\"sp-illustration-object\" filter={`url(#${shadow})`}>\n          <Drawing shade={shade} />\n        </g>\n      </svg>\n      {children ? (\n        <span className=\"sp-illustration-badge\" data-slot=\"illustration-badge\">\n          {children}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/illustration/illustration.tsx"},{"path":"packages/ui/src/components/illustration/illustration.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* An Illustration is an empty-state drawing in the kit's surface\n   * and edge tokens: a soft disc with a few specks, an object on it that\n   * casts a shadow, and an optional glyph in a frosted disc over the object's\n   * lower middle. One drawing at one proportion (220 by 160); the size is its\n   * height, 120, 128 or 160 px, and the width follows. The frosted disc and\n   * its glyph are measured against that height, so they scale with it, which\n   * is also what lets an Empty give the illustration its own height. The\n   * defaults sit at zero specificity, so the media of an Empty, which sets the\n   * height of its size, replaces them (P6). */\n  :where(.sp-illustration) {\n    position: relative;\n    display: inline-block;\n    height: calc(var(--sp-rw-space) * 40);\n    flex-shrink: 0;\n    aspect-ratio: 220 / 160;\n  }\n\n  :where(.sp-illustration[data-size=\"md\"]) {\n    height: calc(var(--sp-rw-space) * 32);\n  }\n\n  :where(.sp-illustration[data-size=\"sm\"]) {\n    height: calc(var(--sp-rw-space) * 30);\n  }\n\n  :root .sp-illustration-svg {\n    display: block;\n    width: 100%;\n    height: 100%;\n    overflow: visible;\n  }\n\n  /* The disc and the specks are the muted surface tint, so they follow the\n   * theme and show through nothing. */\n  :root .sp-illustration-disc {\n    fill: var(--sp-rw-bg-muted);\n  }\n\n  /* The object: an opaque surface with a hairline edge, lit from its top start\n   * corner by a shade that fades out of it, and the folds, lines and marks\n   * drawn on it in the edge tone. */\n  :root .sp-illustration-surface {\n    fill: var(--sp-rw-bg-secondary);\n  }\n\n  :root .sp-illustration-depth {\n    fill: var(--sp-rw-bg-depth);\n  }\n\n  :root .sp-illustration-fold {\n    fill: var(--sp-rw-border-translucent);\n  }\n\n  :root .sp-illustration-edge {\n    fill: none;\n    stroke: var(--sp-rw-border-translucent);\n    stroke-linejoin: round;\n    stroke-width: 1;\n  }\n\n  :root .sp-illustration-shade-start {\n    stop-color: var(--sp-rw-bg-depth);\n  }\n\n  :root .sp-illustration-shade-end {\n    stop-color: var(--sp-rw-bg-depth);\n    stop-opacity: 0;\n  }\n\n  /* The shadow under the object, a tight contact shadow and a long soft one, in\n   * black at the kit's shadow strengths; darker in Dark. */\n  :root .sp-illustration-contact {\n    flood-color: oklch(0% 0 0);\n    flood-opacity: 0.06;\n  }\n\n  :root .sp-illustration-ambient {\n    flood-color: oklch(0% 0 0);\n    flood-opacity: 0.08;\n  }\n\n  :where(.dark, [data-theme=\"dark\"]) .sp-illustration-contact {\n    flood-opacity: 0.24;\n  }\n\n  :where(.dark, [data-theme=\"dark\"]) .sp-illustration-ambient {\n    flood-opacity: 0.32;\n  }\n\n  /* The glyph floats at the bottom middle of the drawing in a frosted disc: a\n   * translucent dark disc that blurs what is behind it, with a white glyph at\n   * half its size. Its size and its distance from the bottom are shares of the\n   * illustration's height. */\n  :root .sp-illustration-badge {\n    position: absolute;\n    bottom: 12.5%;\n    left: 50%;\n    z-index: 10;\n    display: flex;\n    height: 35%;\n    aspect-ratio: 1;\n    align-items: center;\n    justify-content: center;\n    border-radius: var(--sp-rw-radius-full);\n    background: oklch(0% 0 0 / 20%);\n    color: var(--sp-rw-fg-on-accent);\n    translate: -50% 0;\n    -webkit-backdrop-filter: blur(var(--sp-rw-space-1));\n    backdrop-filter: blur(var(--sp-rw-space-1));\n  }\n\n  :root .sp-illustration[data-size=\"md\"] .sp-illustration-badge {\n    bottom: 9.375%;\n    height: 43.75%;\n  }\n\n  :root .sp-illustration[data-size=\"sm\"] .sp-illustration-badge {\n    bottom: 6.667%;\n    height: 40%;\n  }\n\n  :root .sp-illustration-badge > svg {\n    width: 50%;\n    height: 50%;\n    flex-shrink: 0;\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/illustration/illustration.css"},{"path":"packages/ui/src/components/illustration/index.ts","content":"export * from \"./illustration\";\n","type":"registry:ui","target":"~/src/siteplane/components/illustration/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/illustration.md","categories":["Feedback"],"meta":{"avoidWhen":["Avoid in a compact empty state, such as one in a table or a popover; use a Featured Icon in EmptyMedia.","Avoid as the only carrier of a message; the title and the description of the empty state say what it means."],"description":"Show a drawing of a cloud, a box, a credit card or a few documents, with an optional glyph, to give an empty state its picture.","knownLimitations":["Four drawings: cloud, box, credit-card and documents, at one proportion of 220 by 160.","The glyph disc is a share of the height of the Illustration's own size; in an Empty State, give it the size of the Empty too."],"providerNotes":[],"releaseStatus":"preview","searchTerms":["illustration","empty state illustration","empty state picture","cloud illustration","no data drawing","zero state"],"useWhen":["Use in the media of an Empty State with variant illustration, for an empty page, panel or large card such as no files uploaded or a failed payment.","Use the glyph to say what the empty state is about, such as a magnifier for no results or an upload arrow for no files."]}}