I'm just going to go ahead and say this is an awful and hacky solution, but I've already spend the time so why the hell not post it? Who knows; such hacks might be convenient for someone else.
The plan is: we're semi-manually going to replicate what ggridges does, but in vanilla ggplot2 and on a continuous scale. To do this, we'll be abusing the after_stat() function to (1) fill a ribbon with the densities and (2) superassign ourselves the maximum density, which we'll use as an offset.
library(ggplot2)
g <- ggplot(diamonds, aes(x = price, fill = cut)) +
stat_density(
geom = "ribbon", position = "identity",
aes(ymax = after_stat(density + (group - 1) * max(density)),
ymin = after_stat((group - 1) * {yoffset <<- max(density)}))
)
The flaw in this whole plan is that we don't know the offset in advance, so we'd need to build the plot at least once before we can actually use it.
build <- ggplot_build(g)
Lastly, we'll very manually assign the right values on the scales. We'll be using the primary for the discrete labels and the secondary for the continuous labels. You can look at the yoffset value that now should be in your global environment to get a sense of what the right breaks and labels are.
g + scale_y_continuous(
breaks = yoffset * (4:0), labels = levels(diamonds$cut),
sec.axis = dup_axis(
breaks = rep(yoffset * (0:4), 4) + rep(seq(0, 0.0003, by = 0.0001), each = 5),
labels = rep(seq(0, 0.0003, by = 0.0001), each = 5)
)
)

Created on 2021-07-15 by the reprex package (v1.0.0)
A slightly more generalisable way of setting the breaks/labels:
levels <- levels(diamonds$cut)
labels <- scales::extended_breaks(3)(c(0, yoffset * 0.8))
offsets <- yoffset * (seq(length(levels), 1) - 1)
g + scale_y_continuous(
breaks = offsets, labels = levels,
sec.axis = dup_axis(
breaks = rep(offsets, length(labels)) + rep(labels, each = length(offsets)),
labels = rep(labels, each = length(offsets))
)
)