{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "side-panel",
  "title": "Side Panel",
  "description": "A full-height, non-modal panel that opens beside the page, for details, a form or anything else.",
  "dependencies": [
    "@base-ui/react",
    "@hugeicons/core-free-icons",
    "@hugeicons/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json",
    "https://constella.aayush.cv/r/button.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/side-panel.tsx",
      "content": "\"use client\";\n\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { Cancel01Icon } from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { ComponentProps, RefObject } from \"react\";\n\nimport { Button } from \"@/components/primitives/button\";\nimport type { Sx } from \"@/lib/constella/styles\";\nimport { typeStyles } from \"@/lib/constella/styles\";\nimport {\n  colors,\n  durations,\n  easings,\n  fontWeights,\n  media,\n  radii,\n  shadows,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\ntype Styled<T> = Omit<T, \"className\" | \"style\"> & {\n  sx?: Sx;\n};\n\ntype SidePanelContentProps = Styled<DialogPrimitive.Popup.Props> & {\n  /** The `SidePanelSlot` to open in, beside the content. */\n  container: RefObject<HTMLElement | null>;\n};\n\nconst entering = \":is([data-starting-style])\";\n\nconst styles = stylex.create({\n  // Held in view below the header row while the content beside it scrolls.\n  slot: {\n    alignSelf: \"flex-start\",\n    display: \"flex\",\n    flexDirection: \"column\",\n    flexShrink: 0,\n    height: `calc(100svh - ${sizes.header})`,\n    insetBlockStart: sizes.header,\n    position: \"sticky\",\n  },\n  // Base UI wraps the popup in the portal's own element, which has to fill the slot too.\n  portal: {\n    display: \"flex\",\n    flexDirection: \"column\",\n    flexGrow: 1,\n    minHeight: 0,\n  },\n  // Beside the content, lifted off it as the composer is: its fill and its edge a ring in its shadow.\n  surface: {\n    backgroundColor: colors.raised,\n    boxShadow: shadows.panel,\n    borderStartStartRadius: radii.lg,\n    borderStartEndRadius: radii.lg,\n    borderEndStartRadius: radii.lg,\n    borderEndEndRadius: radii.lg,\n    color: colors.textPrimary,\n    display: \"flex\",\n    flexDirection: \"column\",\n    flexShrink: 0,\n    minHeight: 0,\n    overflow: \"hidden\",\n  },\n  // Inset from the content and the window's edges.\n  placed: {\n    marginBlockEnd: space.row,\n    marginBlockStart: space.row,\n    marginInlineEnd: space.row,\n    marginInlineStart: space.row,\n    maxWidth: `calc(100vw - 2 * ${space.row})`,\n    width: sizes.sidePanel,\n  },\n  popup: {\n    flexGrow: 1,\n    opacity: { default: 1, [entering]: 0 },\n    overflow: \"hidden\",\n    transform: {\n      default: \"none\",\n      [entering]: {\n        default: `translateX(${space.sm})`,\n        [media.reducedMotion]: \"none\",\n      },\n    },\n    // Closes at once, so the content beside it widens in one step.\n    transitionDuration: {\n      default: durations.dialog,\n      \":is([data-ending-style])\": \"0s\",\n    },\n    transitionProperty: \"opacity, transform\",\n    transitionTimingFunction: easings.layout,\n  },\n  // The header row less the panel's inset above and below, so a panel inset by `space.row`\n  // centres its controls on the header row's line.\n  header: {\n    alignItems: \"center\",\n    borderBlockEndColor: colors.borderSubtle,\n    borderBlockEndStyle: \"solid\",\n    borderBlockEndWidth: strokes.border,\n    display: \"flex\",\n    flexShrink: 0,\n    gap: space.row,\n    height: `calc(${sizes.header} - 2 * ${space.row})`,\n    paddingInlineEnd: space.row,\n    paddingInlineStart: space.row,\n  },\n  // Its text on the body's edge: the strip's padding plus this.\n  title: {\n    flexGrow: 1,\n    paddingInlineStart: space.popup,\n    fontWeight: fontWeights.medium,\n    marginBlockEnd: 0,\n    marginBlockStart: 0,\n    minWidth: 0,\n    overflow: \"hidden\",\n    textOverflow: \"ellipsis\",\n    whiteSpace: \"nowrap\",\n  },\n  panelTitle: {\n    fontWeight: fontWeights.semibold,\n  },\n  // First in the body: the header is one row, with no room under the title.\n  description: {\n    color: colors.textSecondary,\n    marginBlockEnd: space.md,\n    marginBlockStart: 0,\n    marginInlineEnd: 0,\n    marginInlineStart: 0,\n    textWrap: \"pretty\",\n  },\n  body: {\n    flexGrow: 1,\n    minHeight: 0,\n    overflowY: \"auto\",\n    paddingBlockEnd: space.md,\n    paddingBlockStart: space.md,\n    paddingInlineEnd: space.md,\n    paddingInlineStart: space.md,\n  },\n  footer: {\n    display: \"flex\",\n    flexDirection: \"column\",\n    flexShrink: 0,\n    gap: space.xs,\n    paddingBlockEnd: space.md,\n    paddingBlockStart: space.md,\n    paddingInlineEnd: space.md,\n    paddingInlineStart: space.md,\n  },\n});\n\n/** The side panel's surface without the dialog or its place: the page sizes and places it. */\nfunction Panel({ sx, ...props }: Styled<ComponentProps<\"aside\">>) {\n  return (\n    <aside\n      data-slot=\"panel\"\n      {...props}\n      {...stylex.props([typeStyles.textSm, styles.surface], sx)}\n    />\n  );\n}\n\n/** The panel's top strip: the title first, any actions after it. */\nfunction PanelHeader({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"panel-header\"\n      {...props}\n      {...stylex.props(styles.header, sx)}\n    />\n  );\n}\n\nfunction PanelTitle({ children, sx, ...props }: Styled<ComponentProps<\"h2\">>) {\n  return (\n    <h2\n      data-slot=\"panel-title\"\n      {...props}\n      {...stylex.props(\n        [typeStyles.textBase, styles.title, styles.panelTitle],\n        sx\n      )}\n    >\n      {children}\n    </h2>\n  );\n}\n\n/** Scrolls on its own once the panel's height can't hold it. */\nfunction PanelBody({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div data-slot=\"panel-body\" {...props} {...stylex.props(styles.body, sx)} />\n  );\n}\n\n/** A non-modal dialog: clicking the content beside it leaves it open; Escape or Close shuts it. */\nfunction SidePanel(props: DialogPrimitive.Root.Props) {\n  return (\n    <DialogPrimitive.Root disablePointerDismissal modal={false} {...props} />\n  );\n}\n\n/** Where the panel opens: after the content it sits beside, in a flex row. */\nfunction SidePanelSlot({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"side-panel-slot\"\n      {...props}\n      {...stylex.props(styles.slot, sx)}\n    />\n  );\n}\n\nfunction SidePanelContent({ container, sx, ...props }: SidePanelContentProps) {\n  return (\n    <DialogPrimitive.Portal\n      container={container}\n      {...stylex.props(styles.portal)}\n    >\n      <DialogPrimitive.Popup\n        data-slot=\"side-panel\"\n        {...props}\n        {...stylex.props(\n          [typeStyles.textSm, styles.surface, styles.placed, styles.popup],\n          sx\n        )}\n      />\n    </DialogPrimitive.Portal>\n  );\n}\n\n/** The panel's top strip: the title first, any actions after it, Close last. */\nfunction SidePanelHeader({\n  children,\n  sx,\n  ...props\n}: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"side-panel-header\"\n      {...props}\n      {...stylex.props(styles.header, sx)}\n    >\n      {children}\n      <DialogPrimitive.Close\n        aria-label=\"Close\"\n        render={<Button size=\"icon-sm\" variant=\"ghost\" />}\n      >\n        <HugeiconsIcon\n          aria-hidden\n          icon={Cancel01Icon}\n          size={sizes.icon}\n          strokeWidth={Number(strokes.icon)}\n        />\n      </DialogPrimitive.Close>\n    </div>\n  );\n}\n\nfunction SidePanelTitle({ sx, ...props }: Styled<DialogPrimitive.Title.Props>) {\n  return (\n    <DialogPrimitive.Title\n      data-slot=\"side-panel-title\"\n      {...props}\n      {...stylex.props([typeStyles.textLg, styles.title], sx)}\n    />\n  );\n}\n\nfunction SidePanelDescription({\n  sx,\n  ...props\n}: Styled<DialogPrimitive.Description.Props>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"side-panel-description\"\n      {...props}\n      {...stylex.props([typeStyles.textSm, styles.description], sx)}\n    />\n  );\n}\n\nfunction SidePanelBody({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"side-panel-body\"\n      {...props}\n      {...stylex.props(styles.body, sx)}\n    />\n  );\n}\n\n/** Pinned below the body, which scrolls on its own. */\nfunction SidePanelFooter({ sx, ...props }: Styled<ComponentProps<\"div\">>) {\n  return (\n    <div\n      data-slot=\"side-panel-footer\"\n      {...props}\n      {...stylex.props(styles.footer, sx)}\n    />\n  );\n}\n\nexport {\n  Panel,\n  PanelBody,\n  PanelHeader,\n  PanelTitle,\n  SidePanel,\n  SidePanelBody,\n  SidePanelContent,\n  SidePanelDescription,\n  SidePanelFooter,\n  SidePanelHeader,\n  SidePanelSlot,\n  SidePanelTitle,\n};\nexport type { SidePanelContentProps };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/side-panel/index.md",
  "type": "registry:ui"
}